/**
 * DevExtreme (viz/translators/translator2d.js)
 * Version: 18.1.7 (build 18332)
 * Build date: Wed Nov 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 extend = require("../../core/utils/extend").extend,
    each = require("../../core/utils/iterator").each,
    numericTranslator = require("./numeric_translator"),
    categoryTranslator = require("./category_translator"),
    intervalTranslator = require("./interval_translator"),
    datetimeTranslator = require("./datetime_translator"),
    logarithmicTranslator = require("./logarithmic_translator"),
    vizUtils = require("../core/utils"),
    typeUtils = require("../../core/utils/type"),
    rangeModule = require("./range"),
    getLog = vizUtils.getLog,
    getPower = vizUtils.getPower,
    isDefined = typeUtils.isDefined,
    _abs = Math.abs,
    CANVAS_PROP = ["width", "height", "left", "top", "bottom", "right"],
    NUMBER_EQUALITY_CORRECTION = 1,
    DATETIME_EQUALITY_CORRECTION = 6e4,
    _noop = require("../../core/utils/common").noop,
    _Translator2d, addInterval = require("../../core/utils/date").addInterval;
var validateCanvas = function(canvas) {
    each(CANVAS_PROP, function(_, prop) {
        canvas[prop] = parseInt(canvas[prop]) || 0
    });
    return canvas
};
var makeCategoriesToPoints = function(categories) {
    var categoriesToPoints = {};
    categories.forEach(function(item, i) {
        categoriesToPoints[item.valueOf()] = i
    });
    return categoriesToPoints
};
var validateBusinessRange = function(businessRange) {
    function validate(valueSelector, baseValueSelector) {
        if (!isDefined(businessRange[valueSelector]) && isDefined(businessRange[baseValueSelector])) {
            businessRange[valueSelector] = businessRange[baseValueSelector]
        }
    }
    validate("minVisible", "min");
    validate("maxVisible", "max");
    return businessRange
};

function valuesAreDefinedAndEqual(val1, val2) {
    return isDefined(val1) && isDefined(val2) && val1.valueOf() === val2.valueOf()
}

function prepareBreaks(breaks, range) {
    var br, transformFrom, transformTo, i, transform = "logarithmic" === range.axisType ? function(value) {
            return getLog(value, range.base)
        } : function(value) {
            return value
        },
        array = [],
        length = breaks.length,
        sum = 0;
    for (i = 0; i < length; i++) {
        br = breaks[i];
        transformFrom = transform(br.from);
        transformTo = transform(br.to);
        sum += transformTo - transformFrom;
        array.push({
            trFrom: transformFrom,
            trTo: transformTo,
            from: br.from,
            to: br.to,
            length: sum,
            cumulativeWidth: br.cumulativeWidth
        })
    }
    return array
}

function getCanvasBounds(range) {
    var newMin, newMax, min = range.min,
        max = range.max,
        minVisible = range.minVisible,
        maxVisible = range.maxVisible,
        base = range.base,
        isDateTime = typeUtils.isDate(max) || typeUtils.isDate(min),
        correction = isDateTime ? DATETIME_EQUALITY_CORRECTION : NUMBER_EQUALITY_CORRECTION,
        isLogarithmic = "logarithmic" === range.axisType;
    if (isLogarithmic) {
        maxVisible = getLog(maxVisible, base);
        minVisible = getLog(minVisible, base);
        min = getLog(min, base);
        max = getLog(max, base)
    }
    if (valuesAreDefinedAndEqual(min, max)) {
        newMin = min.valueOf() - correction;
        newMax = max.valueOf() + correction;
        if (isDateTime) {
            min = new Date(newMin);
            max = new Date(newMax)
        } else {
            min = 0 !== min || isLogarithmic ? newMin : 0;
            max = newMax
        }
    }
    if (valuesAreDefinedAndEqual(minVisible, maxVisible)) {
        newMin = minVisible.valueOf() - correction;
        newMax = maxVisible.valueOf() + correction;
        if (isDateTime) {
            minVisible = newMin < min.valueOf() ? min : new Date(newMin);
            maxVisible = newMax > max.valueOf() ? max : new Date(newMax)
        } else {
            if (0 !== minVisible || isLogarithmic) {
                minVisible = newMin < min ? min : newMin
            }
            maxVisible = newMax > max ? max : newMax
        }
    }
    return {
        base: base,
        rangeMin: min,
        rangeMax: max,
        rangeMinVisible: minVisible,
        rangeMaxVisible: maxVisible
    }
}

function getEqualityCorrection(range) {
    var isDateTime = typeUtils.isDate(range.min) || typeUtils.isDate(range.max);
    return isDateTime ? DATETIME_EQUALITY_CORRECTION : NUMBER_EQUALITY_CORRECTION
}

function zoomArgsIsEqualCanvas(zoomArgs) {
    var businessRange = this.getBusinessRange();
    return valuesAreDefinedAndEqual(businessRange.min, businessRange.max) && this.isEqualRange(zoomArgs)
}

function isEqualRange(range) {
    var that = this;
    var businessRange = that.getBusinessRange();
    var canvasOptions = getCanvasBounds(businessRange);
    var correctionPrecision = getEqualityCorrection(businessRange) / 100;
    var comparingRange = new rangeModule.Range(range);
    if ("discrete" === businessRange.axisType) {
        return false
    }
    if (range && "logarithmic" === businessRange.axisType) {
        comparingRange.min = comparingRange.min && getLog(comparingRange.min, businessRange.base);
        comparingRange.max = comparingRange.max && getLog(comparingRange.max, businessRange.base)
    }
    return range && (!isDefined(comparingRange.min) || _abs(comparingRange.min - canvasOptions.rangeMin) <= correctionPrecision) && (!isDefined(comparingRange.max) || _abs(comparingRange.max - canvasOptions.rangeMax) <= correctionPrecision)
}

function checkGestureEventsForScaleEdges(scrollThreshold, scale, scroll, touches, zoomArgs) {
    var that = this,
        businessRange = that.getBusinessRange(),
        isDiscreteAxis = "discrete" === businessRange.axisType,
        scrollBarNearMin = that.scrollHasExtremePosition(scrollThreshold, false),
        scrollBarNearMax = that.scrollHasExtremePosition(scrollThreshold, true),
        isOriginalScale = that.checkScrollForOriginalScale(scrollThreshold),
        scalingEventAtMin = scrollBarNearMin && ((businessRange.rotated ? scroll > 0 : scroll < 0) || 1 !== scale),
        scalingEventAtMax = scrollBarNearMax && ((businessRange.rotated ? scroll < 0 : scroll > 0) || 1 !== scale);
    return 2 === touches && 1 === scale || (zoomArgs || isDiscreteAxis || !this.isEqualRange({
        min: businessRange.minVisible,
        max: businessRange.maxVisible
    })) && !scrollBarNearMin && !scrollBarNearMax || !isOriginalScale && (scalingEventAtMin || scalingEventAtMax) || isOriginalScale && scale > 1
}

function checkScrollForOriginalScale(scrollThreshold) {
    return this.scrollHasExtremePosition(scrollThreshold, false) && this.scrollHasExtremePosition(scrollThreshold, true)
}

function scrollHasExtremePosition(scrollThreshold, isMax) {
    var equalityCorrection, distanceToExtremum, that = this,
        businessRange = that.getBusinessRange(),
        isDiscreteAxis = "discrete" === businessRange.axisType,
        min = isDiscreteAxis ? businessRange.categories[0] : businessRange.min,
        max = isDiscreteAxis ? businessRange.categories[businessRange.categories.length - 1] : businessRange.max,
        isSinglePoint = min === max,
        isMaxExtremum = !businessRange.invert && isMax || businessRange.invert && !isMax,
        axisExtremum = isMaxExtremum ? max : min,
        scrollExtremum = isMaxExtremum ? businessRange.maxVisible : businessRange.minVisible;
    if (isDiscreteAxis) {
        return !isDefined(scrollExtremum) || axisExtremum.valueOf() === scrollExtremum.valueOf()
    } else {
        if ("logarithmic" === businessRange.axisType) {
            axisExtremum = vizUtils.getLog(axisExtremum, businessRange.base);
            scrollExtremum = vizUtils.getLog(scrollExtremum, businessRange.base)
        }
        equalityCorrection = axisExtremum.valueOf() === scrollExtremum.valueOf() ? 0 : getEqualityCorrection(businessRange);
        distanceToExtremum = isSinglePoint ? Math.abs(axisExtremum + (isMaxExtremum ? 1 : -1) * equalityCorrection - scrollExtremum) : Math.abs(axisExtremum - scrollExtremum);
        return distanceToExtremum * that._canvasOptions.ratioOfCanvasRange < scrollThreshold
    }
}

function getCheckingMethodsAboutBreaks(inverted) {
    return {
        isStartSide: !inverted ? function(pos, breaks, start, end) {
            return pos < breaks[0][start]
        } : function(pos, breaks, start, end) {
            return pos <= breaks[breaks.length - 1][end]
        },
        isEndSide: !inverted ? function(pos, breaks, start, end) {
            return pos >= breaks[breaks.length - 1][end]
        } : function(pos, breaks, start, end) {
            return pos > breaks[0][start]
        },
        isInBreak: !inverted ? function(pos, br, start, end) {
            return pos >= br[start] && pos < br[end]
        } : function(pos, br, start, end) {
            return pos > br[end] && pos <= br[start]
        },
        isBetweenBreaks: !inverted ? function(pos, br, prevBreak, start, end) {
            return pos < br[start] && pos >= prevBreak[end]
        } : function(pos, br, prevBreak, start, end) {
            return pos >= br[end] && pos < prevBreak[start]
        },
        getLength: !inverted ? function(br) {
            return br.length
        } : function(br, lastBreak) {
            return lastBreak.length - br.length
        },
        getBreaksSize: !inverted ? function(br) {
            return br.cumulativeWidth
        } : function(br, lastBreak) {
            return lastBreak.cumulativeWidth - br.cumulativeWidth
        }
    }
}
exports.Translator2D = _Translator2d = function(businessRange, canvas, options) {
    this.update(businessRange, canvas, options)
};
_Translator2d.prototype = {
    constructor: _Translator2d,
    reinit: function() {
        var that = this,
            options = that._options,
            range = that._businessRange,
            categories = range.categories || [],
            script = {},
            canvasOptions = that._prepareCanvasOptions(),
            visibleCategories = vizUtils.getCategoriesInfo(categories, range.minVisible, range.maxVisible).categories,
            categoriesLength = visibleCategories.length;
        switch (range.axisType) {
            case "logarithmic":
                script = logarithmicTranslator;
                break;
            case "semidiscrete":
                script = intervalTranslator;
                canvasOptions.ratioOfCanvasRange = canvasOptions.canvasLength / (addInterval(canvasOptions.rangeMaxVisible, options.interval) - canvasOptions.rangeMinVisible);
                break;
            case "discrete":
                script = categoryTranslator;
                that._categories = categories;
                canvasOptions.interval = that._getDiscreteInterval(options.addSpiderCategory ? categoriesLength + 1 : categoriesLength, canvasOptions);
                that._categoriesToPoints = makeCategoriesToPoints(categories, canvasOptions.invert);
                if (categoriesLength) {
                    canvasOptions.startPointIndex = that._categoriesToPoints[visibleCategories[0].valueOf()];
                    that.visibleCategories = visibleCategories
                }
                break;
            default:
                if ("datetime" === range.dataType) {
                    script = datetimeTranslator
                } else {
                    script = numericTranslator
                }
        }
        extend(that, script);
        that._conversionValue = options.conversionValue ? function(value) {
            return value
        } : function(value) {
            return Math.round(value)
        };
        that.sc = {};
        that._checkingMethodsAboutBreaks = [getCheckingMethodsAboutBreaks(false), getCheckingMethodsAboutBreaks(that.isInverted())];
        that._translateBreaks();
        that._calculateSpecialValues()
    },
    _translateBreaks: function() {
        var i, b, end, length, breaks = this._breaks,
            size = this._options.breaksSize;
        if (void 0 === breaks) {
            return
        }
        for (i = 0, length = breaks.length; i < length; i++) {
            b = breaks[i];
            end = this.translate(b.to);
            b.end = end;
            b.start = !b.gapSize ? !this.isInverted() ? end - size : end + size : end
        }
    },
    _checkValueAboutBreaks: function(breaks, pos, start, end, methods) {
        var i, length, br, prevBreak, prop = {
                length: 0,
                breaksSize: void 0,
                inBreak: false
            },
            lastBreak = breaks[breaks.length - 1];
        if (methods.isStartSide(pos, breaks, start, end)) {
            return prop
        } else {
            if (methods.isEndSide(pos, breaks, start, end)) {
                return {
                    length: lastBreak.length,
                    breaksSize: lastBreak.cumulativeWidth,
                    inBreak: false
                }
            }
        }
        for (i = 0, length = breaks.length; i < length; i++) {
            br = breaks[i];
            prevBreak = breaks[i - 1];
            if (methods.isInBreak(pos, br, start, end)) {
                prop.inBreak = true;
                prop.break = br;
                break
            }
            if (prevBreak && methods.isBetweenBreaks(pos, br, prevBreak, start, end)) {
                prop = {
                    length: methods.getLength(prevBreak, lastBreak),
                    breaksSize: methods.getBreaksSize(prevBreak, lastBreak),
                    inBreak: false
                };
                break
            }
        }
        return prop
    },
    isInverted: function() {
        return !(this._options.isHorizontal ^ this._businessRange.invert)
    },
    _getDiscreteInterval: function(categoriesLength, canvasOptions) {
        var correctedCategoriesCount = categoriesLength - (this._options.stick ? 1 : 0);
        return correctedCategoriesCount > 0 ? canvasOptions.canvasLength / correctedCategoriesCount : canvasOptions.canvasLength
    },
    _prepareCanvasOptions: function() {
        var length, that = this,
            businessRange = that._businessRange,
            canvasOptions = that._canvasOptions = getCanvasBounds(businessRange),
            canvas = that._canvas,
            breaks = that._breaks;
        if (that._options.isHorizontal) {
            canvasOptions.startPoint = canvas.left;
            length = canvas.width;
            canvasOptions.endPoint = canvas.width - canvas.right;
            canvasOptions.invert = businessRange.invert
        } else {
            canvasOptions.startPoint = canvas.top;
            length = canvas.height;
            canvasOptions.endPoint = canvas.height - canvas.bottom;
            canvasOptions.invert = !businessRange.invert
        }
        that.canvasLength = canvasOptions.canvasLength = canvasOptions.endPoint - canvasOptions.startPoint;
        canvasOptions.rangeDoubleError = Math.pow(10, getPower(canvasOptions.rangeMax - canvasOptions.rangeMin) - getPower(length) - 2);
        canvasOptions.ratioOfCanvasRange = canvasOptions.canvasLength / (canvasOptions.rangeMaxVisible - canvasOptions.rangeMinVisible);
        if (void 0 !== breaks) {
            canvasOptions.ratioOfCanvasRange = (canvasOptions.canvasLength - breaks[breaks.length - 1].cumulativeWidth) / (canvasOptions.rangeMaxVisible - canvasOptions.rangeMinVisible - breaks[breaks.length - 1].length)
        }
        return canvasOptions
    },
    updateCanvas: function(canvas) {
        this._canvas = validateCanvas(canvas);
        this.reinit()
    },
    updateBusinessRange: function(businessRange) {
        var that = this,
            breaks = businessRange.breaks || [];
        that._businessRange = validateBusinessRange(businessRange);
        that._breaks = breaks.length ? prepareBreaks(breaks, that._businessRange) : void 0;
        that.reinit()
    },
    update: function(businessRange, canvas, options) {
        var that = this;
        that._options = extend(that._options || {}, options);
        that._canvas = validateCanvas(canvas);
        that.updateBusinessRange(businessRange)
    },
    getBusinessRange: function() {
        return this._businessRange
    },
    getCanvasVisibleArea: function() {
        return {
            min: this._canvasOptions.startPoint,
            max: this._canvasOptions.endPoint
        }
    },
    _calculateSpecialValues: function() {
        var invert, canvas_position_default, canvas_position_center_middle, that = this,
            canvasOptions = that._canvasOptions,
            startPoint = canvasOptions.startPoint,
            endPoint = canvasOptions.endPoint,
            range = that._businessRange,
            minVisible = range.minVisible,
            maxVisible = range.maxVisible;
        if (minVisible <= 0 && maxVisible >= 0) {
            canvas_position_default = that.translate(0, 1)
        } else {
            invert = range.invert ^ (minVisible <= 0 && maxVisible <= 0);
            if (that._options.isHorizontal) {
                canvas_position_default = invert ? endPoint : startPoint
            } else {
                canvas_position_default = invert ? startPoint : endPoint
            }
        }
        canvas_position_center_middle = startPoint + canvasOptions.canvasLength / 2;
        that.sc = {
            canvas_position_default: canvas_position_default,
            canvas_position_left: startPoint,
            canvas_position_top: startPoint,
            canvas_position_center: canvas_position_center_middle,
            canvas_position_middle: canvas_position_center_middle,
            canvas_position_right: endPoint,
            canvas_position_bottom: endPoint,
            canvas_position_start: canvasOptions.invert ? endPoint : startPoint,
            canvas_position_end: canvasOptions.invert ? startPoint : endPoint
        }
    },
    translateSpecialCase: function(value) {
        return this.sc[value]
    },
    _calculateProjection: function(distance) {
        var canvasOptions = this._canvasOptions;
        return canvasOptions.invert ? canvasOptions.endPoint - distance : canvasOptions.startPoint + distance
    },
    _calculateUnProjection: function(distance) {
        var canvasOptions = this._canvasOptions;
        return canvasOptions.invert ? canvasOptions.rangeMaxVisible.valueOf() - distance : canvasOptions.rangeMinVisible.valueOf() + distance
    },
    getMinBarSize: function(minBarSize) {
        var visibleArea = this.getCanvasVisibleArea(),
            minValue = this.untranslate(visibleArea.min + minBarSize);
        return _abs(this.untranslate(visibleArea.min) - (!isDefined(minValue) ? this.untranslate(visibleArea.max) : minValue))
    },
    checkMinBarSize: function(value, minShownValue, stackValue) {
        return _abs(value) < minShownValue ? value >= 0 ? minShownValue : -minShownValue : value
    },
    translate: _noop,
    untranslate: _noop,
    getInterval: _noop,
    zoom: _noop,
    getMinScale: _noop,
    zoomArgsIsEqualCanvas: zoomArgsIsEqualCanvas,
    isEqualRange: isEqualRange,
    checkScrollForOriginalScale: checkScrollForOriginalScale,
    scrollHasExtremePosition: scrollHasExtremePosition,
    checkGestureEventsForScaleEdges: checkGestureEventsForScaleEdges,
    getRange: function() {
        return [this.untranslate(this._canvasOptions.startPoint, -1), this.untranslate(this._canvasOptions.endPoint, 1)]
    },
    isEmptyValueRange: function() {
        return this._businessRange.stubData
    },
    getScreenRange: function() {
        return [this._canvasOptions.startPoint, this._canvasOptions.endPoint]
    },
    add: function(value, diff, dir) {
        return this._add(value, diff, (this._businessRange.invert ? -1 : 1) * dir)
    }
};
