/* Pretty handling of log axes.

Copyright (c) 2007-2014 IOLA and Ole Laursen.
Copyright (c) 2015 Ciprian Ceteras cipix2000@gmail.com.
Copyright (c) 2017 Raluca Portase
Licensed under the MIT license.

Set axis.mode to "log" to enable.
*/

/* global jQuery*/

/**
## jquery.flot.logaxis
This plugin is used to create logarithmic axis. This includes tick generation,
formatters and transformers to and from logarithmic representation.

### Methods and hooks
*/

(function ($) {
    'use strict';

    var options = {
        xaxis: {}
    };

    /*tick generators and formatters*/
    var PREFERRED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 10),
        EXTENDED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 4);

    function computePreferedLogTickValues(endLimit, rangeStep) {
        var log10End = Math.floor(Math.log(endLimit) * Math.LOG10E) - 1,
            log10Start = -log10End,
            val, range, vals = [];

        for (var power = log10Start; power <= log10End; power++) {
            range = Math.pow(10, power);
            for (var mult = 1; mult < 9; mult += rangeStep) {
                val = range * mult;
                vals.push(val);
            }
        }
        return vals;
    }

    /**
    - logTickGenerator(plot, axis, noTicks)

    Generates logarithmic ticks, depending on axis range.
    In case the number of ticks that can be generated is less than the expected noTicks/4,
    a linear tick generation is used.
    */
    var logTickGenerator = function (plot, axis, noTicks) {
        var ticks = [],
            minIdx = -1,
            maxIdx = -1,
            surface = plot.getCanvas(),
            logTickValues = PREFERRED_LOG_TICK_VALUES,
            min = clampAxis(axis, plot),
            max = axis.max;

        if (!noTicks) {
            noTicks = 0.3 * Math.sqrt(axis.direction === "x" ? surface.width : surface.height);
        }

        PREFERRED_LOG_TICK_VALUES.some(function (val, i) {
            if (val >= min) {
                minIdx = i;
                return true;
            } else {
                return false;
            }
        });

        PREFERRED_LOG_TICK_VALUES.some(function (val, i) {
            if (val >= max) {
                maxIdx = i;
                return true;
            } else {
                return false;
            }
        });

        if (maxIdx === -1) {
            maxIdx = PREFERRED_LOG_TICK_VALUES.length - 1;
        }

        if (maxIdx - minIdx <= noTicks / 4 && logTickValues.length !== EXTENDED_LOG_TICK_VALUES.length) {
            //try with multiple of 5 for tick values
            logTickValues = EXTENDED_LOG_TICK_VALUES;
            minIdx *= 2;
            maxIdx *= 2;
        }

        var lastDisplayed = null,
            inverseNoTicks = 1 / noTicks,
            tickValue, pixelCoord, tick;

        // Count the number of tick values would appear, if we can get at least
        // nTicks / 4 accept them.
        if (maxIdx - minIdx >= noTicks / 4) {
            for (var idx = maxIdx; idx >= minIdx; idx--) {
                tickValue = logTickValues[idx];
                pixelCoord = (Math.log(tickValue) - Math.log(min)) / (Math.log(max) - Math.log(min));
                tick = tickValue;

                if (lastDisplayed === null) {
                    lastDisplayed = {
                        pixelCoord: pixelCoord,
                        idealPixelCoord: pixelCoord
                    };
                } else {
                    if (Math.abs(pixelCoord - lastDisplayed.pixelCoord) >= inverseNoTicks) {
                        lastDisplayed = {
                            pixelCoord: pixelCoord,
                            idealPixelCoord: lastDisplayed.idealPixelCoord - inverseNoTicks
                        };
                    } else {
                        tick = null;
                    }
                }

                if (tick) {
                    ticks.push(tick);
                }
            }
            // Since we went in backwards order.
            ticks.reverse();
        } else {
            var tickSize = plot.computeTickSize(min, max, noTicks),
                customAxis = {min: min, max: max, tickSize: tickSize};
            ticks = $.plot.linearTickGenerator(customAxis);
        }

        return ticks;
    };

    var clampAxis = function (axis, plot) {
        var min = axis.min,
            max = axis.max;

        if (min <= 0) {
            //for empty graph if axis.min is not strictly positive make it 0.1
            if (axis.datamin === null) {
                min = axis.min = 0.1;
            } else {
                min = processAxisOffset(plot, axis);
            }

            if (max < min) {
                axis.max = axis.datamax !== null ? axis.datamax : axis.options.max;
                axis.options.offset.below = 0;
                axis.options.offset.above = 0;
            }
        }

        return min;
    }

    /**
    - logTickFormatter(value, axis, precision)

    This is the corresponding tickFormatter of the logaxis.
    For a number greater that 10^6 or smaller than 10^(-3), this will be drawn
    with e representation
    */
    var logTickFormatter = function (value, axis, precision) {
        var tenExponent = value > 0 ? Math.floor(Math.log(value) / Math.LN10) : 0;

        if (precision) {
            if ((tenExponent >= -4) && (tenExponent <= 7)) {
                return $.plot.defaultTickFormatter(value, axis, precision);
            } else {
                return $.plot.expRepTickFormatter(value, axis, precision);
            }
        }
        if ((tenExponent >= -4) && (tenExponent <= 7)) {
            //if we have float numbers, return a limited length string(ex: 0.0009 is represented as 0.000900001)
            var formattedValue = tenExponent < 0 ? value.toFixed(-tenExponent) : value.toFixed(tenExponent + 2);
            if (formattedValue.indexOf('.') !== -1) {
                var lastZero = formattedValue.lastIndexOf('0');

                while (lastZero === formattedValue.length - 1) {
                    formattedValue = formattedValue.slice(0, -1);
                    lastZero = formattedValue.lastIndexOf('0');
                }

                //delete the dot if is last
                if (formattedValue.indexOf('.') === formattedValue.length - 1) {
                    formattedValue = formattedValue.slice(0, -1);
                }
            }
            return formattedValue;
        } else {
            return $.plot.expRepTickFormatter(value, axis);
        }
    };

    /*logaxis caracteristic functions*/
    var logTransform = function (v) {
        if (v < PREFERRED_LOG_TICK_VALUES[0]) {
            v = PREFERRED_LOG_TICK_VALUES[0];
        }

        return Math.log(v);
    };

    var logInverseTransform = function (v) {
        return Math.exp(v);
    };

    var invertedTransform = function (v) {
        return -v;
    }

    var invertedLogTransform = function (v) {
        return -logTransform(v);
    }

    var invertedLogInverseTransform = function (v) {
        return logInverseTransform(-v);
    }

    /**
    - setDataminRange(plot, axis)

    It is used for clamping the starting point of a logarithmic axis.
    This will set the axis datamin range to 0.1 or to the first datapoint greater then 0.
    The function is usefull since the logarithmic representation can not show
    values less than or equal to 0.
    */
    function setDataminRange(plot, axis) {
        if (axis.options.mode === 'log' && axis.datamin <= 0) {
            if (axis.datamin === null) {
                axis.datamin = 0.1;
            } else {
                axis.datamin = processAxisOffset(plot, axis);
            }
        }
    }

    function processAxisOffset(plot, axis) {
        var series = plot.getData(),
            range = series
                .filter(function(series) {
                    return series.xaxis === axis || series.yaxis === axis;
                })
                .map(function(series) {
                    return plot.computeRangeForDataSeries(series, null, isValid);
                }),
            min = axis.direction === 'x' ? Math.min(0.1, range[0].xmin) : Math.min(0.1, range[0].ymin);

        axis.min = min;

        return min;
    }

    function isValid(a) {
        return a > 0;
    }

    function init(plot) {
        plot.hooks.processOptions.push(function (plot) {
            $.each(plot.getAxes(), function (axisName, axis) {
                var opts = axis.options;
                if (opts.mode === 'log') {
                    axis.tickGenerator = function (axis) {
                        var noTicks = 11;
                        return logTickGenerator(plot, axis, noTicks);
                    };
                    if (typeof axis.options.tickFormatter !== 'function') {
                        axis.options.tickFormatter = logTickFormatter;
                    }
                    axis.options.transform = opts.inverted ? invertedLogTransform : logTransform;
                    axis.options.inverseTransform = opts.inverted ? invertedLogInverseTransform : logInverseTransform;
                    axis.options.autoScaleMargin = 0;
                    plot.hooks.setRange.push(setDataminRange);
                } else if (opts.inverted) {
                    axis.options.transform = invertedTransform;
                    axis.options.inverseTransform = invertedTransform;
                }
            });
        });
    }

    $.plot.plugins.push({
        init: init,
        options: options,
        name: 'log',
        version: '0.1'
    });

    $.plot.logTicksGenerator = logTickGenerator;
    $.plot.logTickFormatter = logTickFormatter;
})(jQuery);
