/* *
 *
 *  (c) 2010-2020 Torstein Honsi
 *
 *  Extenstion for 3d axes
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
'use strict';
import H from '../parts/Globals.js';
import Tick from '../parts/Tick.js';
import U from '../parts/Utilities.js';
var addEvent = U.addEvent, extend = U.extend, merge = U.merge, pick = U.pick, splat = U.splat, wrap = U.wrap;
import '../parts/Axis.js';
import '../parts/Chart.js';
var ZAxis, Axis = H.Axis, Chart = H.Chart, deg2rad = H.deg2rad, perspective = H.perspective, perspective3D = H.perspective3D, shapeArea = H.shapeArea;
/**
 * @optionparent xAxis
 */
var extendedOptions = {
    labels: {
        /**
         * Defines how the labels are be repositioned according to the 3D chart
         * orientation.
         *
         * - `'offset'`: Maintain a fixed horizontal/vertical distance from the
         *   tick marks, despite the chart orientation. This is the backwards
         *   compatible behavior, and causes skewing of X and Z axes.
         *
         * - `'chart'`: Preserve 3D position relative to the chart.
         *   This looks nice, but hard to read if the text isn't
         *   forward-facing.
         *
         * - `'flap'`: Rotated text along the axis to compensate for the chart
         *   orientation. This tries to maintain text as legible as possible
         *   on all orientations.
         *
         * - `'ortho'`: Rotated text along the axis direction so that the labels
         *   are orthogonal to the axis. This is very similar to `'flap'`,
         *   but prevents skewing the labels (X and Y scaling are still
         *   present).
         *
         * @sample highcharts/3d/skewed-labels/
         *         Skewed labels
         *
         * @since      5.0.15
         * @validvalue ['offset', 'chart', 'flap', 'ortho']
         * @product    highcharts
         * @requires   highcharts-3d
         */
        position3d: 'offset',
        /**
         * If enabled, the axis labels will skewed to follow the perspective.
         *
         * This will fix overlapping labels and titles, but texts become less
         * legible due to the distortion.
         *
         * The final appearance depends heavily on `labels.position3d`.
         *
         * @sample highcharts/3d/skewed-labels/
         *         Skewed labels
         *
         * @since    5.0.15
         * @product  highcharts
         * @requires highcharts-3d
         */
        skew3d: false
    },
    title: {
        /**
         * Defines how the title is repositioned according to the 3D chart
         * orientation.
         *
         * - `'offset'`: Maintain a fixed horizontal/vertical distance from the
         *   tick marks, despite the chart orientation. This is the backwards
         *   compatible behavior, and causes skewing of X and Z axes.
         *
         * - `'chart'`: Preserve 3D position relative to the chart.
         *   This looks nice, but hard to read if the text isn't
         *   forward-facing.
         *
         * - `'flap'`: Rotated text along the axis to compensate for the chart
         *   orientation. This tries to maintain text as legible as possible on
         *   all orientations.
         *
         * - `'ortho'`: Rotated text along the axis direction so that the labels
         *   are orthogonal to the axis. This is very similar to `'flap'`, but
         *   prevents skewing the labels (X and Y scaling are still present).
         *
         * - `undefined`: Will use the config from `labels.position3d`
         *
         * @sample highcharts/3d/skewed-labels/
         *         Skewed labels
         *
         * @type     {"offset"|"chart"|"flap"|"ortho"|null}
         * @since    5.0.15
         * @product  highcharts
         * @requires highcharts-3d
         */
        position3d: null,
        /**
         * If enabled, the axis title will skewed to follow the perspective.
         *
         * This will fix overlapping labels and titles, but texts become less
         * legible due to the distortion.
         *
         * The final appearance depends heavily on `title.position3d`.
         *
         * A `null` value will use the config from `labels.skew3d`.
         *
         * @sample highcharts/3d/skewed-labels/
         *         Skewed labels
         *
         * @type     {boolean|null}
         * @since    5.0.15
         * @product  highcharts
         * @requires highcharts-3d
         */
        skew3d: null
    }
};
/* eslint-disable no-invalid-this */
merge(true, Axis.prototype.defaultOptions, extendedOptions);
addEvent(Axis, 'afterSetOptions', function () {
    var options;
    if (this.chart.is3d && this.chart.is3d() && this.coll !== 'colorAxis') {
        options = this.options;
        options.tickWidth = pick(options.tickWidth, 0);
        options.gridLineWidth = pick(options.gridLineWidth, 1);
    }
});
wrap(Axis.prototype, 'getPlotLinePath', function (proceed) {
    var path = proceed.apply(this, [].slice.call(arguments, 1));
    // Do not do this if the chart is not 3D
    if (!this.chart.is3d() || this.coll === 'colorAxis') {
        return path;
    }
    if (path === null) {
        return path;
    }
    var chart = this.chart, options3d = chart.options.chart.options3d, d = this.isZAxis ? chart.plotWidth : options3d.depth, frame = chart.frame3d;
    var pArr = [
        this.swapZ({ x: path[1], y: path[2], z: 0 }),
        this.swapZ({ x: path[1], y: path[2], z: d }),
        this.swapZ({ x: path[4], y: path[5], z: 0 }),
        this.swapZ({ x: path[4], y: path[5], z: d })
    ];
    var pathSegments = [];
    if (!this.horiz) { // Y-Axis
        if (frame.front.visible) {
            pathSegments.push(pArr[0], pArr[2]);
        }
        if (frame.back.visible) {
            pathSegments.push(pArr[1], pArr[3]);
        }
        if (frame.left.visible) {
            pathSegments.push(pArr[0], pArr[1]);
        }
        if (frame.right.visible) {
            pathSegments.push(pArr[2], pArr[3]);
        }
    }
    else if (this.isZAxis) { // Z-Axis
        if (frame.left.visible) {
            pathSegments.push(pArr[0], pArr[2]);
        }
        if (frame.right.visible) {
            pathSegments.push(pArr[1], pArr[3]);
        }
        if (frame.top.visible) {
            pathSegments.push(pArr[0], pArr[1]);
        }
        if (frame.bottom.visible) {
            pathSegments.push(pArr[2], pArr[3]);
        }
    }
    else { // X-Axis
        if (frame.front.visible) {
            pathSegments.push(pArr[0], pArr[2]);
        }
        if (frame.back.visible) {
            pathSegments.push(pArr[1], pArr[3]);
        }
        if (frame.top.visible) {
            pathSegments.push(pArr[0], pArr[1]);
        }
        if (frame.bottom.visible) {
            pathSegments.push(pArr[2], pArr[3]);
        }
    }
    pathSegments = perspective(pathSegments, this.chart, false);
    return this.chart.renderer.toLineSegments(pathSegments);
});
// Do not draw axislines in 3D
wrap(Axis.prototype, 'getLinePath', function (proceed) {
    // Do not do this if the chart is not 3D
    if (!this.chart.is3d() || this.coll === 'colorAxis') {
        return proceed.apply(this, [].slice.call(arguments, 1));
    }
    return [];
});
wrap(Axis.prototype, 'getPlotBandPath', function (proceed) {
    // Do not do this if the chart is not 3D
    if (!this.chart.is3d() || this.coll === 'colorAxis') {
        return proceed.apply(this, [].slice.call(arguments, 1));
    }
    var args = arguments, from = args[1], to = args[2], path = [], fromPath = this.getPlotLinePath({ value: from }), toPath = this.getPlotLinePath({ value: to });
    if (fromPath && toPath) {
        for (var i = 0; i < fromPath.length; i += 6) {
            path.push('M', fromPath[i + 1], fromPath[i + 2], 'L', fromPath[i + 4], fromPath[i + 5], 'L', toPath[i + 4], toPath[i + 5], 'L', toPath[i + 1], toPath[i + 2], 'Z');
        }
    }
    return path;
});
/**
 * @private
 * @param {Highcharts.Axis} axis
 *        Related axis
 * @param {Highcharts.Position3dObject} pos
 *        Position to fix
 * @param {boolean} [isTitle]
 *        Whether this is a title position
 * @return {Highcharts.Position3dObject}
 *         Fixed position
 */
function fix3dPosition(axis, pos, isTitle) {
    // Do not do this if the chart is not 3D
    if (!axis.chart.is3d() || axis.coll === 'colorAxis') {
        return pos;
    }
    var chart = axis.chart, alpha = deg2rad * chart.options.chart.options3d.alpha, beta = deg2rad * chart.options.chart.options3d.beta, positionMode = pick(isTitle && axis.options.title.position3d, axis.options.labels.position3d), skew = pick(isTitle && axis.options.title.skew3d, axis.options.labels.skew3d), frame = chart.frame3d, plotLeft = chart.plotLeft, plotRight = chart.plotWidth + plotLeft, plotTop = chart.plotTop, plotBottom = chart.plotHeight + plotTop, 
    // Indicates we are labelling an X or Z axis on the "back" of the chart
    reverseFlap = false, offsetX = 0, offsetY = 0, vecX, vecY = { x: 0, y: 1, z: 0 };
    pos = axis.swapZ({ x: pos.x, y: pos.y, z: 0 });
    if (axis.isZAxis) { // Z Axis
        if (axis.opposite) {
            if (frame.axes.z.top === null) {
                return {};
            }
            offsetY = pos.y - plotTop;
            pos.x = frame.axes.z.top.x;
            pos.y = frame.axes.z.top.y;
            vecX = frame.axes.z.top.xDir;
            reverseFlap = !frame.top.frontFacing;
        }
        else {
            if (frame.axes.z.bottom === null) {
                return {};
            }
            offsetY = pos.y - plotBottom;
            pos.x = frame.axes.z.bottom.x;
            pos.y = frame.axes.z.bottom.y;
            vecX = frame.axes.z.bottom.xDir;
            reverseFlap = !frame.bottom.frontFacing;
        }
    }
    else if (axis.horiz) { // X Axis
        if (axis.opposite) {
            if (frame.axes.x.top === null) {
                return {};
            }
            offsetY = pos.y - plotTop;
            pos.y = frame.axes.x.top.y;
            pos.z = frame.axes.x.top.z;
            vecX = frame.axes.x.top.xDir;
            reverseFlap = !frame.top.frontFacing;
        }
        else {
            if (frame.axes.x.bottom === null) {
                return {};
            }
            offsetY = pos.y - plotBottom;
            pos.y = frame.axes.x.bottom.y;
            pos.z = frame.axes.x.bottom.z;
            vecX = frame.axes.x.bottom.xDir;
            reverseFlap = !frame.bottom.frontFacing;
        }
    }
    else { // Y Axis
        if (axis.opposite) {
            if (frame.axes.y.right === null) {
                return {};
            }
            offsetX = pos.x - plotRight;
            pos.x = frame.axes.y.right.x;
            pos.z = frame.axes.y.right.z;
            vecX = frame.axes.y.right.xDir;
            // Rotate 90º on opposite edge
            vecX = { x: vecX.z, y: vecX.y, z: -vecX.x };
        }
        else {
            if (frame.axes.y.left === null) {
                return {};
            }
            offsetX = pos.x - plotLeft;
            pos.x = frame.axes.y.left.x;
            pos.z = frame.axes.y.left.z;
            vecX = frame.axes.y.left.xDir;
        }
    }
    if (positionMode === 'chart') {
        // Labels preserve their direction relative to the chart
        // nothing to do
    }
    else if (positionMode === 'flap') {
        // Labels are be rotated around the axis direction to face the screen
        if (!axis.horiz) { // Y Axis
            vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
        }
        else { // X and Z Axis
            var sin = Math.sin(alpha);
            var cos = Math.cos(alpha);
            if (axis.opposite) {
                sin = -sin;
            }
            if (reverseFlap) {
                sin = -sin;
            }
            vecY = { x: vecX.z * sin, y: cos, z: -vecX.x * sin };
        }
    }
    else if (positionMode === 'ortho') {
        // Labels will be rotated to be ortogonal to the axis
        if (!axis.horiz) { // Y Axis
            vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
        }
        else { // X and Z Axis
            var sina = Math.sin(alpha);
            var cosa = Math.cos(alpha);
            var sinb = Math.sin(beta);
            var cosb = Math.cos(beta);
            var vecZ = { x: sinb * cosa, y: -sina, z: -cosa * cosb };
            vecY = {
                x: vecX.y * vecZ.z - vecX.z * vecZ.y,
                y: vecX.z * vecZ.x - vecX.x * vecZ.z,
                z: vecX.x * vecZ.y - vecX.y * vecZ.x
            };
            var scale = 1 / Math.sqrt(vecY.x * vecY.x + vecY.y * vecY.y + vecY.z * vecY.z);
            if (reverseFlap) {
                scale = -scale;
            }
            vecY = { x: scale * vecY.x, y: scale * vecY.y, z: scale * vecY.z };
        }
    }
    else { // positionMode  == 'offset'
        // Labels will be skewd to maintain vertical / horizontal offsets from
        // axis
        if (!axis.horiz) { // Y Axis
            vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
        }
        else { // X and Z Axis
            vecY = {
                x: Math.sin(beta) * Math.sin(alpha),
                y: Math.cos(alpha),
                z: -Math.cos(beta) * Math.sin(alpha)
            };
        }
    }
    pos.x += offsetX * vecX.x + offsetY * vecY.x;
    pos.y += offsetX * vecX.y + offsetY * vecY.y;
    pos.z += offsetX * vecX.z + offsetY * vecY.z;
    var projected = perspective([pos], axis.chart)[0];
    if (skew) {
        // Check if the label text would be mirrored
        var isMirrored = shapeArea(perspective([
            pos,
            { x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z },
            { x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z }
        ], axis.chart)) < 0;
        if (isMirrored) {
            vecX = { x: -vecX.x, y: -vecX.y, z: -vecX.z };
        }
        var pointsProjected = perspective([
            { x: pos.x, y: pos.y, z: pos.z },
            { x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z },
            { x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z }
        ], axis.chart);
        projected.matrix = [
            pointsProjected[1].x - pointsProjected[0].x,
            pointsProjected[1].y - pointsProjected[0].y,
            pointsProjected[2].x - pointsProjected[0].x,
            pointsProjected[2].y - pointsProjected[0].y,
            projected.x,
            projected.y
        ];
        projected.matrix[4] -= projected.x * projected.matrix[0] +
            projected.y * projected.matrix[2];
        projected.matrix[5] -= projected.x * projected.matrix[1] +
            projected.y * projected.matrix[3];
    }
    return projected;
}
/*
Tick extensions
 */
wrap(Tick.prototype, 'getMarkPath', function (proceed) {
    var path = proceed.apply(this, [].slice.call(arguments, 1));
    var pArr = [
        fix3dPosition(this.axis, { x: path[1], y: path[2], z: 0 }),
        fix3dPosition(this.axis, { x: path[4], y: path[5], z: 0 })
    ];
    return this.axis.chart.renderer.toLineSegments(pArr);
});
addEvent(Tick, 'afterGetLabelPosition', function (e) {
    extend(e.pos, fix3dPosition(this.axis, e.pos));
});
wrap(Axis.prototype, 'getTitlePosition', function (proceed) {
    var pos = proceed.apply(this, [].slice.call(arguments, 1));
    return fix3dPosition(this, pos, true);
});
addEvent(Axis, 'drawCrosshair', function (e) {
    if (this.chart.is3d() && this.coll !== 'colorAxis') {
        if (e.point) {
            e.point.crosshairPos = this.isXAxis ?
                e.point.axisXpos :
                this.len - e.point.axisYpos;
        }
    }
});
addEvent(Axis, 'destroy', function () {
    ['backFrame', 'bottomFrame', 'sideFrame'].forEach(function (prop) {
        if (this[prop]) {
            this[prop] = this[prop].destroy();
        }
    }, this);
});
/*
Z-AXIS
 */
Chart.prototype.addZAxis = function (options) {
    return new ZAxis(this, options);
};
Chart.prototype.collectionsWithUpdate.push('zAxis');
Chart.prototype.collectionsWithInit.zAxis = [Chart.prototype.addZAxis];
Axis.prototype.swapZ = function (p, insidePlotArea) {
    if (this.isZAxis) {
        var plotLeft = insidePlotArea ? 0 : this.chart.plotLeft;
        return {
            x: plotLeft + p.z,
            y: p.y,
            z: p.x - plotLeft
        };
    }
    return p;
};
ZAxis = H.ZAxis = function () {
    this.init.apply(this, arguments);
};
extend(ZAxis.prototype, Axis.prototype);
extend(ZAxis.prototype, {
    isZAxis: true,
    setOptions: function (userOptions) {
        userOptions = merge({
            offset: 0,
            lineWidth: 0
        }, userOptions);
        Axis.prototype.setOptions.call(this, userOptions);
        this.coll = 'zAxis';
    },
    setAxisSize: function () {
        Axis.prototype.setAxisSize.call(this);
        this.width = this.len =
            this.chart.options.chart.options3d.depth;
        this.right = this.chart.chartWidth - this.width - this.left;
    },
    getSeriesExtremes: function () {
        var axis = this, chart = axis.chart;
        axis.hasVisibleSeries = false;
        // Reset properties in case we're redrawing (#3353)
        axis.dataMin =
            axis.dataMax =
                axis.ignoreMinPadding =
                    axis.ignoreMaxPadding = null;
        if (axis.buildStacks) {
            axis.buildStacks();
        }
        // loop through this axis' series
        axis.series.forEach(function (series) {
            if (series.visible ||
                !chart.options.chart.ignoreHiddenSeries) {
                var seriesOptions = series.options, zData, threshold = seriesOptions.threshold;
                axis.hasVisibleSeries = true;
                // Validate threshold in logarithmic axes
                if (axis.positiveValuesOnly && threshold <= 0) {
                    threshold = null;
                }
                zData = series.zData;
                if (zData.length) {
                    axis.dataMin = Math.min(pick(axis.dataMin, zData[0]), Math.min.apply(null, zData));
                    axis.dataMax = Math.max(pick(axis.dataMax, zData[0]), Math.max.apply(null, zData));
                }
            }
        });
    }
});
// Get the Z axis in addition to the default X and Y.
addEvent(Chart, 'afterGetAxes', function () {
    var chart = this, options = this.options, zAxisOptions = options.zAxis = splat(options.zAxis || {});
    if (!chart.is3d()) {
        return;
    }
    this.zAxis = [];
    zAxisOptions.forEach(function (axisOptions, i) {
        axisOptions.index = i;
        // Z-Axis is shown horizontally, so it's kind of a X-Axis
        axisOptions.isX = true;
        var zAxis = chart.addZAxis(axisOptions);
        zAxis.setScale();
    });
});
// Wrap getSlotWidth function to calculate individual width value for each slot
// (#8042).
wrap(Axis.prototype, 'getSlotWidth', function (proceed, tick) {
    if (this.chart.is3d() &&
        tick &&
        tick.label &&
        this.categories &&
        this.chart.frameShapes) {
        var chart = this.chart, ticks = this.ticks, gridGroup = this.gridGroup.element.childNodes, firstGridLine = gridGroup[0].getBBox(), frame3DLeft = chart.frameShapes.left.getBBox(), options3d = chart.options.chart.options3d, origin = {
            x: chart.plotWidth / 2,
            y: chart.plotHeight / 2,
            z: options3d.depth / 2,
            vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0)
        }, labelPos, prevLabelPos, nextLabelPos, slotWidth, tickId = tick.pos, prevTick = ticks[tickId - 1], nextTick = ticks[tickId + 1];
        // Check whether the tick is not the first one and previous tick exists,
        // then calculate position of previous label.
        if (tickId !== 0 && prevTick && prevTick.label.xy) { // #8621
            prevLabelPos = perspective3D({
                x: prevTick.label.xy.x,
                y: prevTick.label.xy.y,
                z: null
            }, origin, origin.vd);
        }
        // If next label position is defined, then recalculate its position
        // basing on the perspective.
        if (nextTick && nextTick.label.xy) {
            nextLabelPos = perspective3D({
                x: nextTick.label.xy.x,
                y: nextTick.label.xy.y,
                z: null
            }, origin, origin.vd);
        }
        labelPos = {
            x: tick.label.xy.x,
            y: tick.label.xy.y,
            z: null
        };
        labelPos = perspective3D(labelPos, origin, origin.vd);
        // If tick is first one, check whether next label position is already
        // calculated, then return difference between the first and the second
        // label. If there is no next label position calculated, return the
        // difference between the first grid line and left 3d frame.
        slotWidth = Math.abs(prevLabelPos ?
            labelPos.x - prevLabelPos.x : nextLabelPos ?
            nextLabelPos.x - labelPos.x :
            firstGridLine.x - frame3DLeft.x);
        return slotWidth;
    }
    return proceed.apply(this, [].slice.call(arguments, 1));
});
