var express = require('express'),
    path = require('path'),
    nodeapi = require('./lib/nodeapi'),
    favicon = require('serve-favicon'),
    logger = require('morgan'),
    cookieParser = require('cookie-parser'),
    settings = require('./lib/settings'),
    routes = require('./routes/index'),
    lib = require('./lib/explorer'),
    db = require('./lib/database'),
    package_metadata = require('./package.json');
var app = express();
var apiAccessList = [];
var viewPaths = [path.join(__dirname, 'views')]
var pluginRoutes = [];
const { exec } = require('child_process');

// pass wallet rpc connection info to nodeapi
nodeapi.setWalletDetails(settings.wallet);
// dynamically build the nodeapi cmd access list by adding all non-blockchain-specific api cmds that have a value
Object.keys(settings.api_cmds).forEach(function(key, index, map) {
  if (key != 'use_rpc' && key != 'rpc_concurrent_tasks' && settings.api_cmds[key] != null && settings.api_cmds[key] != '')
    apiAccessList.push(key);
});
// dynamically find and add additional blockchain_specific api cmds
Object.keys(settings.blockchain_specific).forEach(function(key, index, map) {
  // check if this feature is enabled and has api cmds
  if (settings.blockchain_specific[key].enabled == true && Object.keys(settings.blockchain_specific[key]).indexOf('api_cmds') > -1) {
    // add all blockchain specific api cmds that have a value
    Object.keys(settings.blockchain_specific[key]['api_cmds']).forEach(function(key2, index, map) {
      if (settings.blockchain_specific[key]['api_cmds'][key2] != null && settings.blockchain_specific[key]['api_cmds'][key2] != '')
        apiAccessList.push(key2);
    });
  }
});

// whitelist the cmds in the nodeapi access list
nodeapi.setAccess('only', apiAccessList);

// determine if http traffic should be forwarded to https
if (settings.webserver.tls.enabled == true && settings.webserver.tls.always_redirect == true) {
  app.use(function(req, res, next) {
    if (req.secure) {
      // continue without redirecting
      next();
    } else {
      // add webserver port to the host value if it does not already exist
      const host = req.headers.host + (req.headers.host.indexOf(':') > -1 ? '' : ':' + settings.webserver.port.toString());

      // redirect to the correct https page
      res.redirect(301, 'https://' + host.replace(':' + settings.webserver.port.toString(), (settings.webserver.tls.port != 443 ? ':' + settings.webserver.tls.port.toString() : '')) + req.url);
    }
  });
}

// determine if cors should be enabled
if (settings.webserver.cors.enabled == true) {
  app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", settings.webserver.cors.corsorigin);
    res.header('Access-Control-Allow-Methods', 'DELETE, PUT, GET, POST');
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
  });
}

// loop through all plugins defined in the settings
settings.plugins.allowed_plugins.forEach(function (plugin) {
  // check if this plugin is enabled
  if (plugin.enabled) {
    const pluginName = (plugin.plugin_name == null ? '' : plugin.plugin_name);

    // check if the plugin exists in the plugins directory
    if (db.fs.existsSync(`./plugins/${pluginName}`)) {
      // check if the plugin's local_plugin_settings file exists
      if (db.fs.existsSync(`./plugins/${pluginName}/lib/local_plugin_settings.js`)) {
        // load the local_plugin_settings.js file from the plugin
        let localPluginSettings = require(`./plugins/${pluginName}/lib/local_plugin_settings`);

        // loop through all local plugin settings
        Object.keys(localPluginSettings).forEach(function(key, index, map) {
          // check if this is a known setting type that should be brought into the main settings
          if (key.endsWith('_page') && typeof localPluginSettings[key] === 'object' && localPluginSettings[key]['enabled'] == true) {
            // this is a page setting
            // add the page_id to the page setting
            localPluginSettings[key].page_id = key;

            // add the menu item title to the page setting
            localPluginSettings[key].menu_title = localPluginSettings['localization'][`${key}_menu_title`];

            // check if there is already a page for this plugin
            if (plugin.pages == null) {
              // initialize the pages array
              plugin.pages = [];
            }

            // add this page setting to the main plugin data
            plugin['pages'].push(localPluginSettings[key]);
          } else if (key == 'public_apis') {
            // this is a collection of new apis
            // check if there is an ext section
            if (localPluginSettings[key]['ext'] != null) {
              // loop through all ext apis for this plugin
              Object.keys(localPluginSettings[key]['ext']).forEach(function(extKey, extIndex, extMap) {
                // add the name of the api into the object
                localPluginSettings[key]['ext'][extKey]['api_name'] = extKey;

                // loop through all parameters for this api and replace them in the description string if applicable
                for (let p = 0; p < localPluginSettings[key]['ext'][extKey]['api_parameters'].length; p++)
                  localPluginSettings['localization'][`${extKey}_description`] = localPluginSettings['localization'][`${extKey}_description`].replace(new RegExp(`\\{${(p + 1)}}`, 'g'), localPluginSettings[key]['ext'][extKey]['api_parameters'][p]['parameter_name']);

                // add the localized api description into the object
                localPluginSettings[key]['ext'][extKey]['api_desc'] = localPluginSettings['localization'][`${extKey}_description`];
              });
            }

            // copy the entire public_apis section from the plugin into the main settings
            plugin.public_apis = localPluginSettings[key];
          }
        });
      }

      // check if the plugin's routes/index.js file exists
      if (db.fs.existsSync(`./plugins/${pluginName}/routes/index.js`)) {
        // get the plugin routes and save them to an array
        pluginRoutes.push(require(`./plugins/${pluginName}/routes/index`));

        // check if the plugin has a views directory
        if (db.fs.existsSync(`./plugins/${pluginName}/views`)) {
          // get the list of files in the views directory
          const files = db.fs.readdirSync(`./plugins/${pluginName}/views`);

          // filter the list of files to check if any have the .pug extension
          const pugFiles = files.filter(file => path.extname(file) === '.pug');

          // check if the plugin has 1 or more views
          if (pugFiles.length > 0) {
            // add this plugins view path to the list of view paths
            viewPaths.push(path.resolve(`./plugins/${pluginName}/views`));
          }
        }
      }
    }
  }
});

// view engine setup
app.set('views', viewPaths);
app.set('view engine', 'pug');

var default_favicon = '';

// loop through the favicons
Object.keys(settings.shared_pages.favicons).forEach(function(key, index, map) {
  // remove the public directory from the path if exists
  if (settings.shared_pages.favicons[key] != null && settings.shared_pages.favicons[key].indexOf('public/') > -1)
    settings.shared_pages.favicons[key] = settings.shared_pages.favicons[key].replace(/public\//g, '');

  // check if the favicon file exists
  if (!db.fs.existsSync(path.join('./public', settings.shared_pages.favicons[key])))
    settings.shared_pages.favicons[key] = '';
  else if (default_favicon == '')
    default_favicon = settings.shared_pages.favicons[key];
});

if (default_favicon != '')
  app.use(favicon(path.join('./public', default_favicon)));

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// routes
app.use('/api', nodeapi.app);
app.use('/', routes);

// loop through all plugin routes and add them to the app
pluginRoutes.forEach(function (r) {
  app.use('/', r);
});

// post method to claim an address using verifymessage functionality
app.post('/claim', function(req, res) {
  // validate captcha if applicable
  validate_captcha(settings.claim_address_page.enable_captcha, req.body, function(captcha_error) {
    // check if there was a problem with captcha
    if (captcha_error) {
      // show the captcha error
      res.json({'status': 'failed', 'error': true, 'message': 'The captcha validation failed'});
    } else {
      // filter bad words if enabled
      filter_bad_words((req.body.message == null || req.body.message == '' ? '' : req.body.message), function(claim_error, message) {
        // check if there was an error or if the message was filtered
        if (claim_error != null) {
          // an error occurred with loading the bad-words filter
          res.json({'status': 'failed', 'error': true, 'message': 'Error loading the bad-words filter: ' + claim_error});
        } else if (message == req.body.message) {
          // call the verifymessage api
          lib.verify_message(req.body.address, req.body.signature, req.body.message, function(body) {
            if (body == false)
              res.json({'status': 'failed', 'error': true, 'message': 'Invalid signature'});
            else if (body == true) {
              db.update_claim_name(req.body.address, req.body.message, function(val) {
                // check if the update was successful
                if (val == '')
                  res.json({'status': 'success'});
                else if (val == 'no_address')
                  res.json({'status': 'failed', 'error': true, 'message': 'Wallet address ' + req.body.address + ' is not valid or does not have any transactions'});
                else
                  res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
              });
            } else
              res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
          });
        } else {
          // message was filtered which would change the signature
          res.json({'status': 'failed', 'error': true, 'message': 'Display name contains bad words and cannot be saved: ' + message});
        }
      });
    }
  });
});

function validate_captcha(captcha_enabled, data, cb) {
  // check if captcha is enabled for the requested feature
  if (captcha_enabled == true) {
    // determine the captcha type
    if (settings.captcha.google_recaptcha3.enabled == true) {
      if (data.google_recaptcha3 != null) {
        const request = require('postman-request');

        request({uri: 'https://www.google.com/recaptcha/api/siteverify?secret=' + settings.captcha.google_recaptcha3.secret_key + '&response=' + data.google_recaptcha3, json: true}, function (error, response, body) {
          if (error) {
            // an error occurred while trying to validate the captcha
            return cb(true);
          } else if (body == null || body == '' || typeof body !== 'object') {
            // return data is invalid
            return cb(true);
          } else if (body.score == null || body.score < settings.captcha.google_recaptcha3.pass_score) {
            // captcha challenge failed
            return cb(true);
          } else {
            // captcha challenge passed
            return cb(false);
          }
        });
      } else {
        // a captcha response wasn't received
        return cb(true);
      }
    } else if (settings.captcha.google_recaptcha2.enabled == true) {
      if (data.google_recaptcha2 != null) {
        const request = require('postman-request');

        request({uri: 'https://www.google.com/recaptcha/api/siteverify?secret=' + settings.captcha.google_recaptcha2.secret_key + '&response=' + data.google_recaptcha2, json: true}, function (error, response, body) {
          if (error) {
            // an error occurred while trying to validate the captcha
            return cb(true);
          } else if (body == null || body == '' || typeof body !== 'object') {
            // return data is invalid
            return cb(true);
          } else if (body.success == null || body.success == false) {
            // captcha challenge failed
            return cb(true);
          } else {
            // captcha challenge passed
            return cb(false);
          }
        });
      } else {
        // a captcha response wasn't received
        return cb(true);
      }
    } else if (settings.captcha.hcaptcha.enabled == true) {
      if (data.hcaptcha != null) {
        const request = require('postman-request');

        request({uri: 'https://hcaptcha.com/siteverify?secret=' + settings.captcha.hcaptcha.secret_key + '&response=' + data.hcaptcha, json: true}, function (error, response, body) {
          if (error) {
            // an error occurred while trying to validate the captcha
            return cb(true);
          } else if (body == null || body == '' || typeof body !== 'object') {
            // return data is invalid
            return cb(true);
          } else if (body.success == null || body.success == false) {
            // captcha challenge failed
            return cb(true);
          } else {
            // captcha challenge passed
            return cb(false);
          }
        });
      } else {
        // a captcha response wasn't received
        return cb(true);
      }
    } else {
      // no captcha options are enabled
      return cb(false);
    }
  } else {
    // captcha is not enabled for this feature
    return cb(false);
  }
}

function filter_bad_words(msg, cb) {
  // check if the bad-words filter is enabled
  if (settings.claim_address_page.enable_bad_word_filter == true) {
    // import the bad-words dependency
    import('bad-words').then(function(module) {
      // load the bad-words filter
      const bad_word_lib = module.Filter;
      const bad_word_filter = new bad_word_lib();

       // return the filtered msg
      return cb(null, bad_word_filter.clean(msg));
    })
    .catch(function(err) {
      return cb(err, null);
    });
  } else {
    // return the msg without filtering for bad words
    return cb(null, msg);
  }
}

// post method to receive data from a plugin
app.post('/plugin-request', function(req, res) {
  const pluginLockName = 'plugin';

  // check if another plugin request is already running
  if (lib.is_locked([pluginLockName], true) == true)
    res.json({'status': 'failed', 'error': true, 'message': `Another plugin request is already running..`});
  else {
    // create a new plugin lock before checking the rest of the locks to minimize problems with running scripts at the same time
    lib.create_lock(pluginLockName);

    // check the backup, restore and delete locks since those functions would be problematic when updating data
    if (lib.is_locked(['backup', 'restore', 'delete'], true) == true) {
      lib.remove_lock(pluginLockName);
      res.json({'status': 'failed', 'error': true, 'message': `Another script has locked the database..`});
    } else {
      // all lock tests passed. OK to run plugin request

      let dataObject = {};

      try {
        // attempt to parse the POST data field into a JSON object
        dataObject = JSON.parse(req.body.data);
      } catch {
        // do nothing. errors will be handled below
      }

      // check if the dataObject was populated
      if (dataObject == null || JSON.stringify(dataObject) === '{}') {
        lib.remove_lock(pluginLockName);
        res.json({'status': 'failed', 'error': true, 'message': 'POST data is missing or not in the correct format'});
      } else {
        // check if the plugin secret code is correct and if the coin name was specified
        if (dataObject.plugin_data == null || settings.plugins.plugin_secret_code != dataObject.plugin_data.secret_code) {
          lib.remove_lock(pluginLockName);
          res.json({'status': 'failed', 'error': true, 'message': 'Secret code is missing or incorrect'});
        } else if (dataObject.plugin_data.coin_name == null || dataObject.plugin_data.coin_name == '') {
          lib.remove_lock(pluginLockName);
          res.json({'status': 'failed', 'error': true, 'message': 'Coin name is missing'});
        } else {
          const tableData = dataObject.table_data;

          // check if the table_data seems valid
          if (tableData == null || !Array.isArray(tableData)) {
            lib.remove_lock(pluginLockName);
            res.json({'status': 'failed', 'error': true, 'message': `table_data from POST data is missing or empty`});
          } else {
            const pluginName = (dataObject.plugin_data.plugin_name == null ? '' : dataObject.plugin_data.plugin_name);
            const pluginObj = settings.plugins.allowed_plugins.find(item => item.plugin_name === pluginName && pluginName != '');

            // check if the requested plugin was found in the settings
            if (pluginObj == null) {
              lib.remove_lock(pluginLockName);
              res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not defined in settings`});
            } else {
              // check if the requested plugin is enabled
              if (!pluginObj.enabled) {
                lib.remove_lock(pluginLockName);
                res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not enabled`});
              } else {
                // check if the plugin exists in the plugins directory
                if (!db.fs.existsSync(`./plugins/${pluginName}`)) {
                  lib.remove_lock(pluginLockName);
                  res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not installed in the plugins directory`});
                } else {
                  // check if the plugin's server_functions file exists
                  if (!db.fs.existsSync(`./plugins/${pluginName}/lib/server_functions.js`)) {
                    lib.remove_lock(pluginLockName);
                    res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is missing the /lib/server_functions.js file`});
                  } else {
                    // load the server_functions.js file from the plugin
                    const serverFunctions = require(`./plugins/${pluginName}/lib/server_functions`);

                    // check if the process_plugin_request function exists
                    if (typeof serverFunctions.process_plugin_request !== 'function') {
                      lib.remove_lock(pluginLockName);
                      res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is missing the process_plugin_request function`});
                    } else {
                      // call the process_plugin_request function to process the new table data
                      serverFunctions.process_plugin_request(dataObject.plugin_data.coin_name, tableData, settings.sync.update_timeout, function(response) {
                        lib.remove_lock(pluginLockName);
                        res.json(response);
                      });
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
});

// extended apis
app.use('/ext/getmoneysupply', function(req, res) {
  // check if the getmoneysupply api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmoneysupply.enabled == true) {
    // lookup stats
    db.get_stats(settings.coin.name, function (stats) {
      res.setHeader('content-type', 'text/plain');
      res.end((stats && stats.supply ? stats.supply.toString() : '0'));
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getaddress/:hash', function(req, res) {
  // check if the getaddress api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getaddress.enabled == true) {
    db.get_address(req.params.hash, false, function(address) {
      db.get_address_txs_ajax(req.params.hash, 0, settings.api_page.public_apis.ext.getaddresstxs.max_items_per_query, function(txs, count) {
        if (address) {
          var last_txs = [];

          for (i = 0; i < txs.length; i++) {
            if (typeof txs[i].txid !== "undefined") {
              var out = 0,
                  vin = 0,
                  tx_type = 'vout',
                  row = {};

              txs[i].vout.forEach(function (r) {
                if (r.addresses == req.params.hash)
                  out += r.amount;
              });

              txs[i].vin.forEach(function (s) {
                if (s.addresses == req.params.hash)
                  vin += s.amount;
              });

              if (vin > out)
                tx_type = 'vin';

              row['addresses'] = txs[i].txid;
              row['type'] = tx_type;
              last_txs.push(row);
            }
          }

          var a_ext = {
            address: address.a_id,
            sent: (address.sent / 100000000),
            received: (address.received / 100000000),
            balance: (address.balance / 100000000).toString().replace(/(^-+)/mg, ''),
            last_txs: last_txs
          };

          res.send(a_ext);
        } else
          res.send({ error: 'address not found.', hash: req.params.hash});
      });
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/gettx/:txid', function(req, res) {
  // check if the gettx api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.gettx.enabled == true) {
    var txid = req.params.txid;

    db.get_tx(txid, function(tx) {
      if (tx) {
        lib.get_blockcount(function(blockcount) {
          res.send({ active: 'tx', tx: tx, confirmations: (blockcount - tx.blockindex + 1), blockcount: (blockcount ? blockcount : 0)});
        });
      } else {
        lib.get_rawtransaction(txid, function(rtx) {
          if (rtx && rtx.txid) {
            lib.prepare_vin(rtx, function(vin, tx_type_vin) {
              lib.prepare_vout(rtx.vout, rtx.txid, vin, ((typeof rtx.vjoinsplit === 'undefined' || rtx.vjoinsplit == null) ? [] : rtx.vjoinsplit), function(rvout, rvin, tx_type_vout) {
                const total = lib.calculate_total(rvout);

                if (!rtx.confirmations > 0) {
                  var utx = {
                    txid: rtx.txid,
                    vin: rvin,
                    vout: rvout,
                    total: total.toFixed(8),
                    timestamp: rtx.time,
                    blockhash: '-',
                    blockindex: -1
                  };

                  res.send({ active: 'tx', tx: utx, confirmations: rtx.confirmations, blockcount:-1});
                } else {
                  var utx = {
                    txid: rtx.txid,
                    vin: rvin,
                    vout: rvout,
                    total: total.toFixed(8),
                    timestamp: rtx.time,
                    blockhash: rtx.blockhash,
                    blockindex: rtx.blockheight
                  };

                  lib.get_blockcount(function(blockcount) {
                    res.send({ active: 'tx', tx: utx, confirmations: rtx.confirmations, blockcount: (blockcount ? blockcount : 0)});
                  });
                }
              });
            });
          } else
            res.send({ error: 'tx not found.', hash: txid});
        });
      }
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getbalance/:hash', function(req, res) {
  // check if the getbalance api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getbalance.enabled == true) {
    db.get_address(req.params.hash, false, function(address) {
      if (address) {
        res.setHeader('content-type', 'text/plain');
        res.end((address.balance / 100000000).toString().replace(/(^-+)/mg, ''));
      } else
        res.send({ error: 'address not found.', hash: req.params.hash });
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getdistribution', function(req, res) {
  // check if the getdistribution api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getdistribution.enabled == true) {
    db.get_richlist(settings.coin.name, function(richlist) {
      db.get_stats(settings.coin.name, function(stats) {
        db.get_distribution(richlist, stats, function(dist) {
          res.send(dist);
        });
      });
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getcurrentprice', function(req, res) {
  // check if the getcurrentprice api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getcurrentprice.enabled == true) {
    db.get_stats(settings.coin.name, function (stats) {
      const currency = lib.get_market_currency_code();

      eval('var p_ext = { "last_price_' + currency.toLowerCase() + '": stats.last_price, "last_price_usd": stats.last_usd_price, }');
      res.send(p_ext);
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getbasicstats', function(req, res) {
  // check if the getbasicstats api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getbasicstats.enabled == true) {
    // lookup stats
    db.get_stats(settings.coin.name, function (stats) {
      const currency = lib.get_market_currency_code();

      // check if the masternode count api is enabled
      if (settings.api_page.public_apis.rpc.getmasternodecount.enabled == true && settings.api_cmds['getmasternodecount'] != null && settings.api_cmds['getmasternodecount'] != '') {
        // masternode count api is available
        lib.get_masternodecount(function(masternodestotal) {
          eval('var p_ext = { "block_count": (stats.count ? stats.count : 0), "money_supply": (stats.supply ? stats.supply : 0), "last_price_' + currency.toLowerCase() + '": stats.last_price, "last_price_usd": stats.last_usd_price, "masternode_count": masternodestotal.total }');
          res.send(p_ext);
        });
      } else {
        // masternode count api is not available
        eval('var p_ext = { "block_count": (stats.count ? stats.count : 0), "money_supply": (stats.supply ? stats.supply : 0), "last_price_' + currency.toLowerCase() + '": stats.last_price, "last_price_usd": stats.last_usd_price }');
        res.send(p_ext);
      }
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getlasttxs/:min', function(req, res) {
  // check if the getlasttxs api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
  if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getlasttxs.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
    var min = req.params.min, start, length, internal = false;
    // split url suffix by forward slash and remove blank entries
    var split = req.url.split('/').filter(function(v) { return v; });
    // determine how many parameters were passed
    switch (split.length) {
      case 2:
        // capture start and length
        start = split[0];
        length = split[1];
        break;
      default:
        if (split.length == 1) {
          // capture start
          start = split[0];
        } else if (split.length >= 2) {
          // capture start and length
          start = split[0];
          length = split[1];
          // check if this is an internal request
          if (split.length > 2 && split[2] == 'internal')
            internal = true;
        }

        break;
    }

    // fix parameters
    if (typeof length === 'undefined' || isNaN(length) || length > settings.api_page.public_apis.ext.getlasttxs.max_items_per_query)
      length = settings.api_page.public_apis.ext.getlasttxs.max_items_per_query;
    if (typeof start === 'undefined' || isNaN(start) || start < 0)
      start = 0;
    if (typeof min === 'undefined' || isNaN(min) || min < 0)
      min  = 0;
    else
      min  = (min * 100000000);

    db.get_last_txs(start, length, min, internal, function(data, count) {
      // check if this is an internal request
      if (internal) {
        // display data formatted for internal datatable
        res.json({"data": data, "recordsTotal": count, "recordsFiltered": count});
      } else {
        // display data in more readable format for public api
        res.json(data);
      }
    });
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getaddresstxs/:address/:start/:length', function(req, res) {
  // check if the getaddresstxs api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
  if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getaddresstxs.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
    var internal = false;
    // split url suffix by forward slash and remove blank entries
    var split = req.url.split('/').filter(function(v) { return v; });
    // check if this is an internal request
    if (split.length > 0 && split[0] == 'internal')
      internal = true;
    // fix parameters
    if (typeof req.params.length === 'undefined' || isNaN(req.params.length) || req.params.length > settings.api_page.public_apis.ext.getaddresstxs.max_items_per_query)
      req.params.length = settings.api_page.public_apis.ext.getaddresstxs.max_items_per_query;
    if (typeof req.params.start === 'undefined' || isNaN(req.params.start) || req.params.start < 0)
      req.params.start = 0;
    if (typeof req.params.min === 'undefined' || isNaN(req.params.min) || req.params.min < 0)
      req.params.min  = 0;
    else
      req.params.min  = (req.params.min * 100000000);

    db.get_address_txs_ajax(req.params.address, req.params.start, req.params.length, function(txs, count) {
      var data = [];

      for (i = 0; i < txs.length; i++) {
        if (typeof txs[i].txid !== "undefined") {
          var out = 0;
          var vin = 0;

          txs[i].vout.forEach(function(r) {
            if (r.addresses == req.params.address)
              out += r.amount;
          });

          txs[i].vin.forEach(function(s) {
            if (s.addresses == req.params.address)
              vin += s.amount;
          });

          if (internal) {
            var row = [];

            row.push(txs[i].timestamp);
            row.push(txs[i].txid);
            row.push(Number(out / 100000000));
            row.push(Number(vin / 100000000));
            row.push(Number(txs[i].balance / 100000000));

            data.push(row);
          } else {
            data.push({
              timestamp: txs[i].timestamp,
              txid: txs[i].txid,
              sent: Number(out / 100000000),
              received: Number(vin / 100000000),
              balance: Number(txs[i].balance / 100000000)
            });
          }
        }
      }

      // check if this is an internal request
      if (internal) {
        // display data formatted for internal datatable
        res.json({"data": data, "recordsTotal": count, "recordsFiltered": count});
      } else {
        // display data in more readable format for public api
        res.json(data);
      }
    });
  } else
    res.end(settings.localization.method_disabled);
});

function get_connection_and_block_counts(get_data, cb) {
  // check if the connection and block counts should be returned
  if (get_data) {
    lib.get_connectioncount(function(connections) {
      lib.get_blockcount(function(blockcount) {
        return cb(connections, blockcount);
      });
    });
  } else
    return cb(null, null);
}

app.use('/ext/getsummary', function(req, res) {
  const isInternal = (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1);

  // check if the getsummary api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
  if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getsummary.enabled == true) || isInternal) {
    // check if this is a footer-only method that should only return the connection count and block count
    if (req.headers['footer-only'] != null && req.headers['footer-only'] == 'true') {
      // only return the connection count and block count
      get_connection_and_block_counts(true, function(connections, blockcount) {
        res.send({
          connections: (connections ? connections : '-'),
          blockcount: (blockcount ? blockcount : '-')
        });
      });
    } else {
      // get the connection and block counts only if this is NOT an internal call
      get_connection_and_block_counts(!isInternal, function(connections, blockcount) {
        lib.get_hashrate(function(hashrate) {
          db.get_stats(settings.coin.name, function (stats) {
            lib.get_masternodecount(function(masternodestotal) {
              lib.get_difficulty(function(difficulty) {
                let difficultyHybrid = '';

                if (difficulty && difficulty['proof-of-work']) {
                  if (settings.shared_pages.difficulty == 'Hybrid') {
                    difficultyHybrid = 'POS: ' + difficulty['proof-of-stake'];
                    difficulty = 'POW: ' + difficulty['proof-of-work'];
                  } else if (settings.shared_pages.difficulty == 'POW')
                    difficulty = difficulty['proof-of-work'];
                  else
                    difficulty = difficulty['proof-of-stake'];
                }

                if (hashrate == `${settings.localization.ex_error}: ${settings.localization.check_console}`)
                  hashrate = 0;

                let mn_total = 0;
                let mn_enabled = 0;

                // check if the masternode count api is enabled
                if (settings.api_page.public_apis.rpc.getmasternodecount.enabled == true && settings.api_cmds['getmasternodecount'] != null && settings.api_cmds['getmasternodecount'] != '') {
                  // masternode count api is available
                  if (masternodestotal) {
                    if (masternodestotal.total)
                      mn_total = masternodestotal.total;

                    if (masternodestotal.enabled)
                      mn_enabled = masternodestotal.enabled;
                  }
                }

                res.send({
                  difficulty: (difficulty ? difficulty : '-'),
                  difficultyHybrid: difficultyHybrid,
                  supply: (stats == null || stats.supply == null ? 0 : stats.supply),
                  hashrate: hashrate,
                  lastPrice: (stats == null || stats.last_price == null ? 0 : stats.last_price),
                  lastUSDPrice: (stats == null || stats.last_usd_price == null ? 0 : stats.last_usd_price),
                  connections: (connections ? connections : '-'),
                  blockcount: (blockcount ? blockcount : '-'),
                  masternodeCountOnline: (masternodestotal && mn_enabled != 0 ? mn_enabled : '-'),
                  masternodeCountOffline: (masternodestotal && mn_total != 0 ? Math.floor(mn_total - mn_enabled) : '-')
                });
              });
            });
          });
        });
      });
    }
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getnetworkpeers', function(req, res) {
  // check if the getnetworkpeers api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
  if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getnetworkpeers.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
    // get list of peers
    db.get_peers(function(peers) {
      // loop through peers list and remove the mongo _id and __v keys
      for (i = 0; i < peers.length; i++) {
        delete peers[i]['_doc']['_id'];
        delete peers[i]['_doc']['__v'];
      }

      // sort ip6 addresses to the bottom
      peers.sort(function(a, b) {
        var address1 = a.address.indexOf(':') > -1;
        var address2 = b.address.indexOf(':') > -1;

        if (address1 < address2)
          return -1;
        else if (address1 > address2)
          return 1;
        else
          return 0;
      });

      // return peer data
      res.json(peers);
    });
  } else
    res.end(settings.localization.method_disabled);
});

// get the list of masternodes from local collection
app.use('/ext/getmasternodelist', function(req, res) {
  // check if the getmasternodelist api is enabled or else check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
  if ((settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmasternodelist.enabled == true) || (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1)) {
    // get the masternode list from local collection
    db.get_masternodes(function(masternodes) {
      // loop through masternode list and remove the mongo _id and __v keys
      for (i = 0; i < masternodes.length; i++) {
        delete masternodes[i]['_doc']['_id'];
        delete masternodes[i]['_doc']['__v'];
      }

      // return masternode list
      res.send(masternodes);
    });
  } else
    res.end(settings.localization.method_disabled);
});

// returns a list of masternode reward txs for a single masternode address from a specific block height
app.use('/ext/getmasternoderewards/:hash/:since', function(req, res) {
  // check if the getmasternoderewards api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmasternoderewards.enabled == true) {
    db.get_masternode_rewards(req.params.hash, req.params.since, function(rewards) {
      if (rewards != null) {
        // loop through the tx list to fix vout values and remove unnecessary data such as the always empty vin array and the mongo _id and __v keys
        for (i = 0; i < rewards.length; i++) {
          // remove unnecessary data keys
          delete rewards[i]['vin'];
          delete rewards[i]['_id'];
          delete rewards[i]['__v'];
          // convert amounts from satoshis
          rewards[i]['total'] = rewards[i]['total'] / 100000000;
          rewards[i]['vout']['amount'] = rewards[i]['vout']['amount'] / 100000000;
        }

        // return list of masternode rewards
        res.json(rewards);
      } else
        res.send({error: "failed to retrieve masternode rewards", hash: req.params.hash, since: req.params.since});
    });
  } else
    res.end(settings.localization.method_disabled);
});

// returns the total masternode rewards received for a single masternode address from a specific block height
app.use('/ext/getmasternoderewardstotal/:hash/:since', function(req, res) {
  // check if the getmasternoderewardstotal api is enabled
  if (settings.api_page.enabled == true && settings.api_page.public_apis.ext.getmasternoderewardstotal.enabled == true) {
    db.get_masternode_rewards_totals(req.params.hash, req.params.since, function(total_rewards) {
      if (total_rewards != null) {
        // return the total of masternode rewards
        res.json(total_rewards);
      } else
        res.send({error: "failed to retrieve masternode rewards", hash: req.params.hash, since: req.params.since});
    });
  } else
    res.end(settings.localization.method_disabled);
});

// get the list of orphans from local collection
app.use('/ext/getorphanlist/:start/:length', function(req, res) {
  // check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
  if (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1) {
    // fix parameters
    if (typeof req.params.start === 'undefined' || isNaN(req.params.start) || req.params.start < 0)
      req.params.start = 0;
    if (typeof req.params.length === 'undefined' || isNaN(req.params.length))
      req.params.length = 10;

    // get the orphan list from local collection
    db.get_orphans(req.params.start, req.params.length, function(orphans, count) {
      var data = [];

      for (i = 0; i < orphans.length; i++) {
        var row = [];

        row.push(orphans[i].blockindex);
        row.push(orphans[i].orphan_blockhash);
        row.push(orphans[i].good_blockhash);
        row.push(orphans[i].prev_blockhash);
        row.push(orphans[i].next_blockhash);

        data.push(row);
      }

      // display data formatted for internal datatable
      res.json({"data": data, "recordsTotal": count, "recordsFiltered": count});
    });
  } else
    res.end(settings.localization.method_disabled);
});

// get the last updated date for a particular section
app.use('/ext/getlastupdated/:section', function(req, res) {
  // check the headers to see if it matches an internal ajax request from the explorer itself (TODO: come up with a more secure method of whitelisting ajax calls from the explorer)
  if (req.headers['x-requested-with'] != null && req.headers['x-requested-with'].toLowerCase() == 'xmlhttprequest' && req.headers.referer != null && req.headers.accept.indexOf('text/javascript') > -1 && req.headers.accept.indexOf('application/json') > -1) {
    // fix parameters
    if (req.params.section == null)
      req.params.section = '';

    switch (req.params.section.toLowerCase()) {
      case 'blockchain':
      case 'movement':
        // lookup last updated date
        db.get_stats(settings.coin.name, function (stats) {
          res.json({'last_updated_date': stats.blockchain_last_updated});
        });
        break;
      default:
        res.send({error: 'Cannot find last updated date'});
    }
  } else
    res.end(settings.localization.method_disabled);
});

app.use('/ext/getnetworkchartdata', function(req, res) {
  db.get_network_chart_data(function(data) {
    if (data)
      res.send(data);
    else
      res.send();
  });
});

app.use('/system/restartexplorer', function(req, res, next) {
  // check to ensure this special cmd is only executed by the local server
  if (req._remoteAddress != null && req._remoteAddress.indexOf('127.0.0.1') > -1) {
    // send a msg to the cluster process telling it to restart
    process.send('restart');
    res.end();
  } else {
    // show the error page
    var err = new Error(settings.localization.error_not_found);
    err.status = 404;
    next(err);
  }
});

var market_data = [];
var market_count = 0;

// check if markets are enabled
if (settings.markets_page.enabled == true) {
  // dynamically populate market data
  Object.keys(settings.markets_page.exchanges).forEach(function (key, index, map) {
    // check if market is enabled via settings
    if (settings.markets_page.exchanges[key].enabled == true) {
      // check if market is installed/supported
      if (db.fs.existsSync('./lib/markets/' + key + '.js')) {
        // load market file
        var exMarket = require('./lib/markets/' + key);
        // save market_name and market_logo from market file to settings
        eval('market_data.push({id: "' + key + '", name: "' + (exMarket.market_name == null ? '' : exMarket.market_name) + '", alt_name: "' + (exMarket.market_name_alt == null ? '' : exMarket.market_name_alt) + '", logo: "' + (exMarket.market_logo == null ? '' : exMarket.market_logo) + '", alt_logo: "' + (exMarket.market_logo_alt == null ? '' : exMarket.market_logo_alt) + '", trading_pairs: []});');
        // loop through all trading pairs for this market
        for (var i = 0; i < settings.markets_page.exchanges[key].trading_pairs.length; i++) {
          var isAlt = false;
          var pair = settings.markets_page.exchanges[key].trading_pairs[i].toUpperCase(); // ensure trading pair setting is always uppercase
          var coin_symbol = pair.split('/')[0];
          var pair_symbol = pair.split('/')[1];

          // determine if using the alt name + logo
          if (exMarket.market_url_template != null && exMarket.market_url_template != '') {
            switch ((exMarket.market_url_case == null || exMarket.market_url_case == '' ? 'l' : exMarket.market_url_case.toLowerCase())) {
              case 'l':
              case 'lower':
                isAlt = (exMarket.isAlt != null ? exMarket.isAlt({coin: coin_symbol.toLowerCase(), exchange: pair_symbol.toLowerCase()}) : false);
                break;
              case 'u':
              case 'upper':
                isAlt = (exMarket.isAlt != null ? exMarket.isAlt({coin: coin_symbol.toUpperCase(), exchange: pair_symbol.toUpperCase()}) : false);
                break;
              default:
            }
          }

          // add trading pair to market_data
          market_data[market_data.length - 1].trading_pairs.push({
            pair: pair,
            isAlt: isAlt
          });

          // increment the market count
          market_count++;
        }

        // sort trading pairs by alt status
        market_data[market_data.length - 1].trading_pairs.sort(function(a, b) {
          if (a.isAlt < b.isAlt)
            return -1;
          else if (a.isAlt > b.isAlt)
            return 1;
          else
            return 0;
        });
      }
    }
  });

  // sort market data by market name
  market_data.sort(function(a, b) {
    var name1 = a.name.toLowerCase();
    var name2 = b.name.toLowerCase();

    if (name1 < name2)
      return -1;
    else if (name1 > name2)
      return 1;
    else
      return 0;
  });

  // fix default exchange name case
  if (settings.markets_page.default_exchange.exchange_name != null)
    settings.markets_page.default_exchange.exchange_name = settings.markets_page.default_exchange.exchange_name.toLowerCase();
  else
    settings.markets_page.default_exchange.exchange_name = '';

  // fix default exchange trading pair case
  if (settings.markets_page.default_exchange.trading_pair != null)
    settings.markets_page.default_exchange.trading_pair = settings.markets_page.default_exchange.trading_pair.toUpperCase();
  else
    settings.markets_page.default_exchange.trading_pair = '';

  var ex = settings.markets_page.exchanges;
  var ex_name = settings.markets_page.default_exchange.exchange_name;
  var ex_pair = settings.markets_page.default_exchange.trading_pair;
  var ex_keys = Object.keys(ex);
  var ex_error = '';

  // check to ensure default market and trading pair exist and are enabled
  if (ex[ex_name] == null) {
    // exchange name does not exist in exchanges list
    ex_error = 'Default exchange name is not valid' + ': ' + ex_name;
  } else if (!ex[ex_name].enabled) {
    // exchange is not enabled
    ex_error = 'Default exchange is disabled in settings' + ': ' + ex_name;
  } else if (ex[ex_name].trading_pairs.findIndex(p => p.toUpperCase() == ex_pair.toUpperCase()) == -1) {
    // invalid default exchange trading pair
    ex_error = 'Default exchange trading pair is not valid' + ': ' + ex_pair;
  }

  // check if there was an error msg
  if (ex_error != '') {
    // there was an error, so find the next available market from settings.json
    var new_default_index = -1;

    // find the first enabled exchange with at least one trading pair
    for (var i = 0; i < ex_keys.length; i++) {
      if (ex[ex_keys[i]]['enabled'] === true && ex[ex_keys[i]]['trading_pairs'].length > 0) {
        // found a match so save the index
        new_default_index = i;
        // stop looking for more matches
        break;
      }
    }

    // check if a valid and enabled market was found
    if (new_default_index == -1) {
      // no valid markets found
      console.log('WARNING: ' + ex_error + '. ' + 'No valid or enabled markets found in settings.json. The markets feature will be temporarily disabled. To restore markets functionality, please enable at least 1 market and ensure at least 1 valid trading pair is added. Finally, restart the explorer to resolve the problem');
      // disable the markets feature for this session
      settings.markets_page.enabled = false;
    } else {
      // a valid and enabled market was found to replace the default
      console.log('WARNING: ' + ex_error + '. ' + 'Default exchange will be set to' + ': ' + ex_keys[new_default_index] + '[' + ex[ex_keys[new_default_index]].trading_pairs[0].toUpperCase() + ']');
      // set new default exchange data
      settings.markets_page.default_exchange.exchange_name = ex_keys[new_default_index];
      settings.markets_page.default_exchange.trading_pair = ex[ex_keys[new_default_index]].trading_pairs[0].toUpperCase();
    }
  }
}

// check if home_link_logo file exists
if (!db.fs.existsSync(path.join('./public', settings.shared_pages.page_header.home_link_logo)))
  settings.shared_pages.page_header.home_link_logo = '';

// always disable the rpc masternode list cmd from public apis
settings.api_page.public_apis.rpc.getmasternodelist = { "enabled": false };

// locals
app.set('explorer_version', package_metadata.version);
app.set('localization', settings.localization);
app.set('coin', settings.coin);
app.set('network_history', settings.network_history);
app.set('shared_pages', settings.shared_pages);
app.set('index_page', settings.index_page);
app.set('block_page', settings.block_page);
app.set('transaction_page', settings.transaction_page);
app.set('address_page', settings.address_page);
app.set('error_page', settings.error_page);
app.set('masternodes_page', settings.masternodes_page);
app.set('movement_page', settings.movement_page);
app.set('network_page', settings.network_page);
app.set('richlist_page', settings.richlist_page);
app.set('markets_page', settings.markets_page);
app.set('api_page', settings.api_page);
app.set('claim_address_page', settings.claim_address_page);
app.set('orphans_page', settings.orphans_page);
app.set('captcha', settings.captcha);
app.set('labels', settings.labels);
app.set('default_coingecko_ids', settings.default_coingecko_ids);
app.set('api_cmds', settings.api_cmds);
app.set('blockchain_specific', settings.blockchain_specific);
app.set('plugins', settings.plugins);

// determine panel offset based on which panels are enabled
var paneltotal = 5;
var panelcount = (settings.shared_pages.page_header.panels.network_panel.enabled == true && settings.shared_pages.page_header.panels.network_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.difficulty_panel.enabled == true && settings.shared_pages.page_header.panels.difficulty_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.masternodes_panel.enabled == true && settings.shared_pages.page_header.panels.masternodes_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.coin_supply_panel.enabled == true && settings.shared_pages.page_header.panels.coin_supply_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.price_panel.enabled == true && settings.shared_pages.page_header.panels.price_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.usd_price_panel.enabled == true && settings.shared_pages.page_header.panels.usd_price_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.market_cap_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.usd_market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.usd_market_cap_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.logo_panel.enabled == true && settings.shared_pages.page_header.panels.logo_panel.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.spacer_panel_1.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_1.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.spacer_panel_2.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_2.display_order > 0 ? 1 : 0) +
  (settings.shared_pages.page_header.panels.spacer_panel_3.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_3.display_order > 0 ? 1 : 0);
app.set('paneloffset', paneltotal + 1 - panelcount);

// determine panel order
var panel_order = new Array();

if (settings.shared_pages.page_header.panels.network_panel.enabled == true && settings.shared_pages.page_header.panels.network_panel.display_order > 0) panel_order.push({name: 'network_panel', val: settings.shared_pages.page_header.panels.network_panel.display_order});
if (settings.shared_pages.page_header.panels.difficulty_panel.enabled == true && settings.shared_pages.page_header.panels.difficulty_panel.display_order > 0) panel_order.push({name: 'difficulty_panel', val: settings.shared_pages.page_header.panels.difficulty_panel.display_order});
if (settings.shared_pages.page_header.panels.masternodes_panel.enabled == true && settings.shared_pages.page_header.panels.masternodes_panel.display_order > 0) panel_order.push({name: 'masternodes_panel', val: settings.shared_pages.page_header.panels.masternodes_panel.display_order});
if (settings.shared_pages.page_header.panels.coin_supply_panel.enabled == true && settings.shared_pages.page_header.panels.coin_supply_panel.display_order > 0) panel_order.push({name: 'coin_supply_panel', val: settings.shared_pages.page_header.panels.coin_supply_panel.display_order});
if (settings.shared_pages.page_header.panels.price_panel.enabled == true && settings.shared_pages.page_header.panels.price_panel.display_order > 0) panel_order.push({name: 'price_panel', val: settings.shared_pages.page_header.panels.price_panel.display_order});
if (settings.shared_pages.page_header.panels.usd_price_panel.enabled == true && settings.shared_pages.page_header.panels.usd_price_panel.display_order > 0) panel_order.push({name: 'usd_price_panel', val: settings.shared_pages.page_header.panels.usd_price_panel.display_order});
if (settings.shared_pages.page_header.panels.market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.market_cap_panel.display_order > 0) panel_order.push({name: 'market_cap_panel', val: settings.shared_pages.page_header.panels.market_cap_panel.display_order});
if (settings.shared_pages.page_header.panels.usd_market_cap_panel.enabled == true && settings.shared_pages.page_header.panels.usd_market_cap_panel.display_order > 0) panel_order.push({name: 'usd_market_cap_panel', val: settings.shared_pages.page_header.panels.usd_market_cap_panel.display_order});
if (settings.shared_pages.page_header.panels.logo_panel.enabled == true && settings.shared_pages.page_header.panels.logo_panel.display_order > 0) panel_order.push({name: 'logo_panel', val: settings.shared_pages.page_header.panels.logo_panel.display_order});
if (settings.shared_pages.page_header.panels.spacer_panel_1.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_1.display_order > 0) panel_order.push({name: 'spacer_panel_1', val: settings.shared_pages.page_header.panels.spacer_panel_1.display_order});
if (settings.shared_pages.page_header.panels.spacer_panel_2.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_2.display_order > 0) panel_order.push({name: 'spacer_panel_2', val: settings.shared_pages.page_header.panels.spacer_panel_2.display_order});
if (settings.shared_pages.page_header.panels.spacer_panel_3.enabled == true && settings.shared_pages.page_header.panels.spacer_panel_3.display_order > 0) panel_order.push({name: 'spacer_panel_3', val: settings.shared_pages.page_header.panels.spacer_panel_3.display_order});

panel_order.sort(function(a,b) { return a.val - b.val; });

for (var i = 1; i < 6; i++)
  app.set('panel'+i.toString(), ((panel_order.length >= i) ? panel_order[i-1].name : ''));

app.set('market_data', market_data);
app.set('market_count', market_count);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handler - will print stacktrace when in development mode, otherwise no stacktraces will be leaked to the user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: (app.get('env') === 'development' ? err : {})
  });
});

// determine if tls features should be enabled
if (settings.webserver.tls.enabled == true) {
  function readCertsSync() {
    var tls_options = {};

    try {
      tls_options = {
        key: db.fs.readFileSync(settings.webserver.tls.key_file),
        cert: db.fs.readFileSync(settings.webserver.tls.cert_file),
        ca: db.fs.readFileSync(settings.webserver.tls.chain_file)
      };
    } catch(e) {
      console.warn('There was a problem reading tls certificates. Check that the certificate, chain and key paths are correct.');
    }

    return tls_options;
  }

  const https = require('https');
  let httpd = https.createServer(readCertsSync(), app).listen(settings.webserver.tls.port);

  try {
    let waitForCertsToRefresh;

    // watch for changes to the certificate directory
    db.fs.watch(path.dirname(settings.webserver.tls.key_file), () => {
      clearTimeout(waitForCertsToRefresh);

      // refresh certificates as they are changed on disk
      waitForCertsToRefresh = setTimeout(() => {
        httpd.setSecureContext(readCertsSync());
      }, 1000);
    });
  } catch(e) {
    console.warn('There was a problem reading tls certificates. Check that the certificate, chain and key paths are correct.');
  }
}

// get the latest git commit id (if exists)
exec('git rev-parse HEAD', (err, stdout, stderr) => {
  // check if the commit id was returned
  if (stdout != null && stdout != '') {
    // set the explorer revision code based on the git commit id
    app.set('revision', stdout.substring(0, 7));
  }
});

module.exports = app;