text: var zlib = require('zlib');
  
  var redis = require('redis');
  var async = require('async');
  
  var os = require('os');
  
  var algos = require('stratum-pool/lib/algoProperties.js');
  
  // redis callback Ready check failed bypass trick
  function rediscreateClient(port, host, pass) {
      var client = redis.createClient(port, host);
      if (pass) {
          client.auth(pass);
      }
      return client;
  }
  
  
  /**
   * Sort object properties (only own properties will be sorted).
   * @param {object} obj object to sort properties
   * @param {string|int} sortedBy 1 - sort object properties by specific value.
   * @param {bool} isNumericSort true - sort object properties as numeric value, false - sort as string value.
   * @param {bool} reverse false - reverse sorting.
   * @returns {Array} array of items in [[key,value],[key,value],...] format.
   */
  function sortProperties(obj, sortedBy, isNumericSort, reverse) {
      sortedBy = sortedBy || 1; // by default first key
      isNumericSort = isNumericSort || false; // by default text sort
      reverse = reverse || false; // by default no reverse
  
      var reversed = (reverse) ? -1 : 1;
  
      var sortable = [];
      for (var key in obj) {
          if (obj.hasOwnProperty(key)) {
              sortable.push([key, obj[key]]);
          }
      }
      if (isNumericSort)
          sortable.sort(function (a, b) {
              return reversed * (a[1][sortedBy] - b[1][sortedBy]);
          });
      else
          sortable.sort(function (a, b) {
              var x = a[1][sortedBy].toLowerCase(),
                  y = b[1][sortedBy].toLowerCase();
              return x < y ? reversed * -1 : x > y ? reversed : 0;
          });
      return sortable; // array in format [ [ key1, val1 ], [ key2, val2 ], ... ]
  }
  
  module.exports = function(logger, portalConfig, poolConfigs){
  
      var _this = this;
  
      var logSystem = 'Stats';
  
      var redisClients = [];
      var redisStats;
  
      this.statHistory = [];
      this.statPoolHistory = [];
  
      this.stats = {};
      this.statsString = '';
  
      setupStatsRedis();
      gatherStatHistory();
  
      var canDoStats = true;
  
      Object.keys(poolConfigs).forEach(function(coin){
          if (!canDoStats) return;
  
          var poolConfig = poolConfigs[coin];
          var redisConfig = poolConfig.redis;
  
          for (var i = 0; i < redisClients.length; i++){
              var client = redisClients[i];
              if (client.client.port === redisConfig.port && client.client.host === redisConfig.host){
                  client.coins.push(coin);
                  return;
              }
          }
          redisClients.push({
              coins: [coin],
              client: rediscreateClient(redisConfig.port, redisConfig.host, redisConfig.password)
          });
      });
  
      function setupStatsRedis(){
          redisStats = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
          redisStats.on('error', function(err){
          redisStats.auth(portalConfig.redis.password);
          });
      }
  
     this.getBlocks = function (cback) {
          var allBlocks = {};
          async.each(_this.stats.pools, function(pool, pcb) {
  
              if (_this.stats.pools[pool.name].pending && _this.stats.pools[pool.name].pending.blocks)
                  for (var i=0; i<_this.stats.pools[pool.name].pending.blocks.length; i++)
                      allBlocks[pool.name+"-"+_this.stats.pools[pool.name].pending.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].pending.blocks[i];
  
              if (_this.stats.pools[pool.name].confirmed && _this.stats.pools[pool.name].confirmed.blocks)
                  for (var i=0; i<_this.stats.pools[pool.name].confirmed.blocks.length; i++)
                      allBlocks[pool.name+"-"+_this.stats.pools[pool.name].confirmed.blocks[i].split(':')[2]] = _this.stats.pools[pool.name].confirmed.blocks[i];
  
              pcb();
          }, function(err) {
              cback(allBlocks);
          });
      };
  
      function gatherStatHistory(){
          var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0).toString();
          redisStats.zrangebyscore(['statHistory', retentionTime, '+inf'], function(err, replies){
              if (err) {
                  logger.error(logSystem, 'Historics', 'Error when trying to grab historical stats ' + JSON.stringify(err));
                  return;
              }
              for (var i = 0; i < replies.length; i++){
                  _this.statHistory.push(JSON.parse(replies[i]));
              }
              _this.statHistory = _this.statHistory.sort(function(a, b){
                  return a.time - b.time;
              });
              _this.statHistory.forEach(function(stats){
                  addStatPoolHistory(stats);
              });
          });
      }
  
      function getWorkerStats(address) {
          address = address.split(".")[0];
          if (address.length > 0 && address.startsWith('t')) {
              for (var h in statHistory) {
                  for(var pool in statHistory[h].pools) {
  
                      statHistory[h].pools[pool].workers.sort(sortWorkersByHashrate);
  
                      for(var w in statHistory[h].pools[pool].workers){
                          if (w.startsWith(address)) {
                              if (history[w] == null) {
                                  history[w] = [];
                              }
                              if (workers[w] == null && stats.pools[pool].workers[w] != null) {
                                  workers[w] = stats.pools[pool].workers[w];
                              }
                              if (statHistory[h].pools[pool].workers[w].hashrate) {
                                  history[w].push({time: statHistory[h].time, hashrate:statHistory[h].pools[pool].workers[w].hashrate});
                              }
                          }
                      }
                  }
              }
              return JSON.stringify({"workers": workers, "history": history});
          }
          return null;
      }
  
      function addStatPoolHistory(stats){
          var data = {
              time: stats.time,
              pools: {}
          };
          for (var pool in stats.pools){
              data.pools[pool] = {
                  hashrate: stats.pools[pool].hashrate,
                  workerCount: stats.pools[pool].workerCount,
                  blocks: stats.pools[pool].blocks
              }
          }
          _this.statPoolHistory.push(data);
      }
  
      var magnitude = 100000000;
      var coinPrecision = magnitude.toString().length - 1;
  
      function roundTo(n, digits) {
          if (digits === undefined) {
              digits = 0;
          }
          var multiplicator = Math.pow(10, digits);
          n = parseFloat((n * multiplicator).toFixed(11));
          var test =(Math.round(n) / multiplicator);
          return +(test.toFixed(digits));
      }
  
      var satoshisToCoins = function(satoshis){
          return roundTo((satoshis / magnitude), coinPrecision);
      };
  
      var coinsToSatoshies = function(coins){
          return Math.round(coins * magnitude);
      };
  
      function coinsRound(number) {
          return roundTo(number, coinPrecision);
      }
  
      function readableSeconds(t) {
          var seconds = Math.round(t);
          var minutes = Math.floor(seconds/60);
          var hours = Math.floor(minutes/60);
          var days = Math.floor(hours/24);
          hours = hours-(days*24);
          minutes = minutes-(days*24*60)-(hours*60);
          seconds = seconds-(days*24*60*60)-(hours*60*60)-(minutes*60);
          if (days > 0) { return (days + "d " + hours + "h " + minutes + "m " + seconds + "s"); }
          if (hours > 0) { return (hours + "h " + minutes + "m " + seconds + "s"); }
          if (minutes > 0) {return (minutes + "m " + seconds + "s"); }
          return (seconds + "s");
      }
  
      this.getCoins = function(cback){
          _this.stats.coins = redisClients[0].coins;
          cback();
      };
  
      this.getPayout = function(address, cback){
          async.waterfall([
              function(callback){
                  _this.getBalanceByAddress(address, function(){
                      callback(null, 'test');
                  });
              }
          ], function(err, total){
              cback(coinsRound(total).toFixed(8));
          });
      };
  
      this.getTotalSharesByAddress = function(address, cback) {
          var a = address.split(".")[0];
          var client = redisClients[0].client,
              coins = redisClients[0].coins,
              shares = [];
  
          var pindex = parseInt(0);
          var totalShares = parseFloat(0);
          async.each(_this.stats.pools, function(pool, pcb) {
              pindex++;
              var coin = String(_this.stats.pools[pool.name].name);
              client.hscan(coin + ':shares:roundCurrent', 0, "match", a+"*", "count", 1000, function(error, result) {
                  if (error) {
                      pcb(error);
                      return;
                  }
                  var workerName="";
                  var shares = 0;
                  for (var i in result[1]) {
                      if (Math.abs(i % 2) != 1) {
                          workerName = String(result[1][i]);
                      } else {
                          shares += parseFloat(result[1][i]);
                      }
                  }
                  if (shares>0) {
                      totalShares = shares;
                  }
                  pcb();
              });
          }, function(err) {
              if (err) {
                  cback(0);
                  return;
              }
              if (totalShares > 0 || (pindex >= Object.keys(_this.stats.pools).length)) {
                  cback(totalShares);
                  return;
              }
          });
      };
  
      this.getBalanceByAddress = function(address, cback){
  
          var a = address.split(".")[0];
  
          var client = redisClients[0].client,
              coins = redisClients[0].coins,
              balances = [];
  
          var totalHeld = parseFloat(0);
          var totalPaid = parseFloat(0);
          var totalImmature = parseFloat(0);
  
          async.each(_this.stats.pools, function(pool, pcb) {
              var coin = String(_this.stats.pools[pool.name].name);
              // get all immature balances from address
              client.hscan(coin + ':immature', 0, "match", a+"*", "count", 10000, function(error, pends) {
                  // get all balances from address
                  client.hscan(coin + ':balances', 0, "match", a+"*", "count", 10000, function(error, bals) {
                      // get all payouts from address
                      client.hscan(coin + ':payouts', 0, "match", a+"*", "count", 10000, function(error, pays) {
  
                          var workerName = "";
                          var balAmount = 0;
                          var paidAmount = 0;
                          var pendingAmount = 0;
  
                          var workers = {};
  
                          for (var i in pays[1]) {
                              if (Math.abs(i % 2) != 1) {
                                  workerName = String(pays[1][i]);
                                  workers[workerName] = (workers[workerName] || {});
                              } else {
                                  paidAmount = parseFloat(pays[1][i]);
                                  workers[workerName].paid = coinsRound(paidAmount);
                                  totalPaid += paidAmount;
                              }
                          }
                          for (var b in bals[1]) {
                              if (Math.abs(b % 2) != 1) {
                                  workerName = String(bals[1][b]);
                                  workers[workerName] = (workers[workerName] || {});
                              } else {
                                  balAmount = parseFloat(bals[1][b]);
                                  workers[workerName].balance = coinsRound(balAmount);
                                  totalHeld += balAmount;
                              }
                          }
                          for (var b in pends[1]) {
                              if (Math.abs(b % 2) != 1) {
                                  workerName = String(pends[1][b]);
                                  workers[workerName] = (workers[workerName] || {});
                              } else {
                                  pendingAmount = parseFloat(pends[1][b]);
                                  workers[workerName].immature = coinsRound(pendingAmount);
                                  totalImmature += pendingAmount;
                              }
                          }
  
                          for (var w in workers) {
                              balances.push({
                                  worker:String(w),
                                  balance:workers[w].balance,
                                  paid:workers[w].paid,
                                  immature:workers[w].immature
                              });
                          }
  
                          pcb();
                      });
                  });
              });
          }, function(err) {
              if (err) {
                  callback("There was an error getting balances");
                  return;
              }
  
              _this.stats.balances = balances;
              _this.stats.address = address;
  
              cback({totalHeld:coinsRound(totalHeld), totalPaid:coinsRound(totalPaid), totalImmature:satoshisToCoins(totalImmature), balances});
          });
      };
  
      this.getGlobalStats = function(callback){
  
          var statGatherTime = Date.now() / 1000 | 0;
  
          var allCoinStats = {};
  
          async.each(redisClients, function(client, callback){
              var windowTime = (((Date.now() / 1000) - portalConfig.website.stats.hashrateWindow) | 0).toString();
              var redisCommands = [];
  
              var redisCommandTemplates = [
                  ['zremrangebyscore', ':hashrate', '-inf', '(' + windowTime],
                  ['zrangebyscore', ':hashrate', windowTime, '+inf'],
                  ['hgetall', ':stats'],
                  ['scard', ':blocksPending'],
                  ['scard', ':blocksConfirmed'],
                  ['scard', ':blocksKicked'],
                  ['smembers', ':blocksPending'],
                  ['smembers', ':blocksConfirmed'],
                  ['hgetall', ':shares:roundCurrent'],
                  ['hgetall', ':blocksPendingConfirms'],
                  ['zrange', ':payments', -100, -1],
                  ['hgetall', ':shares:timesCurrent']
              ];
  
              var commandsPerCoin = redisCommandTemplates.length;
  
              client.coins.map(function(coin){
                  redisCommandTemplates.map(function(t){
                      var clonedTemplates = t.slice(0);
                      clonedTemplates[1] = coin + clonedTemplates[1];
                      redisCommands.push(clonedTemplates);
                  });
              });
  
              client.client.multi(redisCommands).exec(function(err, replies){
                  if (err){
                      logger.error(logSystem, 'Global', 'error with getting global stats ' + JSON.stringify(err));
                      callback(err);
                  }
                  else{
  
                      for(var i = 0; i < replies.length; i += commandsPerCoin){
                          var coinName = client.coins[i / commandsPerCoin | 0];
                          var marketStats = {};
                          if (replies[i + 2]) {
                              if (replies[i + 2].coinmarketcap) {
                                  marketStats = replies[i + 2] ? (JSON.parse(replies[i + 2].coinmarketcap)[0] || 0) : 0;
                              }
                          }
                          var coinStats = {
                              name: coinName,
                              symbol: poolConfigs[coinName].coin.symbol.toUpperCase(),
                              algorithm: poolConfigs[coinName].coin.algorithm,
                              poolFees: poolConfigs[coinName].rewardRecipients,
                              hashrates: replies[i + 1],
                              poolStats: {
                                  validShares: replies[i + 2] ? (replies[i + 2].validShares || 0) : 0,
                                  validBlocks: replies[i + 2] ? (replies[i + 2].validBlocks || 0) : 0,
                                  invalidShares: replies[i + 2] ? (replies[i + 2].invalidShares || 0) : 0,
                                  totalPaid: replies[i + 2] ? (replies[i + 2].totalPaid || 0) : 0,
                                  networkBlocks: replies[i + 2] ? (replies[i + 2].networkBlocks || 0) : 0,
                                  networkSols: replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0,
                                  networkSolsString: getReadableNetworkHashRateString(replies[i + 2] ? (replies[i + 2].networkSols || 0) : 0),
                                  networkDiff: replies[i + 2] ? (replies[i + 2].networkDiff || 0) : 0,
                                  networkConnections: replies[i + 2] ? (replies[i + 2].networkConnections || 0) : 0,
                                  networkVersion: replies[i + 2] ? (replies[i + 2].networkSubVersion || 0) : 0,
                                  networkProtocolVersion: replies[i + 2] ? (replies[i + 2].networkProtocolVersion || 0) : 0
                              },
                              marketStats: marketStats,
                              /* block stat counts */
                              blocks: {
                                  pending: replies[i + 3],
                                  confirmed: replies[i + 4],
                                  orphaned: replies[i + 5],
                              },
                              /* show all pending blocks */
                              pending: {
                                  blocks: replies[i + 6].sort(sortBlocks),
                                  confirms: (replies[i + 9] || {})
                              },
                              /* show last 50 found blocks */
                              confirmed: {
                                  blocks: replies[i + 7].sort(sortBlocks).slice(0,50)
                              },
                              payments: [],
                              currentRoundShares: (replies[i + 8] || {}),
                              currentRoundTimes: (replies[i + 11] || {}),
                              maxRoundTime: 0,
                              shareCount: 0
                          };
  						var lastblock = parseInt(replies[i + 7][0].split(':')[2]);
  						var timeoflastblock = Math.floor(replies[i + 7][0].split(':')[4]/1000);
  						var tnow = Math.floor(new Date().getTime()/1000.0);
  						coinStats.blocks.lastblock = lastblock;
  						coinStats.blocks.timesincelast = tnow-timeoflastblock;
  //						console.log(replies);
                          for(var j = replies[i + 10].length; j > 0; j--){
                              var jsonObj;
                              try {
                                  jsonObj = JSON.parse(replies[i + 10][j-1]);
                              } catch(e) {
                                  jsonObj = null;
                              }
                              if (jsonObj !== null) {
                                  coinStats.payments.push(jsonObj);
                              }
                          }
                          allCoinStats[coinStats.name] = (coinStats);
                      }
                      // sort pools alphabetically
                      allCoinStats = sortPoolsByName(allCoinStats);
                      callback();
                  }
              });
          }, function(err){
              if (err){
                  logger.error(logSystem, 'Global', 'error getting all stats' + JSON.stringify(err));
                  callback();
                  return;
              }
  
              var portalStats = {
                  time: statGatherTime,
                  global:{
                      workers: 0,
                      hashrate: 0
                  },
                  algos: {},
                  pools: allCoinStats
              };
  
              Object.keys(allCoinStats).forEach(function(coin){
                  var coinStats = allCoinStats[coin];
                  coinStats.workers = {};
                  coinStats.miners = {};
                  coinStats.shares = 0;
                  coinStats.hashrates.forEach(function(ins){
                      var parts = ins.split(':');
                      var workerShares = parseFloat(parts[0]);
                      var miner = parts[1].split('.')[0];
                      var worker = parts[1];
                      var diff = Math.round(parts[0] * 8192);
                      if (workerShares > 0) {
                          coinStats.shares += workerShares;
                          // build worker stats
                          if (worker in coinStats.workers) {
                              coinStats.workers[worker].shares += workerShares;
                              coinStats.workers[worker].diff = diff;
                          } else {
                              coinStats.workers[worker] = {
                                  name: worker,
                                  diff: diff,
                                  shares: workerShares,
                                  invalidshares: 0,
                                  currRoundShares: 0,
                                  currRoundTime: 0,
                                  hashrate: null,
                                  hashrateString: null,
                                  luckDays: null,
                                  luckHours: null,
                                  paid: 0,
                                  balance: 0
                              };
                          }
                          // build miner stats
                          if (miner in coinStats.miners) {
                              coinStats.miners[miner].shares += workerShares;
                          } else {
                              coinStats.miners[miner] = {
                                  name: miner,
                                  shares: workerShares,
                                  invalidshares: 0,
                                  currRoundShares: 0,
                                  currRoundTime: 0,
                                  hashrate: null,
                                  hashrateString: null,
                                  luckDays: null,
                                  luckHours: null
                              };
                          }
                      }
                      else {
                          // build worker stats
                          if (worker in coinStats.workers) {
                              coinStats.workers[worker].invalidshares -= workerShares; // workerShares is negative number!
                              coinStats.workers[worker].diff = diff;
                          } else {
                              coinStats.workers[worker] = {
                                  name: worker,
                                  diff: diff,
                                  shares: 0,
                                  invalidshares: -workerShares,
                                  currRoundShares: 0,
                                  currRoundTime: 0,
                                  hashrate: null,
                                  hashrateString: null,
                                  luckDays: null,
                                  luckHours: null,
                                  paid: 0,
                                  balance: 0
                              };
                          }
                          // build miner stats
                          if (miner in coinStats.miners) {
                              coinStats.miners[miner].invalidshares -= workerShares; // workerShares is negative number!
                          } else {
                              coinStats.miners[miner] = {
                                  name: miner,
                                  shares: 0,
                                  invalidshares: -workerShares,
                                  currRoundShares: 0,
                                  currRoundTime: 0,
                                  hashrate: null,
                                  hashrateString: null,
                                  luckDays: null,
                                  luckHours: null
                              };
                          }
                      }
                  });
  
                  // sort miners
                  coinStats.miners = sortMinersByHashrate(coinStats.miners);
  
                  var shareMultiplier = Math.pow(2, 32) / algos[coinStats.algorithm].multiplier;
                  coinStats.hashrate = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow;
                  coinStats.hashrateString = _this.getReadableHashRateString(coinStats.hashrate);
  
                  var _blocktime = 160;
                  var _networkHashRate = parseFloat(coinStats.poolStats.networkSols) * 1.2;
                  var _myHashRate = (coinStats.hashrate / 1000000) * 2;
                  coinStats.luckDays =  ((_networkHashRate / _myHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
                  coinStats.luckHours = ((_networkHashRate / _myHashRate * _blocktime) / (60 * 60)).toFixed(3);
                  coinStats.minerCount = Object.keys(coinStats.miners).length;
                  coinStats.workerCount = Object.keys(coinStats.workers).length;
                  portalStats.global.workers += coinStats.workerCount;
  
                  /* algorithm specific global stats */
                  var algo = coinStats.algorithm;
                  if (!portalStats.algos.hasOwnProperty(algo)){
                      portalStats.algos[algo] = {
                          workers: 0,
                          hashrate: 0,
                          hashrateString: null
                      };
                  }	
  				
                  portalStats.algos[algo].hashrate += coinStats.hashrate;
                  portalStats.algos[algo].workers += Object.keys(coinStats.workers).length;
  
                  var _shareTotal = parseFloat(0);
                  var _maxTimeShare = parseFloat(0);
                  for (var worker in coinStats.currentRoundShares) {
                      var miner = worker.split(".")[0];
                      if (miner in coinStats.miners) {
                          coinStats.miners[miner].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]);
                      }
                      if (worker in coinStats.workers) {
                          coinStats.workers[worker].currRoundShares += parseFloat(coinStats.currentRoundShares[worker]);
                      }
                      _shareTotal += parseFloat(coinStats.currentRoundShares[worker]);
                  }
                  for (var worker in coinStats.currentRoundTimes) {
                      var time = parseFloat(coinStats.currentRoundTimes[worker]);
                      if (_maxTimeShare < time) { _maxTimeShare = time; }
                      var miner = worker.split(".")[0];    // split poolId from minerAddress
                      if (miner in coinStats.miners && coinStats.miners[miner].currRoundTime < time) {
                          coinStats.miners[miner].currRoundTime = time;
                      }
                  }
  
                  coinStats.shareCount = _shareTotal;
                  coinStats.maxRoundTime = _maxTimeShare;
                  coinStats.maxRoundTimeString = readableSeconds(_maxTimeShare);
  
                  for (var worker in coinStats.workers) {
                      var _workerRate = shareMultiplier * coinStats.workers[worker].shares / portalConfig.website.stats.hashrateWindow;
                      var _wHashRate = (_workerRate / 1000000) * 2;
                      coinStats.workers[worker].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
                      coinStats.workers[worker].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
                      coinStats.workers[worker].hashrate = _workerRate;
                      coinStats.workers[worker].hashrateString = _this.getReadableHashRateString(_workerRate);
                      var miner = worker.split('.')[0];
                      if (miner in coinStats.miners) {
                          coinStats.workers[worker].currRoundTime = coinStats.miners[miner].currRoundTime;
                      }
                  }
                  for (var miner in coinStats.miners) {
                      var _workerRate = shareMultiplier * coinStats.miners[miner].shares / portalConfig.website.stats.hashrateWindow;
                      var _wHashRate = (_workerRate / 1000000) * 2;
                      coinStats.miners[miner].luckDays = ((_networkHashRate / _wHashRate * _blocktime) / (24 * 60 * 60)).toFixed(3);
                      coinStats.miners[miner].luckHours = ((_networkHashRate / _wHashRate * _blocktime) / (60 * 60)).toFixed(3);
                      coinStats.miners[miner].hashrate = _workerRate;
                      coinStats.miners[miner].hashrateString = _this.getReadableHashRateString(_workerRate);
                  }
  
                  // sort workers by name
                  coinStats.workers = sortWorkersByName(coinStats.workers);
  
                  delete coinStats.hashrates;
                  delete coinStats.shares;
              });
  
              Object.keys(portalStats.algos).forEach(function(algo){
                  var algoStats = portalStats.algos[algo];
                  algoStats.hashrateString = _this.getReadableHashRateString(algoStats.hashrate);
              });
  
              _this.stats = portalStats;
  
              // save historical hashrate, not entire stats!
              var saveStats = JSON.parse(JSON.stringify(portalStats));
              Object.keys(saveStats.pools).forEach(function(pool){
                  delete saveStats.pools[pool].pending;
                  delete saveStats.pools[pool].confirmed;
                  delete saveStats.pools[pool].currentRoundShares;
                  delete saveStats.pools[pool].currentRoundTimes;
                  delete saveStats.pools[pool].payments;
                  delete saveStats.pools[pool].miners;
              });
              _this.statsString = JSON.stringify(saveStats);
              _this.statHistory.push(saveStats);
  
              addStatPoolHistory(portalStats);
  
              var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0);
  
              for (var i = 0; i < _this.statHistory.length; i++){
                  if (retentionTime < _this.statHistory[i].time){
                      if (i > 0) {
                          _this.statHistory = _this.statHistory.slice(i);
                          _this.statPoolHistory = _this.statPoolHistory.slice(i);
                      }
                      break;
                  }
              }
  
              redisStats.multi([
                  ['zadd', 'statHistory', statGatherTime, _this.statsString],
                  ['zremrangebyscore', 'statHistory', '-inf', '(' + retentionTime]
              ]).exec(function(err, replies){
                  if (err)
                      logger.error(logSystem, 'Historics', 'Error adding stats to historics ' + JSON.stringify(err));
              });
              callback();
          });
  
      };
  
      function sortPoolsByName(objects) {
          var newObject = {};
          var sortedArray = sortProperties(objects, 'name', false, false);
          for (var i = 0; i < sortedArray.length; i++) {
              var key = sortedArray[i][0];
              var value = sortedArray[i][1];
              newObject[key] = value;
          }
          return newObject;
      }
  
      function sortBlocks(a, b) {
          var as = parseInt(a.split(":")[2]);
          var bs = parseInt(b.split(":")[2]);
          if (as > bs) return -1;
          if (as < bs) return 1;
          return 0;
      }
  
      function sortWorkersByName(objects) {
          var newObject = {};
          var sortedArray = sortProperties(objects, 'name', false, false);
          for (var i = 0; i < sortedArray.length; i++) {
              var key = sortedArray[i][0];
              var value = sortedArray[i][1];
              newObject[key] = value;
          }
          return newObject;
      }
  
      function sortMinersByHashrate(objects) {
          var newObject = {};
          var sortedArray = sortProperties(objects, 'shares', true, true);
          for (var i = 0; i < sortedArray.length; i++) {
              var key = sortedArray[i][0];
              var value = sortedArray[i][1];
              newObject[key] = value;
          }
          return newObject;
      }
  
      function sortWorkersByHashrate(a, b) {
          if (a.hashrate === b.hashrate) {
              return 0;
          }
          else {
              return (a.hashrate < b.hashrate) ? -1 : 1;
          }
      }
  
      this.getReadableHashRateString = function(hashrate){
          hashrate = (hashrate * 2);
          if (hashrate < 1000000) {
              return (Math.round(hashrate / 1000) / 1000 ).toFixed(2)+' Sol/s';
          }
          var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
          var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
          hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
          return hashrate.toFixed(2) + byteUnits[i];
      };
  
      function getReadableNetworkHashRateString(hashrate) {
          hashrate = (hashrate * 1000000);
          if (hashrate < 1000000)
              return '0 Sol';
          var byteUnits = [ ' Sol/s', ' KSol/s', ' MSol/s', ' GSol/s', ' TSol/s', ' PSol/s' ];
          var i = Math.floor((Math.log(hashrate/1000) / Math.log(1000)) - 1);
          hashrate = (hashrate/1000) / Math.pow(1000, i + 1);
          return hashrate.toFixed(2) + byteUnits[i];
      }
  };
  