/**
 * @module echarts/animation/Animator
 */

import Clip from './Clip';
import * as color from '../tool/color';
import {eqNaN, extend, isArrayLike, isFunction, isGradientObject, isNumber, isString, keys, logError, map} from '../core/util';
import {ArrayLike, Dictionary} from '../core/types';
import easingFuncs, { AnimationEasing } from './easing';
import Animation from './Animation';
import { createCubicEasingFunc } from './cubicEasing';
import { isLinearGradient, isRadialGradient } from '../svg/helper';

type NumberArray = ArrayLike<number>
type InterpolatableType = string | number | NumberArray | NumberArray[];

interface ParsedColorStop {
    color: number[],
    offset: number
};

interface ParsedGradientObject {
    colorStops: ParsedColorStop[]
    x: number
    y: number
    global: boolean
}
interface ParsedLinearGradientObject extends ParsedGradientObject {
    x2: number
    y2: number
}
interface ParsedRadialGradientObject extends ParsedGradientObject {
    r: number
}

const arraySlice = Array.prototype.slice;

function interpolateNumber(p0: number, p1: number, percent: number): number {
    return (p1 - p0) * percent + p0;
}
function interpolate1DArray(
    out: NumberArray,
    p0: NumberArray,
    p1: NumberArray,
    percent: number
) {
    // TODO Handling different length TypedArray
    const len = p0.length;
    for (let i = 0; i < len; i++) {
        out[i] = interpolateNumber(p0[i], p1[i], percent);
    }
    return out;
}

function interpolate2DArray(
    out: NumberArray[],
    p0: NumberArray[],
    p1: NumberArray[],
    percent: number
) {
    const len = p0.length;
    // TODO differnt length on each item?
    const len2 = len && p0[0].length;
    for (let i = 0; i < len; i++) {
        if (!out[i]) {
            out[i] = [];
        }
        for (let j = 0; j < len2; j++) {
            out[i][j] = interpolateNumber(p0[i][j], p1[i][j], percent);
        }
    }
    return out;
}

function add1DArray(
    out: NumberArray,
    p0: NumberArray,
    p1: NumberArray,
    sign: 1 | -1
) {
    const len = p0.length;
    for (let i = 0; i < len; i++) {
        out[i] = p0[i] + p1[i] * sign;
    }
    return out;
}

function add2DArray(
    out: NumberArray[],
    p0: NumberArray[],
    p1: NumberArray[],
    sign: 1 | -1
) {
    const len = p0.length;
    const len2 = len && p0[0].length;
    for (let i = 0; i < len; i++) {
        if (!out[i]) {
            out[i] = [];
        }
        for (let j = 0; j < len2; j++) {
            out[i][j] = p0[i][j] + p1[i][j] * sign;
        }
    }
    return out;
}

function fillColorStops(val0: ParsedColorStop[], val1: ParsedColorStop[]) {
    const len0 = val0.length;
    const len1 = val1.length;

    const shorterArr = len0 > len1 ? val1 : val0;
    const shorterLen = Math.min(len0, len1);
    const last = shorterArr[shorterLen - 1] || { color: [0, 0, 0, 0], offset: 0 };
    for (let i = shorterLen; i < Math.max(len0, len1); i++) {
        // Use last color stop to fill the shorter array
        shorterArr.push({
            offset: last.offset,
            color: last.color.slice()
        });
    }
}
// arr0 is source array, arr1 is target array.
// Do some preprocess to avoid error happened when interpolating from arr0 to arr1
function fillArray(
    val0: NumberArray | NumberArray[],
    val1: NumberArray | NumberArray[],
    arrDim: 1 | 2
) {
    // TODO Handling different length TypedArray
    let arr0 = val0 as (number | number[])[];
    let arr1 = val1 as (number | number[])[];
    if (!arr0.push || !arr1.push) {
        return;
    }
    const arr0Len = arr0.length;
    const arr1Len = arr1.length;
    if (arr0Len !== arr1Len) {
        // FIXME Not work for TypedArray
        const isPreviousLarger = arr0Len > arr1Len;
        if (isPreviousLarger) {
            // Cut the previous
            arr0.length = arr1Len;
        }
        else {
            // Fill the previous
            for (let i = arr0Len; i < arr1Len; i++) {
                arr0.push(arrDim === 1 ? arr1[i] : arraySlice.call(arr1[i]));
            }
        }
    }
    // Handling NaN value
    const len2 = arr0[0] && (arr0[0] as number[]).length;
    for (let i = 0; i < arr0.length; i++) {
        if (arrDim === 1) {
            if (isNaN(arr0[i] as number)) {
                arr0[i] = arr1[i];
            }
        }
        else {
            for (let j = 0; j < len2; j++) {
                if (isNaN((arr0 as number[][])[i][j])) {
                    (arr0 as number[][])[i][j] = (arr1 as number[][])[i][j];
                }
            }
        }
    }
}

export function cloneValue(value: InterpolatableType) {
    if (isArrayLike(value)) {
        const len = value.length;
        if (isArrayLike(value[0])) {
            const ret = [];
            for (let i = 0; i < len; i++) {
                ret.push(arraySlice.call(value[i]));
            }
            return ret;
        }

        return arraySlice.call(value);
    }

    return value;
}

function rgba2String(rgba: number[]): string {
    rgba[0] = Math.floor(rgba[0]) || 0;
    rgba[1] = Math.floor(rgba[1]) || 0;
    rgba[2] = Math.floor(rgba[2]) || 0;
    rgba[3] = rgba[3] == null ? 1 : rgba[3];

    return 'rgba(' + rgba.join(',') + ')';
}

function guessArrayDim(value: ArrayLike<unknown>): 1 | 2 {
    return isArrayLike(value && (value as ArrayLike<unknown>)[0]) ? 2 : 1;
}

const VALUE_TYPE_NUMBER = 0;
const VALUE_TYPE_1D_ARRAY = 1;
const VALUE_TYPE_2D_ARRAY = 2;
const VALUE_TYPE_COLOR = 3;
const VALUE_TYPE_LINEAR_GRADIENT = 4;
const VALUE_TYPE_RADIAL_GRADIENT = 5;
// Other value type that can only use discrete animation.
const VALUE_TYPE_UNKOWN = 6;

type ValueType = 0 | 1 | 2 | 3 | 4 | 5 | 6;

type Keyframe = {
    time: number
    value: unknown
    percent: number
    // Raw value for discrete animation.
    rawValue: unknown

    easing?: AnimationEasing    // Raw easing
    easingFunc?: (percent: number) => number
    additiveValue?: unknown
}


function isGradientValueType(valType: ValueType): valType is 4 | 5 {
    return valType === VALUE_TYPE_LINEAR_GRADIENT || valType === VALUE_TYPE_RADIAL_GRADIENT;
}
function isArrayValueType(valType: ValueType): valType is 1 | 2 {
    return valType === VALUE_TYPE_1D_ARRAY || valType === VALUE_TYPE_2D_ARRAY;
}


let tmpRgba: number[] = [0, 0, 0, 0];

class Track {

    keyframes: Keyframe[] = []

    propName: string

    valType: ValueType

    discrete: boolean = false

    _invalid: boolean = false;

    private _finished: boolean

    private _needsSort: boolean = false

    private _additiveTrack: Track
    // Temporal storage for interpolated additive value.
    private _additiveValue: unknown

    // Info for run
    /**
     * Last frame
     */
    private _lastFr = 0
    /**
     * Percent of last frame.
     */
    private _lastFrP = 0

    constructor(propName: string) {
        this.propName = propName;
    }

    isFinished() {
        return this._finished;
    }

    setFinished() {
        this._finished = true;
        // Also set additive track to finished.
        // Make sure the final value stopped on the latest track
        if (this._additiveTrack) {
            this._additiveTrack.setFinished();
        }
    }

    needsAnimate() {
        return this.keyframes.length >= 1;
    }

    getAdditiveTrack() {
        return this._additiveTrack;
    }

    addKeyframe(time: number, rawValue: unknown, easing?: AnimationEasing) {
        this._needsSort = true;

        let keyframes = this.keyframes;
        let len = keyframes.length;

        let discrete = false;
        let valType: ValueType = VALUE_TYPE_UNKOWN;
        let value = rawValue;

        // Handling values only if it's possible to be interpolated.
        if (isArrayLike(rawValue)) {
            let arrayDim = guessArrayDim(rawValue);
            valType = arrayDim;
            // Not a number array.
            if (arrayDim === 1 && !isNumber(rawValue[0])
                || arrayDim === 2 && !isNumber(rawValue[0][0])) {
                discrete = true;
            }
        }
        else {
            if (isNumber(rawValue) && !eqNaN(rawValue)) {
                valType = VALUE_TYPE_NUMBER;
            }
            else if (isString(rawValue)) {
                if (!isNaN(+rawValue)) {    // Can be string number like '2'
                    valType = VALUE_TYPE_NUMBER;
                }
                else {
                    const colorArray = color.parse(rawValue);
                    if (colorArray) {
                        value = colorArray;
                        valType = VALUE_TYPE_COLOR;
                    }
                }
            }
            else if (isGradientObject(rawValue)) {
                // TODO Color to gradient or gradient to color.
                const parsedGradient = extend({}, value) as unknown as ParsedGradientObject;
                parsedGradient.colorStops = map(rawValue.colorStops, colorStop => ({
                    offset: colorStop.offset,
                    color: color.parse(colorStop.color)
                }));
                if (isLinearGradient(rawValue)) {
                    valType = VALUE_TYPE_LINEAR_GRADIENT;
                }
                else if (isRadialGradient(rawValue)) {
                    valType = VALUE_TYPE_RADIAL_GRADIENT;
                }
                value = parsedGradient;
            }
        }

        if (len === 0) {
            // Inference type from the first keyframe.
            this.valType = valType;
        }
         // Not same value type or can't be interpolated.
        else if (valType !== this.valType || valType === VALUE_TYPE_UNKOWN) {
            discrete = true;
        }

        this.discrete = this.discrete || discrete;

        const kf: Keyframe = {
            time,
            value,
            rawValue,
            percent: 0
        };
        if (easing) {
            // Save the raw easing name to be used in css animation output
            kf.easing = easing;
            kf.easingFunc = isFunction(easing)
                ? easing
                : easingFuncs[easing] || createCubicEasingFunc(easing);
        }
        // Not check if value equal here.
        keyframes.push(kf);
        return kf;
    }

    prepare(maxTime: number, additiveTrack?: Track) {
        let kfs = this.keyframes;
        if (this._needsSort) {
            // Sort keyframe as ascending
            kfs.sort(function (a: Keyframe, b: Keyframe) {
                return a.time - b.time;
            });
        }

        const valType = this.valType;
        const kfsLen = kfs.length;
        const lastKf = kfs[kfsLen - 1];
        const isDiscrete = this.discrete;
        const isArr = isArrayValueType(valType);
        const isGradient = isGradientValueType(valType);

        for (let i = 0; i < kfsLen; i++) {
            const kf = kfs[i];
            const value = kf.value;
            const lastValue = lastKf.value;
            kf.percent = kf.time / maxTime;
            if (!isDiscrete) {
                if (isArr && i !== kfsLen - 1) {
                    // Align array with target frame.
                    fillArray(value as NumberArray, lastValue as NumberArray, valType);
                }
                else if (isGradient) {
                    fillColorStops(
                        (value as ParsedLinearGradientObject).colorStops,
                        (lastValue as ParsedLinearGradientObject).colorStops
                    );
                }
            }
        }

        // Only apply additive animaiton on INTERPOLABLE SAME TYPE values.
        if (
            !isDiscrete
            // TODO support gradient
            && valType !== VALUE_TYPE_RADIAL_GRADIENT
            && additiveTrack
            // If two track both will be animated and have same value format.
            && this.needsAnimate()
            && additiveTrack.needsAnimate()
            && valType === additiveTrack.valType
            && !additiveTrack._finished
        ) {
            this._additiveTrack = additiveTrack;

            const startValue = kfs[0].value;
            // Calculate difference
            for (let i = 0; i < kfsLen; i++) {
                if (valType === VALUE_TYPE_NUMBER) {
                    kfs[i].additiveValue = kfs[i].value as number - (startValue as number);
                }
                else if (valType === VALUE_TYPE_COLOR) {
                    kfs[i].additiveValue =
                        add1DArray([], kfs[i].value as NumberArray, startValue as NumberArray, -1);
                }
                else if (isArrayValueType(valType)) {
                    kfs[i].additiveValue = valType === VALUE_TYPE_1D_ARRAY
                        ? add1DArray([], kfs[i].value as NumberArray, startValue as NumberArray, -1)
                        : add2DArray([], kfs[i].value as NumberArray[], startValue as NumberArray[], -1);
                }
            }
        }
    }

    step(target: any, percent: number) {
        if (this._finished) {   // Track may be set to finished.
            return;
        }

        if (this._additiveTrack && this._additiveTrack._finished) {
            // Remove additive track if it's finished.
            this._additiveTrack = null;
        }
        const isAdditive = this._additiveTrack != null;
        const valueKey = isAdditive ? 'additiveValue' : 'value';

        const valType = this.valType;
        const keyframes = this.keyframes;
        const kfsNum = keyframes.length;
        const propName = this.propName;
        const isValueColor = valType === VALUE_TYPE_COLOR;
        // Find the range keyframes
        // kf1-----kf2---------current--------kf3
        // find kf2 and kf3 and do interpolation
        let frameIdx;
        const lastFrame = this._lastFr;
        const mathMin = Math.min;
        let frame;
        let nextFrame;
        if (kfsNum === 1) {
            frame = nextFrame = keyframes[0];
        }
        else {
            // In the easing function like elasticOut, percent may less than 0
            if (percent < 0) {
                frameIdx = 0;
            }
            else if (percent < this._lastFrP) {
                // Start from next key
                // PENDING start from lastFrame ?
                const start = mathMin(lastFrame + 1, kfsNum - 1);
                for (frameIdx = start; frameIdx >= 0; frameIdx--) {
                    if (keyframes[frameIdx].percent <= percent) {
                        break;
                    }
                }
                frameIdx = mathMin(frameIdx, kfsNum - 2);
            }
            else {
                for (frameIdx = lastFrame; frameIdx < kfsNum; frameIdx++) {
                    if (keyframes[frameIdx].percent > percent) {
                        break;
                    }
                }
                frameIdx = mathMin(frameIdx - 1, kfsNum - 2);
            }

            nextFrame = keyframes[frameIdx + 1];
            frame = keyframes[frameIdx];
        }

        // Defensive coding.
        if (!(frame && nextFrame)) {
            return;
        }

        this._lastFr = frameIdx;
        this._lastFrP = percent;

        const interval = (nextFrame.percent - frame.percent);
        let w = interval === 0 ? 1 : mathMin((percent - frame.percent) / interval, 1);

        // Apply different easing of each keyframe.
        // Use easing specified in target frame.
        if (nextFrame.easingFunc) {
            w = nextFrame.easingFunc(w);
        }

        // If value is arr
        let targetArr = isAdditive ? this._additiveValue
            : (isValueColor ? tmpRgba : target[propName]);

        if ((isArrayValueType(valType) || isValueColor) && !targetArr) {
            targetArr = this._additiveValue = [];
        }

        if (this.discrete) {
            // use raw value without parse in discrete animation.
            target[propName] = w < 1 ? frame.rawValue : nextFrame.rawValue;
        }
        else if (isArrayValueType(valType)) {
            valType === VALUE_TYPE_1D_ARRAY
                ? interpolate1DArray(
                    targetArr as NumberArray,
                    frame[valueKey] as NumberArray,
                    nextFrame[valueKey] as NumberArray,
                    w
                )
                : interpolate2DArray(
                    targetArr as NumberArray[],
                    frame[valueKey] as NumberArray[],
                    nextFrame[valueKey] as NumberArray[],
                    w
                );
        }
        else if (isGradientValueType(valType)) {
            const val = frame[valueKey] as ParsedGradientObject;
            const nextVal = nextFrame[valueKey] as ParsedGradientObject;
            const isLinearGradient = valType === VALUE_TYPE_LINEAR_GRADIENT;
            target[propName] = {
                type: isLinearGradient ? 'linear' : 'radial',
                x: interpolateNumber(val.x, nextVal.x, w),
                y: interpolateNumber(val.y, nextVal.y, w),
                // TODO performance
                colorStops: map(val.colorStops, (colorStop, idx) => {
                    const nextColorStop = nextVal.colorStops[idx];
                    return {
                        offset: interpolateNumber(colorStop.offset, nextColorStop.offset, w),
                        color: rgba2String(interpolate1DArray(
                            [] as number[], colorStop.color, nextColorStop.color, w
                        ) as number[])
                    };
                }),
                global: nextVal.global
            };
            if (isLinearGradient) {
                // Linear
                target[propName].x2 = interpolateNumber(
                    (val as ParsedLinearGradientObject).x2, (nextVal as ParsedLinearGradientObject).x2, w
                );
                target[propName].y2 = interpolateNumber(
                    (val as ParsedLinearGradientObject).y2, (nextVal as ParsedLinearGradientObject).y2, w
                );
            }
            else {
                // Radial
                target[propName].r = interpolateNumber(
                    (val as ParsedRadialGradientObject).r, (nextVal as ParsedRadialGradientObject).r, w
                );
            }
        }
        else if (isValueColor) {
            interpolate1DArray(
                targetArr,
                frame[valueKey] as NumberArray,
                nextFrame[valueKey] as NumberArray,
                w
            );
            if (!isAdditive) {  // Convert to string later:)
                target[propName] = rgba2String(targetArr);
            }
        }
        else {
            const value = interpolateNumber(frame[valueKey] as number, nextFrame[valueKey] as number, w);
            if (isAdditive) {
                this._additiveValue = value;
            }
            else {
                target[propName] = value;
            }
        }

        // Add additive to target
        if (isAdditive) {
            this._addToTarget(target);
        }
    }

    private _addToTarget(target: any) {
        const valType = this.valType;
        const propName = this.propName;
        const additiveValue = this._additiveValue;

        if (valType === VALUE_TYPE_NUMBER) {
            // Add a difference value based on the change of previous frame.
            target[propName] = target[propName] + additiveValue;
        }
        else if (valType === VALUE_TYPE_COLOR) {
            // TODO reduce unnecessary parse
            color.parse(target[propName], tmpRgba);
            add1DArray(tmpRgba, tmpRgba, additiveValue as NumberArray, 1);
            target[propName] = rgba2String(tmpRgba);
        }
        else if (valType === VALUE_TYPE_1D_ARRAY) {
            add1DArray(target[propName], target[propName], additiveValue as NumberArray, 1);
        }
        else if (valType === VALUE_TYPE_2D_ARRAY) {
            add2DArray(target[propName], target[propName], additiveValue as NumberArray[], 1);
        }
    }
}


type DoneCallback = () => void;
type AbortCallback = () => void;
export type OnframeCallback<T> = (target: T, percent: number) => void;

export type AnimationPropGetter<T> = (target: T, key: string) => InterpolatableType;
export type AnimationPropSetter<T> = (target: T, key: string, value: InterpolatableType) => void;

export default class Animator<T> {

    animation?: Animation

    targetName?: string

    scope?: string

    __fromStateTransition?: string

    private _tracks: Dictionary<Track> = {}
    private _trackKeys: string[] = []

    private _target: T

    private _loop: boolean
    private _delay: number
    private _maxTime = 0

    /**
     * If force run regardless of empty tracks when duration is set.
     */
    private _force: boolean;

    /**
     * If animator is paused
     * @default false
     */
    private _paused: boolean
    // 0: Not started
    // 1: Invoked started
    // 2: Has been run for at least one frame.
    private _started = 0

    /**
     * If allow discrete animation
     * @default false
     */
    private _allowDiscrete: boolean

    private _additiveAnimators: Animator<any>[]

    private _doneCbs: DoneCallback[]
    private _onframeCbs: OnframeCallback<T>[]

    private _abortedCbs: AbortCallback[]

    private _clip: Clip = null

    constructor(
        target: T,
        loop: boolean,
        allowDiscreteAnimation?: boolean,  // If doing discrete animation on the values can't be interpolated
        additiveTo?: Animator<any>[]
    ) {
        this._target = target;
        this._loop = loop;
        if (loop && additiveTo) {
            logError('Can\' use additive animation on looped animation.');
            return;
        }
        this._additiveAnimators = additiveTo;

        this._allowDiscrete = allowDiscreteAnimation;
    }

    getMaxTime() {
        return this._maxTime;
    }

    getDelay() {
        return this._delay;
    }

    getLoop() {
        return this._loop;
    }

    getTarget() {
        return this._target;
    }

    /**
     * Target can be changed during animation
     * For example if style is changed during state change.
     * We need to change target to the new style object.
     */
    changeTarget(target: T) {
        this._target = target;
    }

    /**
     * Set Animation keyframe
     * @param time time of keyframe in ms
     * @param props key-value props of keyframe.
     * @param easing
     */
    when(time: number, props: Dictionary<any>, easing?: AnimationEasing) {
        return this.whenWithKeys(time, props, keys(props) as string[], easing);
    }


    // Fast path for add keyframes of aniamteTo
    whenWithKeys(time: number, props: Dictionary<any>, propNames: string[], easing?: AnimationEasing) {
        const tracks = this._tracks;
        for (let i = 0; i < propNames.length; i++) {
            const propName = propNames[i];

            let track = tracks[propName];
            if (!track) {
                track = tracks[propName] = new Track(propName);

                let initialValue;
                const additiveTrack = this._getAdditiveTrack(propName);
                if (additiveTrack) {
                    const addtiveTrackKfs = additiveTrack.keyframes;
                    const lastFinalKf = addtiveTrackKfs[addtiveTrackKfs.length - 1];
                    // Use the last state of additived animator.
                    initialValue = lastFinalKf && lastFinalKf.value;
                    if (additiveTrack.valType === VALUE_TYPE_COLOR && initialValue) {
                        // Convert to rgba string
                        initialValue = rgba2String(initialValue as number[]);
                    }
                }
                else {
                    initialValue = (this._target as any)[propName];
                }
                // Invalid value
                if (initialValue == null) {
                    // zrLog('Invalid property ' + propName);
                    continue;
                }
                // If time is <= 0
                //  Then props is given initialize value
                //  Note: initial percent can be negative, which means the initial value is before the animation start.
                // Else
                //  Initialize value from current prop value
                if (time > 0) {
                    track.addKeyframe(0, cloneValue(initialValue), easing);
                }

                this._trackKeys.push(propName);
            }
            track.addKeyframe(time, cloneValue(props[propName]), easing);
        }
        this._maxTime = Math.max(this._maxTime, time);
        return this;
    }

    pause() {
        this._clip.pause();
        this._paused = true;
    }

    resume() {
        this._clip.resume();
        this._paused = false;
    }

    isPaused(): boolean {
        return !!this._paused;
    }

    /**
     * Set duration of animator.
     * Will run this duration regardless the track max time or if trackes exits.
     * @param duration
     * @returns
     */
    duration(duration: number) {
        this._maxTime = duration;
        this._force = true;
        return this;
    }

    private _doneCallback() {
        this._setTracksFinished();
        // Clear clip
        this._clip = null;

        const doneList = this._doneCbs;
        if (doneList) {
            const len = doneList.length;
            for (let i = 0; i < len; i++) {
                doneList[i].call(this);
            }
        }
    }
    private _abortedCallback() {
        this._setTracksFinished();

        const animation = this.animation;
        const abortedList = this._abortedCbs;

        if (animation) {
            animation.removeClip(this._clip);
        }
        this._clip = null;

        if (abortedList) {
            for (let i = 0; i < abortedList.length; i++) {
                abortedList[i].call(this);
            }
        }
    }
    private _setTracksFinished() {
        const tracks = this._tracks;
        const tracksKeys = this._trackKeys;
        for (let i = 0; i < tracksKeys.length; i++) {
            tracks[tracksKeys[i]].setFinished();
        }
    }

    private _getAdditiveTrack(trackName: string): Track {
        let additiveTrack;
        const additiveAnimators = this._additiveAnimators;
        if (additiveAnimators) {
            for (let i = 0; i < additiveAnimators.length; i++) {
                const track = additiveAnimators[i].getTrack(trackName);
                if (track) {
                    // Use the track of latest animator.
                    additiveTrack = track;
                }
            }
        }
        return additiveTrack;
    }

    /**
     * Start the animation
     * @param easing
     * @return
     */
    start(easing?: AnimationEasing) {
        if (this._started > 0) {
            return;
        }
        this._started = 1;

        const self = this;

        const tracks: Track[] = [];
        const maxTime = this._maxTime || 0;
        for (let i = 0; i < this._trackKeys.length; i++) {
            const propName = this._trackKeys[i];
            const track = this._tracks[propName];
            const additiveTrack = this._getAdditiveTrack(propName);
            const kfs = track.keyframes;
            const kfsNum = kfs.length;
            track.prepare(maxTime, additiveTrack);
            if (track.needsAnimate()) {
                // Set value directly if discrete animation is not allowed.
                if (!this._allowDiscrete && track.discrete) {
                    const lastKf = kfs[kfsNum - 1];
                    // Set final value.
                    if (lastKf) {
                        // use raw value without parse.
                        (self._target as any)[track.propName] = lastKf.rawValue;
                    }
                    track.setFinished();
                }
                else {
                    tracks.push(track);
                }
            }
        }
        // Add during callback on the last clip
        if (tracks.length || this._force) {
            const clip = new Clip({
                life: maxTime,
                loop: this._loop,
                delay: this._delay || 0,
                onframe(percent: number) {
                    self._started = 2;
                    // Remove additived animator if it's finished.
                    // For the purpose of memory effeciency.
                    const additiveAnimators = self._additiveAnimators;
                    if (additiveAnimators) {
                        let stillHasAdditiveAnimator = false;
                        for (let i = 0; i < additiveAnimators.length; i++) {
                            if (additiveAnimators[i]._clip) {
                                stillHasAdditiveAnimator = true;
                                break;
                            }
                        }
                        if (!stillHasAdditiveAnimator) {
                            self._additiveAnimators = null;
                        }
                    }

                    for (let i = 0; i < tracks.length; i++) {
                        // NOTE: don't cache target outside.
                        // Because target may be changed.
                        tracks[i].step(self._target, percent);
                    }

                    const onframeList = self._onframeCbs;
                    if (onframeList) {
                        for (let i = 0; i < onframeList.length; i++) {
                            onframeList[i](self._target, percent);
                        }
                    }
                },
                ondestroy() {
                    self._doneCallback();
                }
            });
            this._clip = clip;

            if (this.animation) {
                this.animation.addClip(clip);
            }

            if (easing) {
                clip.setEasing(easing);
            }
        }
        else {
            // This optimization will help the case that in the upper application
            // the view may be refreshed frequently, where animation will be
            // called repeatly but nothing changed.
            this._doneCallback();
        }

        return this;
    }
    /**
     * Stop animation
     * @param {boolean} forwardToLast If move to last frame before stop
     */
    stop(forwardToLast?: boolean) {
        if (!this._clip) {
            return;
        }
        const clip = this._clip;
        if (forwardToLast) {
            // Move to last frame before stop
            clip.onframe(1);
        }

        this._abortedCallback();
    }
    /**
     * Set when animation delay starts
     * @param time 单位ms
     */
    delay(time: number) {
        this._delay = time;
        return this;
    }
    /**
     * 添加动画每一帧的回调函数
     * @param callback
     */
    during(cb: OnframeCallback<T>) {
        if (cb) {
            if (!this._onframeCbs) {
                this._onframeCbs = [];
            }
            this._onframeCbs.push(cb);
        }
        return this;
    }
    /**
     * Add callback for animation end
     * @param cb
     */
    done(cb: DoneCallback) {
        if (cb) {
            if (!this._doneCbs) {
                this._doneCbs = [];
            }
            this._doneCbs.push(cb);
        }
        return this;
    }

    aborted(cb: AbortCallback) {
        if (cb) {
            if (!this._abortedCbs) {
                this._abortedCbs = [];
            }
            this._abortedCbs.push(cb);
        }
        return this;
    }

    getClip() {
        return this._clip;
    }

    getTrack(propName: string) {
        return this._tracks[propName];
    }

    getTracks() {
        return map(this._trackKeys, key => this._tracks[key]);
    }

    /**
     * Return true if animator is not available anymore.
     */
    stopTracks(propNames: string[], forwardToLast?: boolean): boolean {
        if (!propNames.length || !this._clip) {
            return true;
        }
        const tracks = this._tracks;
        const tracksKeys = this._trackKeys;

        for (let i = 0; i < propNames.length; i++) {
            const track = tracks[propNames[i]];
            if (track && !track.isFinished()) {
                if (forwardToLast) {
                    track.step(this._target, 1);
                }
                // If the track has not been run for at least one frame.
                // The property may be stayed at the final state. when setToFinal is set true.
                // For example:
                // Animate x from 0 to 100, then animate to 150 immediately.
                // We want the x is translated from 0 to 150, not 100 to 150.
                else if (this._started === 1) {
                    track.step(this._target, 0);
                }
                // Set track to finished
                track.setFinished();
            }
        }
        let allAborted = true;
        for (let i = 0; i < tracksKeys.length; i++) {
            if (!tracks[tracksKeys[i]].isFinished()) {
                allAborted = false;
                break;
            }
        }
        // Remove clip if all tracks has been aborted.
        if (allAborted) {
            this._abortedCallback();
        }

        return allAborted;
    }

    /**
     * Save values of final state to target.
     * It is mainly used in state mangement. When state is switching during animation.
     * We need to save final state of animation to the normal state. Not interpolated value.
     *
     * @param target
     * @param trackKeys
     * @param firstOrLast If save first frame or last frame
     */
    saveTo(
        target: T,
        trackKeys?: readonly string[],
        firstOrLast?: boolean
    ) {
        if (!target) {  // DO nothing if target is not given.
            return;
        }

        trackKeys = trackKeys || this._trackKeys;

        for (let i = 0; i < trackKeys.length; i++) {
            const propName = trackKeys[i];
            const track = this._tracks[propName];
            if (!track || track.isFinished()) {   // Ignore finished track.
                continue;
            }
            const kfs = track.keyframes;
            const kf = kfs[firstOrLast ? 0 : kfs.length - 1];
            if (kf) {
                // TODO CLONE?
                // Use raw value without parse.
                (target as any)[propName] = cloneValue(kf.rawValue as any);
            }
        }
    }

    // Change final value after animator has been started.
    // NOTE: Be careful to use it.
    __changeFinalValue(finalProps: Dictionary<any>, trackKeys?: readonly string[]) {
        trackKeys = trackKeys || keys(finalProps);

        for (let i = 0; i < trackKeys.length; i++) {
            const propName = trackKeys[i];

            const track = this._tracks[propName];
            if (!track) {
                continue;
            }

            const kfs = track.keyframes;
            if (kfs.length > 1) {
                // Remove the original last kf and add again.
                const lastKf = kfs.pop();

                track.addKeyframe(lastKf.time, finalProps[propName]);
                // Prepare again.
                track.prepare(this._maxTime, track.getAdditiveTrack());
            }
        }
    }
}

export type AnimatorTrack = Track;