/**
 * DevExtreme (viz/chart_components/multi_axes_synchronizer.js)
 * Version: 17.2.10 (build 18271)
 * Build date: Fri Sep 28 2018
 *
 * Copyright (c) 2012 - 2018 Developer Express Inc. ALL RIGHTS RESERVED
 * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
 */
"use strict";
var debug = require("../../core/utils/console").debug,
    typeUtils = require("../../core/utils/type"),
    _each = require("../../core/utils/iterator").each,
    vizUtils = require("../core/utils"),
    _isDefined = typeUtils.isDefined,
    adjust = require("../../core/utils/math").adjust,
    _math = Math,
    _floor = _math.floor,
    _max = _math.max,
    _abs = _math.abs,
    _map = require("../core/utils").map;
var getValueAxesPerPanes = function(valueAxes) {
    var result = {};
    _each(valueAxes, function(_, axis) {
        var pane = axis.pane;
        if (!result[pane]) {
            result[pane] = []
        }
        result[pane].push(axis)
    });
    return result
};
var linearConverter = {
    transform: function(v, b) {
        return adjust(vizUtils.getLog(v, b))
    },
    addInterval: function(v, i) {
        return adjust(v + i)
    },
    getInterval: function(base, tickInterval) {
        return tickInterval
    }
};
var logConverter = {
    transform: function(v, b) {
        return adjust(vizUtils.raiseTo(v, b))
    },
    addInterval: function(v, i) {
        return adjust(v * i)
    },
    getInterval: function(base, tickInterval) {
        return _math.pow(base, tickInterval)
    }
};
var convertAxisInfo = function(axisInfo, converter) {
    if (!axisInfo.isLogarithmic) {
        return
    }
    var tick, interval, base = axisInfo.logarithmicBase,
        tickValues = axisInfo.tickValues,
        ticks = [];
    axisInfo.minValue = converter.transform(axisInfo.minValue, base);
    axisInfo.oldMinValue = converter.transform(axisInfo.oldMinValue, base);
    axisInfo.maxValue = converter.transform(axisInfo.maxValue, base);
    axisInfo.oldMaxValue = converter.transform(axisInfo.oldMaxValue, base);
    axisInfo.tickInterval = _math.round(axisInfo.tickInterval);
    if (axisInfo.tickInterval < 1) {
        axisInfo.tickInterval = 1
    }
    interval = converter.getInterval(base, axisInfo.tickInterval);
    tick = converter.transform(tickValues[0], base);
    while (ticks.length < tickValues.length) {
        ticks.push(tick);
        tick = converter.addInterval(tick, interval)
    }
    ticks.tickInterval = axisInfo.tickInterval;
    axisInfo.tickValues = ticks
};
var populateAxesInfo = function(axes) {
    return _map(axes, function(axis) {
        var minValue, maxValue, businessRange, tickInterval, synchronizedValue, ticksValues = axis.getTicksValues(),
            majorTicks = ticksValues.majorTicksValues,
            options = axis.getOptions(),
            axisInfo = null;
        if (majorTicks && majorTicks.length > 0 && typeUtils.isNumeric(majorTicks[0]) && "discrete" !== options.type && !axis.getTranslator().getBusinessRange().stubData) {
            businessRange = axis.getTranslator().getBusinessRange();
            tickInterval = axis._tickInterval;
            minValue = businessRange.minVisible;
            maxValue = businessRange.maxVisible;
            synchronizedValue = options.synchronizedValue;
            if (minValue === maxValue && _isDefined(synchronizedValue)) {
                tickInterval = _abs(majorTicks[0] - synchronizedValue) || 1;
                minValue = majorTicks[0] - tickInterval;
                maxValue = majorTicks[0] + tickInterval
            }
            axisInfo = {
                axis: axis,
                isLogarithmic: "logarithmic" === options.type,
                logarithmicBase: businessRange.base,
                tickValues: majorTicks,
                minorValues: ticksValues.minorTicksValues,
                minorTickInterval: axis._minorTickInterval,
                minValue: minValue,
                oldMinValue: minValue,
                maxValue: maxValue,
                oldMaxValue: maxValue,
                inverted: businessRange.invert,
                tickInterval: tickInterval,
                synchronizedValue: synchronizedValue
            };
            if (businessRange.stubData) {
                axisInfo.stubData = true;
                axisInfo.tickInterval = axisInfo.tickInterval || options.tickInterval;
                axisInfo.isLogarithmic = false
            }
            convertAxisInfo(axisInfo, linearConverter)
        }
        return axisInfo
    })
};
var updateTickValues = function(axesInfo) {
    var maxTicksCount = 0;
    _each(axesInfo, function(_, axisInfo) {
        maxTicksCount = _max(maxTicksCount, axisInfo.tickValues.length)
    });
    _each(axesInfo, function(_, axisInfo) {
        var ticksMultiplier, ticksCount, additionalStartTicksCount = 0,
            synchronizedValue = axisInfo.synchronizedValue,
            tickValues = axisInfo.tickValues,
            tickInterval = axisInfo.tickInterval;
        if (_isDefined(synchronizedValue)) {
            axisInfo.baseTickValue = axisInfo.invertedBaseTickValue = synchronizedValue;
            axisInfo.tickValues = [axisInfo.baseTickValue]
        } else {
            if (tickValues.length > 1 && tickInterval) {
                ticksMultiplier = _floor((maxTicksCount + 1) / tickValues.length);
                ticksCount = ticksMultiplier > 1 ? _floor((maxTicksCount + 1) / ticksMultiplier) : maxTicksCount;
                additionalStartTicksCount = _floor((ticksCount - tickValues.length) / 2);
                while (additionalStartTicksCount > 0 && 0 !== tickValues[0]) {
                    tickValues.unshift(adjust(tickValues[0] - tickInterval));
                    additionalStartTicksCount--
                }
                while (tickValues.length < ticksCount) {
                    tickValues.push(adjust(tickValues[tickValues.length - 1] + tickInterval))
                }
                axisInfo.tickInterval = tickInterval / ticksMultiplier
            }
            axisInfo.baseTickValue = tickValues[0];
            axisInfo.invertedBaseTickValue = tickValues[tickValues.length - 1]
        }
    })
};
var getAxisRange = function(axisInfo) {
    return axisInfo.maxValue - axisInfo.minValue || 1
};
var getMainAxisInfo = function(axesInfo) {
    for (var i = 0; i < axesInfo.length; i++) {
        if (!axesInfo[i].stubData) {
            return axesInfo[i]
        }
    }
    return null
};
var correctMinMaxValues = function(axesInfo) {
    var mainAxisInfo = getMainAxisInfo(axesInfo),
        mainAxisInfoTickInterval = mainAxisInfo.tickInterval;
    _each(axesInfo, function(_, axisInfo) {
        var scale, move, mainAxisBaseValueOffset, valueFromAxisInfo;
        if (axisInfo !== mainAxisInfo) {
            if (mainAxisInfoTickInterval && axisInfo.tickInterval) {
                if (axisInfo.stubData && _isDefined(axisInfo.synchronizedValue)) {
                    axisInfo.oldMinValue = axisInfo.minValue = axisInfo.baseTickValue - (mainAxisInfo.baseTickValue - mainAxisInfo.minValue) / mainAxisInfoTickInterval * axisInfo.tickInterval;
                    axisInfo.oldMaxValue = axisInfo.maxValue = axisInfo.baseTickValue - (mainAxisInfo.baseTickValue - mainAxisInfo.maxValue) / mainAxisInfoTickInterval * axisInfo.tickInterval
                }
                scale = mainAxisInfoTickInterval / getAxisRange(mainAxisInfo) / axisInfo.tickInterval * getAxisRange(axisInfo);
                axisInfo.maxValue = axisInfo.minValue + getAxisRange(axisInfo) / scale
            }
            if (mainAxisInfo.inverted && !axisInfo.inverted || !mainAxisInfo.inverted && axisInfo.inverted) {
                mainAxisBaseValueOffset = mainAxisInfo.maxValue - mainAxisInfo.invertedBaseTickValue
            } else {
                mainAxisBaseValueOffset = mainAxisInfo.baseTickValue - mainAxisInfo.minValue
            }
            valueFromAxisInfo = getAxisRange(axisInfo);
            move = (mainAxisBaseValueOffset / getAxisRange(mainAxisInfo) - (axisInfo.baseTickValue - axisInfo.minValue) / valueFromAxisInfo) * valueFromAxisInfo;
            axisInfo.minValue -= move;
            axisInfo.maxValue -= move
        }
    })
};
var calculatePaddings = function(axesInfo) {
    var minPadding, maxPadding, startPadding = 0,
        endPadding = 0;
    _each(axesInfo, function(_, axisInfo) {
        var inverted = axisInfo.inverted;
        minPadding = axisInfo.minValue > axisInfo.oldMinValue ? (axisInfo.minValue - axisInfo.oldMinValue) / getAxisRange(axisInfo) : 0;
        maxPadding = axisInfo.maxValue < axisInfo.oldMaxValue ? (axisInfo.oldMaxValue - axisInfo.maxValue) / getAxisRange(axisInfo) : 0;
        startPadding = _max(startPadding, inverted ? maxPadding : minPadding);
        endPadding = _max(endPadding, inverted ? minPadding : maxPadding)
    });
    return {
        start: startPadding,
        end: endPadding
    }
};
var correctMinMaxValuesByPaddings = function(axesInfo, paddings) {
    _each(axesInfo, function(_, info) {
        var range = getAxisRange(info),
            inverted = info.inverted;
        info.minValue = adjust(info.minValue - paddings[inverted ? "end" : "start"] * range);
        info.maxValue = adjust(info.maxValue + paddings[inverted ? "start" : "end"] * range)
    })
};
var updateTickValuesIfSynchronizedValueUsed = function(axesInfo) {
    var hasSynchronizedValue = false;
    _each(axesInfo, function(_, info) {
        hasSynchronizedValue = hasSynchronizedValue || _isDefined(info.synchronizedValue)
    });
    _each(axesInfo, function(_, info) {
        var tick, tickInterval = info.tickInterval,
            tickValues = info.tickValues,
            maxValue = info.maxValue,
            minValue = info.minValue;
        if (hasSynchronizedValue && tickInterval) {
            while ((tick = adjust(tickValues[0] - tickInterval)) >= minValue) {
                tickValues.unshift(tick)
            }
            tick = tickValues[tickValues.length - 1];
            while ((tick = adjust(tick + tickInterval)) <= maxValue) {
                tickValues.push(tick)
            }
        }
        while (tickValues[0] < minValue) {
            tickValues.shift()
        }
        while (tickValues[tickValues.length - 1] > maxValue) {
            tickValues.pop()
        }
    })
};
var applyMinMaxValues = function(axesInfo) {
    _each(axesInfo, function(_, info) {
        var axis = info.axis,
            range = axis.getTranslator().getBusinessRange();
        if (range.min === range.minVisible) {
            range.min = info.minValue
        }
        if (range.max === range.maxVisible) {
            range.max = info.maxValue
        }
        range.minVisible = info.minValue;
        range.maxVisible = info.maxValue;
        if (_isDefined(info.stubData)) {
            range.stubData = info.stubData
        }
        if (range.min > range.minVisible) {
            range.min = range.minVisible
        }
        if (range.max < range.maxVisible) {
            range.max = range.maxVisible
        }
        axis.getTranslator().updateBusinessRange(range);
        axis.setTicks({
            majorTicks: info.tickValues,
            minorTicks: info.minorValues
        })
    })
};
var correctAfterSynchronize = function(axesInfo) {
    var correctValue, validAxisInfo, invalidAxisInfo = [];
    _each(axesInfo, function(i, info) {
        if (info.oldMaxValue - info.oldMinValue === 0) {
            invalidAxisInfo.push(info)
        } else {
            if (!_isDefined(correctValue) && !_isDefined(info.synchronizedValue)) {
                correctValue = _abs((info.maxValue - info.minValue) / (info.tickValues[_floor(info.tickValues.length / 2)] - info.minValue || info.maxValue));
                validAxisInfo = info
            }
        }
    });
    if (!_isDefined(correctValue)) {
        return
    }
    _each(invalidAxisInfo, function(i, info) {
        var firstTick = info.tickValues[0],
            correctedTick = firstTick * correctValue,
            tickValues = validAxisInfo.tickValues,
            centralTick = tickValues[_floor(tickValues.length / 2)];
        if (firstTick > 0) {
            info.maxValue = correctedTick;
            info.minValue = 0
        } else {
            if (firstTick < 0) {
                info.minValue = correctedTick;
                info.maxValue = 0
            } else {
                if (0 === firstTick) {
                    info.maxValue = validAxisInfo.maxValue - centralTick;
                    info.minValue = validAxisInfo.minValue - centralTick
                }
            }
        }
    })
};

function updateMinorTicks(axesInfo) {
    axesInfo.forEach(function(axisInfo) {
        if (!axisInfo.minorTickInterval) {
            return
        }
        var ticks = [];
        var interval = axisInfo.minorTickInterval,
            tickCount = axisInfo.tickInterval / interval - 1;
        for (var i = 1; i < axisInfo.tickValues.length; i++) {
            var tick = axisInfo.tickValues[i - 1];
            for (var j = 0; j < tickCount; j++) {
                tick += interval;
                ticks.push(tick)
            }
        }
        axisInfo.minorValues = ticks
    })
}
var multiAxesSynchronizer = {
    synchronize: function(valueAxes) {
        _each(getValueAxesPerPanes(valueAxes), function(_, axes) {
            var axesInfo, paddings;
            if (axes.length > 1) {
                axesInfo = populateAxesInfo(axes);
                if (axesInfo.length < 2 || !getMainAxisInfo(axesInfo)) {
                    return
                }
                updateTickValues(axesInfo);
                correctMinMaxValues(axesInfo);
                paddings = calculatePaddings(axesInfo);
                correctMinMaxValuesByPaddings(axesInfo, paddings);
                correctAfterSynchronize(axesInfo);
                updateTickValuesIfSynchronizedValueUsed(axesInfo);
                updateMinorTicks(axesInfo);
                _each(axesInfo, function() {
                    convertAxisInfo(this, logConverter)
                });
                applyMinMaxValues(axesInfo)
            }
        })
    }
};
module.exports = multiAxesSynchronizer;
