/**
 * Echarts, logarithmic axis reform
 *
 * @author sushuang (sushuang@baidu.com),
 *         Ievgenii (@Ievgeny, ievgeny@zoomdata.com)
 */
module.exports = function (require) {
  // Reference
  var number = require('./number');

  var Mt = Math;
  var mathLog = Mt.log;
  var mathPow = Mt.pow;
  var mathAbs = Mt.abs;
  var mathCeil = Mt.ceil;
  var mathFloor = Mt.floor; // Constant

  var LOG_BASE = Mt.E; // It is not necessary to specify log base,
  // because log(logBase, x) = ln(x) / ln(logBase),
  // thus final result (axis tick location) is only determined by ln(x).

  var LN10 = Mt.LN10;
  var LN2 = Mt.LN2;
  var LN2D10 = LN2 / LN10;
  var EPSILON = 1e-9;
  var DEFAULT_SPLIT_NUMBER = 5;
  var MIN_BASE_10_SPLIT_NUMBER = 2;
  var SUPERSCRIPTS = {
    '0': '⁰',
    '1': '¹',
    '2': '²',
    '3': '³',
    '4': '⁴',
    '5': '⁵',
    '6': '⁶',
    '7': '⁷',
    '8': '⁸',
    '9': '⁹',
    '-': '⁻'
  }; // Static variable

  var logPositive;
  var logLabelBase;
  var logLabelMode; // enumeration:
  // 'plain' (i.e. axis labels are shown like 10000)
  // 'exponent' (i.e. axis labels are shown like 10²)

  var lnBase;
  var custOpts;
  var splitNumber;
  var logMappingOffset;
  var absMin;
  var absMax;
  var tickList;
  /**
   * Test cases:
   * [2, 4, 8, 16, 32, 64, 128]
   * [0.01, 0.1, 10, 100, 1000] logLabelBase: 3
   * [0.01, 0.1, 10, 100, 1000] logLabelBase: -12
   * [-2, -4, -8, -16, -32, -64, -128] logLabelBase: 3
   * [2, 4, 8, 16, '-', 64, 128]
   * [2, 4, 8, 16, 32, 64]
   * [2, 4, 8, 16, 32]
   * [0.00000256, 0.0016, 0.04, 0.2]
   * [0.1, 1, 10, 100, 1000, 10000, 100000, 1000000] splitNumber: 3
   * [1331, 3434, 500, 1, 1212, 4]
   * [0.14, 2, 45, 1001, 200, 0.33, 10001]
   * [0.00001, 0.00005]
   * [0.00001, 0.00005] boundaryGap: [0.2, 0.4]
   * [0.001, 2, -45, 1001, 200, 0.33, 10000]
   * [0.00000001, 0.00000012]
   * [0.000000000000001]
   * [0.00000001, 0.00000001]
   * [3, 3]
   * [12, -3, 47, 19]
   * [12, -3, 47, 19] logPositive: false
   * [-2, -4, -8, -16, -32, -64, -128]
   * [-2, -4, -8, -16, -32, -64]
   * [2, 4, 8, 16, 32] boundaryGap: [0.2, 0.4]
   * []
   * [0]
   * [10, 10, 10]
   * [0.00003, 0.00003, 0.00003]
   * [0.00001, 0.00001, 0.00001]
   * [-0.00001, -0.00001, -0.00001]
   * ['-', '-']
   * ['-', 10]
   * logarithmic axis in scatter (try dataZoom)
   * logarithmic axis width dataZoom component (try xAxis and yAxis)
   */

  /**
   * Main function. Return data object with values for axis building.
   *
   * @public
   * @param {Object} [opts] Configurable options
   * @param {number} opts.dataMin data Minimum
   * @param {number} opts.dataMax data Maximum
   * @param {number=} opts.logPositive Logarithmic sign. If not specified, it will be auto-detected.
   * @param {number=} opts.logLabelBase Logaithmic base in axis label.
   *                                    If not specified, it will be set to 10 (and use 2 for detail)
   * @param {number=} opts.splitNumber Number of sections perfered.
   * @return {Object} {
   *                      dataMin: New min,
   *                      dataMax: New max,
   *                      tickList: [Array of tick data]
   *                      logPositive: Type of data sign
   *                      dataMappingMethods: [Set of logarithmic methods]
   *                  }
   */

  function smartLogSteps(opts) {
    clearStaticVariables();
    custOpts = opts || {};
    reformSetting();
    makeTicksList();
    return [makeResult(), clearStaticVariables()][0];
  }
  /**
   * All of static variables must be clear here.
   */


  function clearStaticVariables() {
    logPositive = custOpts = logMappingOffset = lnBase = absMin = absMax = splitNumber = tickList = logLabelBase = logLabelMode = null;
  }
  /**
   * Determine sign (logPositive, negative) of data set, if not specified.
   * Reform min and max of data.
   */


  function reformSetting() {
    // Settings of log label base
    logLabelBase = custOpts.logLabelBase;

    if (logLabelBase == null) {
      logLabelMode = 'plain';
      logLabelBase = 10;
      lnBase = LN10;
    } else {
      logLabelBase = +logLabelBase;

      if (logLabelBase < 1) {
        // log base less than 1 is not supported.
        logLabelBase = 10;
      }

      logLabelMode = 'exponent';
      lnBase = mathLog(logLabelBase);
    } // Settings of split number


    splitNumber = custOpts.splitNumber;
    splitNumber == null && (splitNumber = DEFAULT_SPLIT_NUMBER); // Setting of data min and max

    var dataMin = parseFloat(custOpts.dataMin);
    var dataMax = parseFloat(custOpts.dataMax);

    if (!isFinite(dataMin) && !isFinite(dataMax)) {
      dataMin = dataMax = 1;
    } else if (!isFinite(dataMin)) {
      dataMin = dataMax;
    } else if (!isFinite(dataMax)) {
      dataMax = dataMin;
    } else if (dataMin > dataMax) {
      dataMax = [dataMin, dataMin = dataMax][0]; // Exchange min, max.
    } // Settings of log positive


    logPositive = custOpts.logPositive; // If not specified, determine sign by data.

    if (logPositive == null) {
      // LogPositive is false when dataMax <= 0 && dataMin < 0.
      // LogPositive is true when dataMin >= 0.
      // LogPositive is true when dataMax >= 0 && dataMin < 0 (singular points may exists)
      logPositive = dataMax > 0 || dataMin === 0;
    } // Settings of absMin and absMax, which must be greater than 0.


    absMin = logPositive ? dataMin : -dataMax;
    absMax = logPositive ? dataMax : -dataMin; // FIXME
    // If there is any data item less then zero, it is suppose to be igonred and min should be re-calculated.
    // But it is difficult to do that in current code stucture.
    // So refactor of xxAxis.js is desired.

    absMin < EPSILON && (absMin = EPSILON);
    absMax < EPSILON && (absMax = EPSILON);
  }
  /**
   * Make tick list.
   */


  function makeTicksList() {
    tickList = []; // Estimate max exponent and min exponent

    var maxDataLog = fixAccurate(mathLog(absMax) / lnBase);
    var minDataLog = fixAccurate(mathLog(absMin) / lnBase);
    var maxExpon = mathCeil(maxDataLog);
    var minExpon = mathFloor(minDataLog);
    var spanExpon = maxExpon - minExpon;
    var spanDataLog = maxDataLog - minDataLog;

    if (logLabelMode === 'exponent') {
      baseAnalysis();
    } else {
      // logLabelMode === 'plain', we will self-adapter
      !(spanExpon <= MIN_BASE_10_SPLIT_NUMBER && splitNumber > MIN_BASE_10_SPLIT_NUMBER) ? baseAnalysis() : detailAnalysis();
    } // In this situation, only draw base-10 ticks.
    // Base-10 ticks: 10^h (i.e. 0.01, 0.1, 1, 10, 100, ...)


    function baseAnalysis() {
      if (spanExpon < splitNumber) {
        splitNumber = spanExpon;
      } // Suppose:
      //      spanExpon > splitNumber
      //      stepExpon := floor(spanExpon / splitNumber)
      //      splitNumberFloat := spanExpon / stepExpon
      // There are tow expressions which are identically-true:
      //      splitNumberFloat - splitNumber <= 1
      //      stepExpon * ceil(splitNumberFloat) - spanExpon <= stepExpon
      // So we can calculate as follows:


      var stepExpon = mathFloor(fixAccurate(spanExpon / splitNumber)); // Put the plot in the middle of the min, max.

      var splitNumberAdjust = mathCeil(fixAccurate(spanExpon / stepExpon));
      var spanExponAdjust = stepExpon * splitNumberAdjust;
      var halfDiff = (spanExponAdjust - spanDataLog) / 2;
      var minExponAdjust = mathFloor(fixAccurate(minDataLog - halfDiff));

      if (aroundZero(minExponAdjust - minDataLog)) {
        minExponAdjust -= 1;
      } // Build logMapping offset


      logMappingOffset = -minExponAdjust * lnBase; // Build tickList

      for (var n = minExponAdjust; n - stepExpon <= maxDataLog; n += stepExpon) {
        tickList.push(mathPow(logLabelBase, n));
      }
    } // In this situation, base-2|10 ticks are used to make detailed split.
    // Base-2|10 ticks: 10^h * 2^k (i.e. 0.1, 0.2, 0.4, 1, 2, 4, 10, 20, 40),
    // where k in [0, 1, 2].
    // Because LN2 * 3 < LN10 and LN2 * 4 > LN10, k should be less than 3.
    // And when k === 3, the tick is too close to that of k === 0, which looks weird.
    // So we do not use 3.


    function detailAnalysis() {
      // Find max exponent and min exponent.
      // Calculate base on 3-hexadecimal (0, 1, 2, 10, 11, 12, 20).
      var minDecimal = toDecimalFrom4Hex(minExpon, 0);
      var endDecimal = minDecimal + 2;

      while (minDecimal < endDecimal && toH(minDecimal + 1) + toK(minDecimal + 1) * LN2D10 < minDataLog) {
        minDecimal++;
      }

      var maxDecimal = toDecimalFrom4Hex(maxExpon, 0);
      var endDecimal = maxDecimal - 2; // maxDecimal is greater than 4

      while (maxDecimal > endDecimal && toH(maxDecimal - 1) + toK(maxDecimal - 1) * LN2D10 > maxDataLog) {
        maxDecimal--;
      } // Build logMapping offset


      logMappingOffset = -(toH(minDecimal) * LN10 + toK(minDecimal) * LN2); // Build logMapping tickList

      for (var i = minDecimal; i <= maxDecimal; i++) {
        var h = toH(i);
        var k = toK(i);
        tickList.push(mathPow(10, h) * mathPow(2, k));
      }
    } // Convert to decimal number from 4-hexadecimal number,
    // where h, k means: if there is a 4-hexadecimal numer 23, then h is 2, k is 3.
    // h can be any integer (notice: h can be greater than 10 or less than 0),
    // and k belongs to [0, 1, 2, 3].


    function toDecimalFrom4Hex(h, k) {
      return h * 3 + k;
    }

    function toK(decimal) {
      return decimal - toH(decimal) * 3; // Can not calculate by '%'
    }

    function toH(decimal) {
      return mathFloor(fixAccurate(decimal / 3));
    }
  }
  /**
   * Make result
   */


  function makeResult() {
    var resultTickList = [];

    for (var i = 0, len = tickList.length; i < len; i++) {
      resultTickList[i] = (logPositive ? 1 : -1) * tickList[i];
    }

    !logPositive && resultTickList.reverse();
    var dataMappingMethods = makeDataMappingMethods();
    var value2Coord = dataMappingMethods.value2Coord;
    var newDataMin = value2Coord(resultTickList[0]);
    var newDataMax = value2Coord(resultTickList[resultTickList.length - 1]);

    if (newDataMin === newDataMax) {
      newDataMin -= 1;
      newDataMax += 1;
    }

    return {
      dataMin: newDataMin,
      dataMax: newDataMax,
      tickList: resultTickList,
      logPositive: logPositive,
      labelFormatter: makeLabelFormatter(),
      dataMappingMethods: dataMappingMethods
    };
  }
  /**
   * Make axis label formatter.
   */


  function makeLabelFormatter() {
    if (logLabelMode === 'exponent') {
      // For label style like 3⁴.
      // Static variables should be fixed in the scope of the methods.
      var myLogLabelBase = logLabelBase;
      var myLnBase = lnBase;
      return function (value) {
        if (!isFinite(parseFloat(value))) {
          return '';
        }

        var sign = '';

        if (value < 0) {
          value = -value;
          sign = '-';
        }

        return sign + myLogLabelBase + makeSuperscriptExponent(mathLog(value) / myLnBase);
      };
    } else {
      return function (value) {
        // Normal style like 0.001, 10,000,0
        if (!isFinite(parseFloat(value))) {
          return '';
        }

        return number.addCommas(formatNumber(value));
      };
    }
  }
  /**
   * Make calculate methods.
   */


  function makeDataMappingMethods() {
    // Static variables should be fixed in the scope of the methods.
    var myLogPositive = logPositive;
    var myLogMappingOffset = logMappingOffset;
    return {
      value2Coord: function (x) {
        if (x == null || isNaN(x) || !isFinite(x)) {
          return x;
        }

        x = parseFloat(x); // to number

        if (!isFinite(x)) {
          x = EPSILON;
        } else if (myLogPositive && x < EPSILON) {
          // FIXME
          // It is suppose to be ignore, but not be set to EPSILON. See comments above.
          x = EPSILON;
        } else if (!myLogPositive && x > -EPSILON) {
          x = -EPSILON;
        }

        x = mathAbs(x);
        return (myLogPositive ? 1 : -1) * (mathLog(x) + myLogMappingOffset);
      },
      coord2Value: function (x) {
        if (x == null || isNaN(x) || !isFinite(x)) {
          return x;
        }

        x = parseFloat(x); // to number

        if (!isFinite(x)) {
          x = EPSILON;
        }

        return myLogPositive ? mathPow(LOG_BASE, x - myLogMappingOffset) : -mathPow(LOG_BASE, -x + myLogMappingOffset);
      }
    };
  }
  /**
   * For example, Math.log(1000) / Math.LN10 get the result of 2.9999999999999996, rather than 3.
   * This method trys to fix it.
   * (accMath.div can not fix this problem yet.)
   */


  function fixAccurate(result) {
    return +Number(+result).toFixed(14);
  }
  /**
   * Avoid show float number like '1e-9', '-1e-10', ...
   * @return {string}
   */


  function formatNumber(num) {
    return Number(num).toFixed(15).replace(/\.?0*$/, '');
  }
  /**
   * Make superscript exponent
   */


  function makeSuperscriptExponent(exponent) {
    exponent = formatNumber(Math.round(exponent)); // Do not support float superscript.
    // (because I can not find superscript style of '.')

    var result = [];

    for (var i = 0, len = exponent.length; i < len; i++) {
      var cha = exponent.charAt(i);
      result.push(SUPERSCRIPTS[cha] || '');
    }

    return result.join('');
  }
  /**
   * Decide whether near zero
   */


  function aroundZero(val) {
    return val > -EPSILON && val < EPSILON;
  }

  return smartLogSteps;
}(require);