/* *
 *
 *  (c) 2010-2026 Highsoft AS
 *  Author: Torstein Hønsi
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 * */
'use strict';
import Color from '../Color/Color.js';
const { parse: color } = Color;
import H from '../Globals.js';
const { win } = H;
import { isNumber, objectEach } from '../../Shared/Utilities.js';
/* eslint-disable no-invalid-this, valid-jsdoc */
/* *
 *
 *  Class
 *
 * */
/**
 * An animator object used internally. One instance applies to one property
 * (attribute or style prop) on one element. Animation is always initiated
 * through {@link SVGElement#animate}.
 *
 * @example
 * let rect = renderer.rect(0, 0, 10, 10).add();
 * rect.animate({ width: 100 });
 *
 * @internal
 * @class
 * @name Highcharts.Fx
 *
 * @param {Highcharts.HTMLDOMElement|Highcharts.SVGElement} elem
 * The element to animate.
 *
 * @param {Partial<Highcharts.AnimationOptionsObject>} options
 * Animation options.
 *
 * @param {string} prop
 * The single attribute or CSS property to animate.
 */
class Fx {
    /* *
     *
     *  Constructors
     *
     * */
    constructor(elem, options, prop) {
        /**
         * Current position of the animation, a value between 0 and 1.
         * @internal
         */
        this.pos = NaN;
        this.options = options;
        this.elem = elem;
        this.prop = prop;
    }
    /* *
     *
     *  Functions
     *
     * */
    /**
     * Set the current step of a path definition on SVGElement.
     *
     * @function Highcharts.Fx#dSetter
     *
     */
    dSetter() {
        const paths = this.paths, start = paths?.[0], end = paths?.[1], now = this.now || 0;
        let path = [];
        // Land on the final path without adjustment points appended in the ends
        if (now === 1 || !start || !end) {
            path = this.toD || [];
        }
        else if (start.length === end.length && now < 1) {
            for (let i = 0; i < end.length; i++) {
                // Tween between the start segment and the end segment. Start
                // with a copy of the end segment and tween the appropriate
                // numerics
                const startSeg = start[i];
                const endSeg = end[i];
                const tweenSeg = [];
                for (let j = 0; j < endSeg.length; j++) {
                    const startItem = startSeg[j];
                    const endItem = endSeg[j];
                    // Tween numbers
                    if (isNumber(startItem) &&
                        isNumber(endItem) &&
                        // Arc boolean flags
                        !(endSeg[0] === 'A' && (j === 4 || j === 5))) {
                        tweenSeg[j] = startItem + now * (endItem - startItem);
                        // Strings, take directly from the end segment
                    }
                    else {
                        tweenSeg[j] = endItem;
                    }
                }
                path.push(tweenSeg);
            }
            // If animation is finished or length not matching, land on right value
        }
        else {
            path = end;
        }
        this.elem.attr('d', path, void 0, true);
    }
    /**
     * Update the element with the current animation step.
     *
     * @function Highcharts.Fx#update
     *
     */
    update() {
        const elem = this.elem, prop = this.prop, // If destroyed, it is null
        now = this.now ?? 1, step = this.options.step;
        // Animation setter defined from outside
        if (this[prop + 'Setter']) {
            this[prop + 'Setter']();
            // Other animations on SVGElement
        }
        else if (elem && elem.attr) {
            if (elem.element) {
                elem.attr(prop, now, void 0, true);
            }
            // HTML styles, raw HTML content like container size
        }
        else if (elem) {
            elem.style[prop] = now + (this.unit || '');
        }
        step?.call(elem, now, this);
    }
    /**
     * Run an animation.
     *
     * @function Highcharts.Fx#run
     *
     * @param {number} from
     *        The current value, value to start from.
     *
     * @param {number} to
     *        The end value, value to land on.
     *
     * @param {string} unit
     *        The property unit, for example `px`.
     *
     */
    run(from, to, unit) {
        const { elem, options } = this, { complete, curAnim = {} } = options, timer = (gotoEnd) => (timer.stopped ? false : this.step(gotoEnd)), requestAnimationFrame = win.requestAnimationFrame ||
            function (step) {
                setTimeout(step, 13);
            }, step = () => {
            for (let i = 0; i < Fx.timers.length; i++) {
                if (!Fx.timers[i]()) {
                    Fx.timers.splice(i--, 1);
                }
            }
            if (Fx.timers.length) {
                requestAnimationFrame(step);
            }
        };
        if (from === to && !elem['forceAnimate:' + this.prop]) {
            delete curAnim[this.prop];
            if (complete &&
                Object.keys(curAnim).length === 0) {
                complete.call(elem);
            }
        }
        else { // #7166
            this.startTime = +new Date();
            this.start = from;
            this.end = to;
            this.unit = unit;
            this.now = this.start;
            this.pos = 0;
            timer.elem = elem;
            timer.prop = this.prop;
            if (timer() && Fx.timers.push(timer) === 1) {
                requestAnimationFrame(step);
            }
        }
    }
    /**
     * Run a single step in the animation.
     *
     * @function Highcharts.Fx#step
     *
     * @param {boolean} [gotoEnd]
     *        Whether to go to the endpoint of the animation after abort.
     *
     * @return {boolean}
     *         Returns `true` if animation continues.
     */
    step(gotoEnd) {
        const t = +new Date(), options = this.options, elem = this.elem, complete = options.complete, duration = options.duration, curAnim = options.curAnim;
        let ret, done;
        // #2616, element is destroyed
        if (elem?.attr && !elem.element) {
            ret = false;
        }
        else if (gotoEnd || t >= duration + this.startTime) {
            this.now = this.end;
            this.pos = 1;
            this.update();
            curAnim[this.prop] = true;
            done = true;
            objectEach(curAnim, function (val) {
                if (val !== true) {
                    done = false;
                }
            });
            if (done && complete) {
                complete.call(elem);
            }
            ret = false;
        }
        else {
            this.pos = options.easing((t - this.startTime) / duration);
            this.now = this.start + ((this.end -
                this.start) * this.pos);
            this.update();
            ret = true;
        }
        return ret;
    }
    /**
     * Prepare start and end values so that the path can be animated one to one.
     *
     * @function Highcharts.Fx#initPath
     *
     * @param {Highcharts.SVGElement} elem
     *        The SVGElement item.
     *
     * @param {Highcharts.SVGPathArray|undefined} fromD
     *        Starting path definition.
     *
     * @param {Highcharts.SVGPathArray} toD
     *        Ending path definition.
     *
     * @return {Array<Highcharts.SVGPathArray,Highcharts.SVGPathArray>}
     *         An array containing start and end paths in array form so that
     *         they can be animated in parallel.
     */
    initPath(elem, fromD, toD) {
        const startX = elem.startX, endX = elem.endX, end = toD.slice(), // Copy
        isArea = elem.isArea, positionFactor = isArea ? 2 : 1, disableAnimation = fromD &&
            toD.length > fromD.length &&
            toD.hasStackedCliffs; // #16925
        let shift, fullLength, i, reverse, start = fromD?.slice(); // Copy
        if (!start || disableAnimation) {
            return [end, end];
        }
        /**
         * If shifting points, prepend a dummy point to the end path.
         * @internal
         */
        function prepend(arr, other) {
            while (arr.length < fullLength) {
                // Move to, line to or curve to?
                const moveSegment = arr[0], otherSegment = other[fullLength - arr.length];
                if (otherSegment && moveSegment[0] === 'M') {
                    if (otherSegment[0] === 'C') {
                        arr[0] = [
                            'C',
                            moveSegment[1],
                            moveSegment[2],
                            moveSegment[1],
                            moveSegment[2],
                            moveSegment[1],
                            moveSegment[2]
                        ];
                    }
                    else {
                        arr[0] = ['L', moveSegment[1], moveSegment[2]];
                    }
                }
                // Prepend a copy of the first point
                arr.unshift(moveSegment);
                // For areas, the bottom path goes back again to the left, so we
                // need to append a copy of the last point.
                if (isArea) {
                    const z = arr.pop();
                    arr.push(arr[arr.length - 1], z); // Append point and the Z
                }
            }
        }
        /**
         * Copy and append last point until the length matches the end length.
         * @internal
         */
        function append(arr) {
            while (arr.length < fullLength) {
                // Pull out the slice that is going to be appended or inserted.
                // In a line graph, the positionFactor is 1, and the last point
                // is sliced out. In an area graph, the positionFactor is 2,
                // causing the middle two points to be sliced out, since an area
                // path starts at left, follows the upper path then turns and
                // follows the bottom back.
                const segmentToAdd = arr[Math.floor(arr.length / positionFactor) - 1].slice();
                // Disable the first control point of curve segments
                if (segmentToAdd[0] === 'C') {
                    segmentToAdd[1] = segmentToAdd[5];
                    segmentToAdd[2] = segmentToAdd[6];
                }
                if (!isArea) {
                    arr.push(segmentToAdd);
                }
                else {
                    const lowerSegmentToAdd = arr[Math.floor(arr.length / positionFactor)].slice();
                    arr.splice(arr.length / 2, 0, segmentToAdd, lowerSegmentToAdd);
                }
            }
        }
        // For sideways animation, find out how much we need to shift to get the
        // start path Xs to match the end path Xs.
        if (startX && endX && endX.length) {
            for (i = 0; i < startX.length; i++) {
                // Moving left, new points coming in on right
                if (startX[i] === endX[0]) {
                    shift = i;
                    break;
                    // Moving right
                }
                else if (startX[0] ===
                    endX[endX.length - startX.length + i]) {
                    shift = i;
                    reverse = true;
                    break;
                    // Fixed from the right side, "scaling" left
                }
                else if (startX[startX.length - 1] ===
                    endX[endX.length - startX.length + i]) {
                    shift = startX.length - i;
                    break;
                }
            }
            if (typeof shift === 'undefined') {
                start = [];
            }
        }
        if (start.length && isNumber(shift)) {
            // The common target length for the start and end array, where both
            // arrays are padded in opposite ends
            fullLength = end.length + shift * positionFactor;
            if (!reverse) {
                prepend(end, start);
                append(start);
            }
            else {
                prepend(start, end);
                append(end);
            }
        }
        return [start, end];
    }
    /**
     * Handle animation of the color attributes directly.
     *
     * @function Highcharts.Fx#fillSetter
     *
     */
    fillSetter() {
        Fx.prototype.strokeSetter.apply(this, arguments);
    }
    /**
     * Handle animation of the color attributes directly.
     *
     * @function Highcharts.Fx#strokeSetter
     *
     */
    strokeSetter() {
        this.elem.attr(this.prop, color(this.start).tweenTo(color(this.end), this.pos), void 0, true);
    }
}
/* *
 *
 *  Static Properties
 *
 * */
/** @internal */
Fx.timers = [];
/* *
 *
 *  Default Export
 *
 * */
/** @internal */
export default Fx;
