text: const Luno = require("bitx");
  const _ = require('lodash');
  const moment = require('moment');
  const node_util = require('util');
  const Errors = require('../exchangeErrors');
  const retry = require('../exchangeUtils').retry;
  const name = 'Luno';
  
  var tradeAttempt = 0;
  
  const Trader = function(config) {
    if (_.isObject(config)) {
      this.key = config.key;
      this.secret = config.secret;
      this.currency = config.currency;
      this.asset = config.asset;
      this.pair = config.asset + config.currency;
    }
    this.luno = new Luno(this.key, this.secret, {
      pair: this.pair
    });
    this.market = _.find(Trader.getCapabilities().markets, (market) => {
      return market.pair[0] === this.currency && market.pair[1] === this.asset;
    });
    this.interval = 2000;
  }
  
  const log = function() {
    const message = node_util.format.apply(null, _.toArray(arguments));
    console.log(moment().format('YYYY-MM-DD HH:mm:ss') + ' (DEBUG):    ' + message);
  }
  
  Trader.prototype.inspect = function(obj) {
    return node_util.inspect(obj, {
      showhidden: false,
      depth: null,
      breakLength: Infinity,
      colors: true
    });
  };
  
  const setPrecision = (amount, digits) => {
    let precision = 100000000;
    if (Number.isInteger(digits)) precision = Math.pow(10, digits);
    amount *= precision;
    amount = Math.floor(amount);
    amount /= precision;
    return amount;
  }
  
  const round = (value, decimals = 14) => {
    return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
  }
  
  const includes = (str, list) => {
    if (!_.isString(str))
      return false;
    let result = _.some(list, item => str.includes(item));
    return result;
  }
  
  const recoverableErrors = [
    'SOCKETTIMEDOUT',
    'TIMEDOUT',
    'CONNRESET',
    'CONNREFUSED',
    'NOTFOUND'
  ];
  
  
  const processResponse = function(funcName, callback) {
    return (error, body) => {
      /* BitX Error Codes
            Error: BitX error 400: invalid id
            Error: BitX error 401: "error":"API key not found","error_code":"ErrAPIKeyNotFound","error_action":
            Error: BitX error 429: -- API limit
            Error: BitX error 500: Something went wrong, we're looking into it.
       */
  
      if (!error && !body) {
        error = new Error('received empty response');
      }
  
      if (error) {
        log(name, funcName + '() received error -->', error.message);
  
        if (includes(error.message, recoverableErrors)) {
          error.notFatal = true;
        }
  
        if (includes(error.message, ['error 429'])) {
          error.notFatal = true;
          error.backoffDelay = 3000;
        }
  
        if (includes(error.message, ['error 500'])) {
          error.notFatal = true;
          error.backoffDelay = 15000;
        }
  
        if (includes(error.message, ['Insufficient balance'])) {
          error.notFatal = true;
          error.backoffDelay = 2000;
          tradeAttempt++;
          if (tradeAttempt >= 3) {
            error.notFatal = false;
            log(name, funcName + '() giving up after 3 failed attempts.');
          }
        }
  
        return callback(error, undefined);
      }
  
      return callback(undefined, body);
    }
  };
  
  //------- Gekko Functions ---------//
  
  Trader.prototype.getTicker = function(callback) {
    // log(name, 'getTicker()');
  
    const process = (err, data) => {
      if (err) {
        log(name, 'Error: -->', err);
        return callback(err);
      }
      const ticker = {
        ask: data.ask,
        bid: data.bid,
        spread: round(data.ask - data.bid)
      };
      log(name, 'getTicker() -->', this.inspect(ticker));
      callback(undefined, ticker);
    };
  
    const handler = cb => this.luno.getTicker(processResponse('getTicker', cb));
    retry(null, handler, process);
  }
  
  Trader.prototype.getFee = function(callback) {
    // log(name, 'getFee()');
  
    if (this.pair === 'ETHXBT')
      return callback(undefined, 0.000025);
    else if (this.pair === 'XBTIDR')
      return callback(undefined, 0.00002);
    else
      return callback(undefined, 0.0001);
  
    /*
    const process = (err, data) => {
      if (err) {
        // log(name, 'Error: -->', err);
        return callback(err);
      }
      // log(name, 'getFee() --> fee:', data.taker_fee / 100);
      callback(undefined, data.taker_fee / 100);
    };
    const handler = cb => this.luno.getFee(processResponse('getFee', cb));
    retry(null, handler, process);
    */
  }
  
  Trader.prototype.getPortfolio = function(callback) {
    // log(name, 'getPortfolio()');
  
    const process = (err, data) => {
      if (err) {
        log(name, 'Error: -->', err);
        return callback(err);
      }
      const assetProfile = _.find(data.balance, a => a.asset === this.asset);
      const currencyProfile = _.find(data.balance, a => a.asset === this.currency);
      let assetAmount = round(assetProfile.balance - assetProfile.reserved);
      let currencyAmount = round(currencyProfile.balance - currencyProfile.reserved);
  
      if (!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
        assetAmount = 0;
      }
  
      if (!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
        currencyAmount = 0;
      }
  
      const portfolio = [{
          name: this.asset.toUpperCase(),
          amount: assetAmount
        },
        {
          name: this.currency.toUpperCase(),
          amount: currencyAmount
        }
      ];
      log(name, 'getPortfolio() --> ' + this.inspect(portfolio));
      callback(undefined, portfolio);
    };
  
    const handler = cb => this.luno.getBalance(processResponse('getPortfolio', cb));
    retry(null, handler, process);
  }
  
  Trader.prototype.buy = function(amount, price, callback) {
    log(name, 'buy() amount:', amount, 'price:', price);
  
    tradeAttempt = 0;
    amount = round(amount);
    price = round(price);
    const process = (err, data) => {
      if (err) {
        log(name, 'unable to buy:', err.message);
        return callback(err);
      }
      log(name, 'buy() order id: -->', data.order_id);
      callback(undefined, data.order_id);
    };
  
    const handler = cb => this.luno.postBuyOrder(amount, price, processResponse('buy', cb));
    retry(null, handler, process);
  }
  
  Trader.prototype.sell = function(amount, price, callback) {
    log(name, 'sell() amount:', amount, 'price:', price);
  
    tradeAttempt = 0;
    amount = round(amount);
    price = round(price);
    const process = (err, data) => {
      if (err) {
        log(name, 'unable to sell:', err.message);
        return callback(err);
      }
      log(name, 'sell() order id: -->', data.order_id);
      callback(undefined, data.order_id);
    };
  
    const handler = cb => this.luno.postSellOrder(amount, price, processResponse('sell', cb));
    retry(null, handler, process);
  }
  
  Trader.prototype.roundAmount = function(amount) {
    amount = setPrecision(amount, this.market.precision);
    return amount;
  }
  
  Trader.prototype.roundPrice = function(price) {
    return +price;
  }
  
  Trader.prototype.getOrder = function(order, callback) {
    // log(name, 'getOrder() order id:', order);
  
    if (!order) {
      return callback('invalid order_id', false);
    }
    const process = (err, data) => {
      if (err) {
        log(name, 'Error: -->', err);
        return callback(err);
      }
      const price = parseFloat(data.limit_price);
      const amount = parseFloat(data.base);
      let date = moment();
      const fees = {
        [this.asset]: +data.fee_base,
        [this.currency]: +data.fee_counter
      };
      const feePercent = round(data.fee_base / data.base * 100, 2);
      if (data.state === 'PENDING') {
        date = moment(data.creation_timestamp);
      } else {
        date = moment(data.completed_timestamp);
      }
      const result = {
        price,
        amount,
        date,
        fees,
        feePercent
      };
      log(name, 'getOrder() -->', this.inspect(result));
      callback(undefined, result);
    };
  
    const handler = cb => this.luno.getOrder(order, processResponse('getOrder', cb));
    retry(null, handler, process);
  }
  
  Trader.prototype.checkOrder = function(order, callback) {
    // log(name, 'checkOrder() order id:', order);
  
    if (!order) {
      return callback('invalid order_id');
    }
    const process = (err, data) => {
      if (err) {
        log(name, 'Error: -->', err);
        return callback(err);
      }
      const result = {
        open: data.state === 'PENDING',
        executed: data.limit_volume === data.base,
        filledAmount: +data.base,
        remaining: round(+data.limit_volume - +data.base)
      }
      log(name, 'checkOrder()', order, 'result:', this.inspect(result));
      callback(undefined, result);
    };
  
    const handler = cb => this.luno.getOrder(order, processResponse('checkOrder', cb));
    retry(null, handler, process);
  }
  
  Trader.prototype.cancelOrder = function(order, callback) {
    log(name, 'cancelOrder() order id:', order);
  
    if (!order) {
      return callback('invalid order_id');
    }
    const process = (err, data) => {
      if (err) {
        if (_.some(err.message, 'Cannot stop unknown')) {
          log(name, 'unable to cancel order:', order, '(' + err.message + ') assuming success...');
        } else {
          log(name, 'unable to cancel order:', order, '(' + err.message + ') aborting...');
          return callback(err);
        }
      }
  
      if (data && !data.success) {
        log('cancelOrder() --> status:', data.success);
        return callback(undefined, false);
      }
  
      this.checkOrder(order, (error, orderStatus) => {
        if (error) {
          log(name, 'cancelOrder\'s checkOrder failed. What do i do here?');
          return callback(error, false);
        }
  
        if (orderStatus.executed) {
          log(name, 'cancelOrder() -->', order, 'was fulfilled before cancelOrder was completed.');
          return callback(undefined, true);
        }
        const remaining = {
          remaining: orderStatus.remaining,
          filled: orderStatus.filledAmount
        }
        log(name, 'cancelOrder() --> status: false remaining:', this.inspect(remaining));
        return callback(undefined, false, remaining);
      });
    }
  
    const handler = cb => this.luno.stopOrder(order, processResponse('cancelOrder', cb));
    retry(null, handler, process);
  }
  
  Trader.prototype.getTrades = function(since, callback, descending) {
    // log(name, 'getTrades() since:', since);
  
    const process = (err, result) => {
      if (err) {
        log(name, 'Error: -->', err);
        return callback(err);
      }
      let trades = _.map(result.trades, (t) => {
        return {
          price: t.price,
          date: Math.round(t.timestamp / 1000),
          amount: t.volume,
          tid: t.timestamp
        }
      });
      if (!descending) {
        trades = trades.reverse()
      }
      callback(undefined, trades);
    };
  
    if (moment.isMoment(since)) since = since.valueOf();
    (_.isNumber(since) && since > 0) ? since: since = 0;
  
    const options = {
      pair: this.pair,
      since: since
    }
  
    const handler = cb => this.luno.getTrades(options, processResponse('getTrades', cb));
    retry(null, handler, process);
  }
  
  Trader.getCapabilities = function() {
    return {
      name: 'Luno',
      slug: 'luno',
      currencies: ['MYR', 'KES', 'NGN', 'ZAR', 'XBT'],
      assets: ['ETH', 'XBT'],
      markets: [
        { pair: ['XBT', 'ETH'], minimalOrder: { amount: 0.01,   unit: 'asset' }, precision: 2 },
        { pair: ['MYR', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
        { pair: ['KES', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
        { pair: ['NGN', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
        { pair: ['ZAR', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
      ],
      requires: ['key', 'secret'],
      providesFullHistory: true,
      providesHistory: 'date',
      // maxHistoryFetch: 100,
      tid: 'tid',
      tradable: true,
      forceReorderDelay: true,
      gekkoBroker: 0.6
    };
  }
  
  module.exports = Trader;
  