packages/scenejs/src/SceneItem.ts

import Animator, { isDirectionReverse } from "./Animator";
import Frame from "./Frame";
import {
    toFixed,
    isFixed,
    playCSS,
    toId,
    getRealId,
    makeId,
    isPausedCSS,
    isRole,
    getValueByNames,
    isEndedCSS,
    setPlayCSS,
    getNames,
    updateFrame,
    isSceneItem,
    isArrayLike,
} from "./utils";
import { dotValue } from "./utils/dot";
import {
    START_ANIMATION,
    PREFIX, THRESHOLD,
    TIMING_FUNCTION, ALTERNATE, ALTERNATE_REVERSE, INFINITE,
    REVERSE, EASING, FILL_MODE, DIRECTION, ITERATION_COUNT,
    EASING_NAME, DELAY, PLAY_SPEED, DURATION, PAUSE_ANIMATION,
    DATA_SCENE_ID, SELECTOR, ROLES, NAME_SEPARATOR
} from "./consts";
import {
    isObject, isArray, isUndefined, decamelize,
    ANIMATION, fromCSS, addClass, removeClass, hasClass,
    KEYFRAMES, isFunction,
    IObject, $, splitComma, toArray, isString, IArrayFormat,
    dot as dotNumber,
    find,
    findIndex,
    getKeys,
    sortOrders,
} from "@daybrush/utils";
import {
    NameType, AnimateElement, AnimatorState,
    SceneItemState, SceneItemOptions, EasingType, PlayCondition, DirectionType, SceneItemEvents, SceneItemSelectorType
} from "./types";
import OrderMap from "order-map";
import styled, { InjectResult, StyledInjector } from "css-styled";
import { Ref } from "@cfcs/core";

function getNearTimeIndex(times: number[], time: number) {
    const length = times.length;

    for (let i = 0; i < length; ++i) {
        if (times[i] === time) {
            return [i, i];
        } else if (times[i] > time) {
            return [i > 0 ? i - 1 : 0, i];
        }
    }
    return [length - 1, length - 1];
}
function makeAnimationProperties(properties: object) {
    const cssArray = [];

    for (const name in properties) {
        cssArray.push(`${ANIMATION}-${decamelize(name)}:${properties[name]};`);
    }
    return cssArray.join("");
}
function addTime(times: number[], time: number) {
    const length = times.length;
    for (let i = 0; i < length; ++i) {
        if (time < times[i]) {
            times.splice(i, 0, time);
            return;
        }
    }
    times[length] = time;
}
function addEntry(entries: number[][], time: number, keytime: number) {
    const prevEntry = entries[entries.length - 1];

    (!prevEntry || prevEntry[0] !== time || prevEntry[1] !== keytime) &&
        entries.push([toFixed(time), toFixed(keytime)]);
}
export function getEntries(times: number[], states: AnimatorState[]) {
    let entries = times.map(time => ([time, time]));
    let nextEntries = [];

    states.forEach(state => {
        const iterationCount = state[ITERATION_COUNT] as number;
        const delay = state[DELAY];
        const playSpeed = state[PLAY_SPEED];
        const direction = state[DIRECTION];
        const intCount = Math.ceil(iterationCount);
        const currentDuration = entries[entries.length - 1][0];
        const length = entries.length;
        const lastTime = currentDuration * iterationCount;

        for (let i = 0; i < intCount; ++i) {
            const isReverse =
                direction === REVERSE ||
                direction === ALTERNATE && i % 2 ||
                direction === ALTERNATE_REVERSE && !(i % 2);

            for (let j = 0; j < length; ++j) {
                const entry = entries[isReverse ? length - j - 1 : j];
                const time = entry[1];
                const currentTime = currentDuration * i + (isReverse ? currentDuration - entry[0] : entry[0]);
                const prevEntry = entries[isReverse ? length - j : j - 1];

                if (currentTime > lastTime) {
                    if (j !== 0) {
                        const prevTime = currentDuration * i +
                            (isReverse ? currentDuration - prevEntry[0] : prevEntry[0]);
                        const divideTime = dotNumber(prevEntry[1], time, lastTime - prevTime, currentTime - lastTime);

                        addEntry(nextEntries, (delay + currentDuration * iterationCount) / playSpeed, divideTime);
                    }
                    break;
                } else if (
                    currentTime === lastTime
                    && nextEntries.length
                    && nextEntries[nextEntries.length - 1][0] === lastTime + delay
                ) {
                    break;
                }
                addEntry(nextEntries, (delay + currentTime) / playSpeed, time);
            }
        }
        // delay time
        delay && nextEntries.unshift([0, nextEntries[0][1]]);

        entries = nextEntries;
        nextEntries = [];
    });

    return entries;
}
/**
* manage Frame Keyframes and play keyframes.
* @extends Animator
* @example
const item = new SceneItem({
    0: {
        display: "none",
    },
    1: {
        display: "block",
        opacity: 0,
    },
    2: {
        opacity: 1,
    }
});
*/
class SceneItem extends Animator<SceneItemOptions, SceneItemState, SceneItemEvents> {
    public times: number[] = [];
    public items: IObject<Frame> = {};
    public nameMap = new OrderMap(NAME_SEPARATOR);
    public elements: AnimateElement[] = [];
    public styled: StyledInjector;
    public styledInjector: InjectResult;
    public temp: Frame;
    private needUpdate: boolean = true;
    private target: any;
    private targetFunc: (frame: Frame) => void;
    private registeredElement: SceneItemSelectorType = false;

    /**
      * @param - properties
      * @param - options
      * @example
      const item = new SceneItem({
          0: {
              display: "none",
          },
          1: {
              display: "block",
              opacity: 0,
          },
          2: {
              opacity: 1,
          }
      });
       */
    constructor(properties?: any, options?: Partial<SceneItemOptions>) {
        super();
        this.load(properties, options);
    }
    public getDuration() {
        const times = this.times;
        const length = times.length;

        return (length === 0 ? 0 : times[length - 1]) || this.state[DURATION];
    }
    /**
      * get size of list
      * @return {Number} length of list
      */
    public size() {
        return this.times.length;
    }
    public setDuration(duration: number) {
        if (!duration) {
            return this;
        }
        const originalDuration = this.getDuration();

        if (originalDuration > 0) {
            const ratio = duration / originalDuration;
            const { times, items } = this;
            const obj: IObject<Frame> = {};

            this.times = times.map(time => {
                const time2 = toFixed(time * ratio);

                obj[time2] = items[time];

                return time2;
            });
            this.items = obj;
        } else {
            this.newFrame(duration);
        }
        return this;
    }
    public setId(id?: number | string) {
        const state = this.state;
        const elements = this.elements;
        const length = elements.length;

        state.id = id || makeId(!!length);

        if (length && !state[SELECTOR]) {
            const sceneId = toId(this.getId());

            state[SELECTOR] = `[${DATA_SCENE_ID}="${sceneId}"]`;
            elements.forEach(element => {
                element.setAttribute(DATA_SCENE_ID, sceneId);
            });
        }
        return this;
    }

    /**
      * Set properties to the sceneItem at that time
      * @param {Number} time - time
      * @param {...String|Object} [properties] - property names or values
      * @return {SceneItem} An instance itself
      * @example
  item.set(0, "a", "b") // item.getFrame(0).set("a", "b")
  console.log(item.get(0, "a")); // "b"
      */
    public set(time: any, ...args: any[]) {
        if (isSceneItem(time)) {
            return this.set(0, time);
        } else if (isArray(time)) {
            const length = time.length;

            for (let i = 0; i < length; ++i) {
                const t = length === 1 ? 0 : this.getUnitTime(`${i / (length - 1) * 100}%`);

                this.set(t, time[i]);
            }
        } else if (isObject(time)) {
            for (const t in time) {
                const value = time[t];

                splitComma(t).forEach(eachTime => {
                    const realTime = this.getUnitTime(eachTime);

                    if (isNaN(realTime)) {
                        getNames(value, [eachTime]).forEach(names => {
                            const innerValue = getValueByNames(names.slice(1), value);
                            const arr = isArray(innerValue) ?
                                innerValue : [getValueByNames(names, this.target), innerValue];
                            const length = arr.length;

                            for (let i = 0; i < length; ++i) {
                                this.newFrame(`${i / (length - 1) * 100}%`).set(...names, arr[i]);
                            }
                        });
                    } else {
                        this.set(realTime, value);
                    }
                });
            }
        } else if (!isUndefined(time)) {
            const value = args[0];

            splitComma(time + "").forEach(eachTime => {
                const realTime = this.getUnitTime(eachTime);

                if (isSceneItem(value)) {
                    const delay = value.getDelay();
                    const frames = value.toObject(!this.hasFrame(realTime + delay));
                    const duration = value.getDuration();
                    const direction = value.getDirection();
                    const isReverse = direction.indexOf("reverse") > -1;

                    for (const frameTime in frames) {
                        const nextTime = isReverse ? duration - parseFloat(frameTime) : parseFloat(frameTime);
                        this.set(realTime + nextTime, frames[frameTime]);
                    }
                } else if (args.length === 1 && isArray(value)) {
                    value.forEach((item: any) => {
                        this.set(realTime, item);
                    });
                } else {
                    const frame = this.newFrame(realTime);

                    frame.set(...args);
                }
            });
        }
        this.needUpdate = true;
        return this;
    }
    /**
      * Get properties of the sceneItem at that time
      * @param {Number} time - time
      * @param {...String|Object} args property's name or properties
      * @return {Number|String|PropertyObejct} property value
      * @example
  item.get(0, "a"); // item.getFrame(0).get("a");
  item.get(0, "transform", "translate"); // item.getFrame(0).get("transform", "translate");
      */
    public get(time: string | number, ...args: NameType[]) {
        const frame = this.getFrame(time);

        return frame && frame.get(...args);
    }
    /**
      * get properties orders
      * @param - property names
      * @example
      item.getOrders(["display"]) // => []
      item.getOrders(["transform"]) // => ["translate", "scale"]
      */
    public getOrders(names: NameType[]): NameType[] | undefined {
        this.needUpdate && this.update();

        return this.nameMap.get(names);
    }
    /**
      * set properties orders
      * @param - property names
      * @param - orders
      * @example
      item.getOrders(["transform"]) // => ["translate", "scale"]
      item.setOrders(["transform"], ["scale", "tralsate"])
      */
    public setOrders(names: NameType[], orders: NameType[]): NameType[] {
        this.needUpdate && this.update();

        const result = this.nameMap.set(names, orders);

        this.updateFrameOrders();

        return result;
    }
    /**
      * get properties order object
      * @example
      console.log(item.getOrderObject());
      */
    public getOrderObject() {
        return this.nameMap.getObject();
    }
    /**
      * set properties orders object
      * @param - properties orders object
      * @example
      item.setOrderObject({
          "": ["transform"],
          "transform": ["scale", "tralsate"],
      });
      */
    public setOrderObject(obj: IObject<NameType[]>) {
        this.nameMap.setObject(obj);

        this.updateFrameOrders();
    }
    public remove(time: string | number, ...args: any[]): this;
    /**
      * remove properties to the sceneItem at that time
      * @param {Number} time - time
      * @param {...String|Object} [properties] - property names or values
      * @return {SceneItem} An instance itself
      * @example
  item.remove(0, "a");
      */
    public remove(time: string | number, ...args: NameType[]) {
        if (args.length) {
            const frame = this.getFrame(time);

            frame && frame.remove(...args);
        } else {
            this.removeFrame(time);
        }
        this.needUpdate = true;
        return this;
    }
    /**
      * Append the item or object at the last time.
      * @param - the scene item or item object
      * @return An instance itself
      * @example
  item.append(new SceneItem({
      0: {
          opacity: 0,
      },
      1: {
          opacity: 1,
      }
  }));
  item.append({
      0: {
          opacity: 0,
      },
      1: {
          opacity: 1,
      }
  });
  item.set(item.getDuration(), {
      0: {
          opacity: 0,
      },
      1: {
          opacity: 1,
      }
  });
      */
    public append(item: SceneItem | IObject<any>) {
        if (isSceneItem(item)) {
            this.set(this.getDuration(), item);
        } else {
            this.append(new SceneItem(item));
        }
        return this;
    }
    /**
      * Push the front frames for the time and prepend the scene item or item object.
      * @param - the scene item or item object
      * @return An instance itself
      */
    public prepend(item: SceneItem | IObject<any>) {
        if (isSceneItem(item)) {
            const unshiftTime = item.getDuration() + item.getDelay();
            const firstFrame = this.getFrame(0);
            // remove first frame
            this.removeFrame(0);
            this.unshift(unshiftTime);
            this.set(0, item);
            this.set(unshiftTime + THRESHOLD, firstFrame);
        } else {
            this.prepend(new SceneItem(item));
        }
        return this;
    }
    /**
     * Push out the amount of time.
     * @param - time to push
     * @example
   item.get(0); // frame 0
   item.unshift(3);
   item.get(3) // frame 0
     */
    public unshift(time: number) {
        const { times, items } = this;
        const obj: IObject<Frame> = {};

        this.times = times.map(t => {
            const time2 = toFixed(time + t);

            obj[time2] = items[t];
            return time2;
        });
        this.items = obj;
        return this;
    }
    /**
     * Get the frames in the item in object form.
     * @return {}
     * @example
 item.toObject();
 // {0: {display: "none"}, 1: {display: "block"}}
     */
    public toObject(isStartZero = true): IObject<Frame> {
        const obj: IObject<Frame> = {};
        const delay = this.getDelay();

        this.forEach((frame: Frame, time: number) => {
            obj[(!time && !isStartZero ? THRESHOLD : 0) + delay + time] = frame.clone();
        });
        return obj;
    }
    /**
     * Specifies an element to synchronize items' keyframes.
     * @param {string} selectors - Selectors to find elements in items.
     * @return {SceneItem} An instance itself
     * @example
item.setSelector("#id.class");
     */
    public setSelector(target: SceneItemSelectorType) {
        this.setElement(target);
        return this;
    }
    /**
     * Get the elements connected to SceneItem.
     */
    public getElements(): AnimateElement[] {
        return this.elements;
    }
    /**
     * Specifies an element to synchronize item's keyframes.
     * @param - elements to synchronize item's keyframes.
     * @param - Make sure that you have peusdo.
     * @return {SceneItem} An instance itself
     * @example
item.setElement(document.querySelector("#id.class"));
item.setElement(document.querySelectorAll(".class"));
     */
    public setElements(target: SceneItemSelectorType): this {
        return this.setElement(target);
    }
    /**
     * Specifies an element to synchronize item's keyframes.
     * @param - elements to synchronize item's keyframes.
     * @param - Make sure that you have peusdo.
     * @return {SceneItem} An instance itself
     * @example
item.setElement(document.querySelector("#id.class"));
item.setElement(document.querySelectorAll(".class"));
     */
    public setElement(target: SceneItemSelectorType) {
        if (target !== true && this.registeredElement !== target) {
            this.registeredElement = target;
        }
        const state = this.state;
        const selectorTarget = this.registeredElement;
        let nextTarget: SceneItemSelectorType = target;
        let elements: AnimateElement[] = [];

        if (isFunction(selectorTarget)) {
            nextTarget = selectorTarget(this.getId(), 0);
        }
        if (!nextTarget) {
            return this;
        } else if (nextTarget === true || isString(nextTarget)) {
            const prevSelector = (isString(state[SELECTOR]) && state[SELECTOR] as string) || `${state.id}`;
            const selector = nextTarget === true ? prevSelector : nextTarget as string;
            const matches = /([\s\S]+)(:+[a-zA-Z]+)$/g.exec(selector);

            try {
                elements = toArray($(matches ? matches[1] : selector, true));
            } catch (e) {
                elements = [];
            }
            state[SELECTOR] = selector;
        } else if (isArrayLike(nextTarget)) {
            elements = toArray(nextTarget);
        } else if (nextTarget instanceof Element) {
            elements = [nextTarget];
        } else if ("current" in nextTarget || "value" in nextTarget) {
            const currentTarget = nextTarget.current || nextTarget.value;

            if (currentTarget) {
                elements = [currentTarget];
            } else {
                elements = [];
            }
        }
        if (!elements.length) {
            return this;
        }
        this.elements = elements;
        this.setId(this.getId());
        this.target = elements[0].style;
        this.targetFunc = (frame: Frame) => {
            const attributes = frame.get("attribute");

            if (attributes) {
                for (const name in attributes) {
                    elements.forEach(el => {
                        el.setAttribute(name, attributes[name]);
                    });
                }
            }
            if (frame.has("html")) {
                const html = frame.get("html");

                elements.forEach(el => {
                    el.innerHTML = html;
                });
            }
            const cssText = frame.toCSSText();

            if (state.cssText !== cssText) {
                state.cssText = cssText;

                elements.forEach(el => {
                    el.style.cssText += cssText;
                });
                return frame;
            }
        };
        return this;
    }
    public setTarget(target: any): this {
        this.target = target;
        this.targetFunc = (frame: Frame) => {
            const obj = frame.get();

            for (const name in obj) {
                target[name] = obj[name];
            }
        };
        return this;
    }
    /**
      * add css styles of items's element to the frame at that time.
      * @param - Time to synchronize and set css
      * @param - elements to synchronize item's keyframes.
      * @return {SceneItem} An instance itself
      * @example
  item.setElement(document.querySelector("#id.class"));
  item.setCSS(0, ["opacity"]);
  item.setCSS(0, ["opacity", "width", "height"]);
      */
    public setCSS(time: number, properties: string[] = []) {
        this.set(time, fromCSS(this.elements, properties));
        return this;
    }
    public setTime(time: number | string, isTick?: boolean, isParent?: boolean, parentEasing?: EasingType) {
        super.setTime(time, isTick, isParent, () => {
            const iterationTime = this.getIterationTime();
            const easing = this.getEasing() || parentEasing;
            const frame = this.getNowFrame(iterationTime, easing);
            const currentTime = this.getTime();

            this.temp = frame;
            /**
             * This event is fired when timeupdate and animate.
             * @event SceneItem#animate
             * @param {Number} param.currentTime The total time that the animator is running.
             * @param {Number} param.time The iteration time during duration that the animator is running.
             * @param {Frame} param.frame frame of that time.
             */
            this.trigger("animate", {
                frame,
                currentTime,
                time: iterationTime,
            });
            this.targetFunc && this.targetFunc(frame);
        });
        return this;
    }
    /**
      * update property names used in frames.
      * @return {SceneItem} An instance itself
      * @example
  item.update();
      */
    public update() {
        const prevNameMap = this.nameMap;
        const names = {};
        this.forEach(frame => {
            updateFrame(names, frame.properties);
        });

        const nameMap = new OrderMap(NAME_SEPARATOR);

        function pushKeys(map: IObject<any>, stack: NameType[]) {
            const keys = getKeys(map);

            sortOrders(keys, prevNameMap.get(stack));

            nameMap.set(stack, keys);
            keys.forEach(key => {
                const nextMap = map[key];
                if (isObject(nextMap)) {
                    pushKeys(nextMap, [...stack, key]);
                }
            });
        }
        pushKeys(names, []);

        this.nameMap = nameMap;

        this.forEach(frame => {
            frame.setOrderObject(nameMap.orderMap);
        });
        this.needUpdate = false;
        return this;
    }
    /**
      * Create and add a frame to the sceneItem at that time
      * @param {Number} time - frame's time
      * @return {Frame} Created frame.
      * @example
  item.newFrame(time);
      */
    public newFrame(time: string | number) {
        let frame = this.getFrame(time);

        if (frame) {
            return frame;
        }
        frame = new Frame();

        this.setFrame(time, frame);
        return frame;
    }
    /**
      * Add a frame to the sceneItem at that time
      * @param {Number} time - frame's time
      * @return {SceneItem} An instance itself
      * @example
  item.setFrame(time, frame);
      */
    public setFrame(time: string | number, frame: Frame) {
        const realTime = this.getUnitTime(time);

        this.items[realTime] = frame;
        addTime(this.times, realTime);
        this.needUpdate = true;
        return this;
    }
    public getFrame(time: number | string, ...names: any[]): Frame;
    /**
      * get sceneItem's frame at that time
      * @param {Number} time - frame's time
      * @return {Frame} sceneItem's frame at that time
      * @example
  const frame = item.getFrame(time);
      */
    public getFrame(time: number | string) {
        return this.items[this.getUnitTime(time)];
    }
    public removeFrame(time: number | string, ...names: any[]): this;
    /**
      * remove sceneItem's frame at that time
      * @param - frame's time
      * @return {SceneItem} An instance itself
      * @example
  item.removeFrame(time);
      */
    public removeFrame(time: number | string) {
        const realTime = this.getUnitTime(time);
        const items = this.items;
        const index = this.times.indexOf(realTime);

        delete items[realTime];

        // remove time
        if (index > -1) {
            this.times.splice(index, 1);
        }
        this.needUpdate = true;
        return this;
    }
    /**
      * check if the item has a frame at that time
      * @param {Number} time - frame's time
      * @return {Boolean} true: the item has a frame // false: not
      * @example
  if (item.hasFrame(10)) {
      // has
  } else {
      // not
  }
      */
    public hasFrame(time: number | string) {
        return this.getUnitTime(time) in this.items;
    }
    /**
      * Check if keyframes has propery's name
      * @param - property's time
      * @return {boolean} true: if has property, false: not
      * @example
    item.hasName(["transform", "translate"]); // true or not
      */
    public hasName(args: string[]) {
        this.needUpdate && this.update();
        return !!this.nameMap.hasName(args);
    }
    /**
      * merge frame of the previous time at the next time.
    * @param - The time of the frame to merge
    * @param - The target frame
      * @return {SceneItem} An instance itself
      * @example
  // getFrame(1) contains getFrame(0)
  item.merge(0, 1);
      */
    public mergeFrame(time: number | string, frame: Frame) {
        if (frame) {
            const toFrame = this.newFrame(time);

            toFrame.merge(frame);
        }
        return this;
    }
    /**
      * Get frame of the current time
      * @param {Number} time - the current time
      * @param {function} easing - the speed curve of an animation
      * @return {Frame} frame of the current time
      * @example
  let item = new SceneItem({
      0: {
          display: "none",
      },
      1: {
          display: "block",
          opacity: 0,
      },
      2: {
          opacity: 1,
      }
  });
  // opacity: 0.7; display:"block";
  const frame = item.getNowFrame(1.7);
      */
    public getNowFrame(time: number, parentEasing?: EasingType, isAccurate?: boolean) {
        this.needUpdate && this.update();
        const frame = new Frame();
        const [left, right] = getNearTimeIndex(this.times, time);
        let realEasing = this.getEasing() || parentEasing;
        let nameMap = this.nameMap;

        if (this.hasName([TIMING_FUNCTION])) {
            const nowEasing = this.getNowValue(time, [TIMING_FUNCTION], left, right, false, 0, true);

            isFunction(nowEasing) && (realEasing = nowEasing);
        }
        if (isAccurate) {
            const prevFrame = this.getFrame(time);
            const prevOrderMap = prevFrame.orderMap.filter([], orders => {
                return prevFrame.has(...orders);
            });

            for (const name in ROLES) {
                const orders = nameMap.get([name]);
                if (prevOrderMap.get([name]) && orders) {
                    prevOrderMap.set([name], orders);
                }
            }
            nameMap = prevOrderMap;
        }
        const names = nameMap.gets([]);

        frame.setOrderObject(nameMap.orderMap);
        names.forEach(properties => {
            const value = this.getNowValue(time, properties, left, right, isAccurate, realEasing, isFixed(properties));

            if (isUndefined(value)) {
                return;
            }
            frame.set(properties, value);
        });
        return frame;
    }
    /**
     * Get the current computed frame.
     * (If needUpdate is true, get a new computed frame, not the temp that has already been saved.)
     */
    public getCurrentFrame(needUpdate?: boolean, parentEasing?: EasingType): Frame {
        const iterationTime = this.getIterationTime();

        const frame = needUpdate || this.needUpdate || !this.temp
            ? this.getComputedFrame(iterationTime, parentEasing)
            : this.temp;

        this.temp = frame;

        return frame;
    }
    /**
     * Get the computed frame corresponding to the time.
     */
    public getComputedFrame(time: number, parentEasing?: EasingType, isAccurate?: boolean): Frame {
        return this.getNowFrame(time, parentEasing, isAccurate);
    }
    public load(properties: any = {}, options = properties.options) {
        options && this.setOptions(options);

        if (isArray(properties)) {
            this.set(properties);
        } else if (properties.keyframes) {
            this.set(properties.keyframes);
        } else {
            for (const time in properties) {
                if (time !== "options") {
                    this.set({
                        [time]: properties[time],
                    });
                }
            }
        }
        if (options && options[DURATION]) {
            this.setDuration(options[DURATION]);
        }
        return this;
    }
    /**
       * clone SceneItem.
       * @return {SceneItem} An instance of clone
       * @example
       * item.clone();
       */
    public clone() {
        const item = new SceneItem();

        item.setOptions(this.state);
        item.setOrderObject(this.nameMap.orderMap);

        this.forEach((frame: Frame, time: number) => {
            item.setFrame(time, frame.clone());
        });
        return item;
    }
    /**
       * executes a provided function once for each scene item.
       * @param - Function to execute for each element, taking three arguments
       * @return {Keyframes} An instance itself
       */
    public forEach(callback: (item: Frame, time: number, items: IObject<Frame>) => void) {
        const times = this.times;
        const items = this.items;

        times.forEach(time => {
            callback(items[time], time, items);
        });
        return this;
    }
    public setOptions(options: Partial<SceneItemOptions> = {}) {
        super.setOptions(options);
        const { id, selector, elements, element, target } = options;

        id && this.setId(id);
        if (target) {
            this.setTarget(target);
        } else if (selector && !this.state.noRegisterElement) {
            this.setSelector(selector);
        } else if (elements || element) {
            this.setElement(elements || element);
        }
        return this;
    }
    public toCSS(
        playCondition: PlayCondition = { className: START_ANIMATION },
        parentDuration = this.getDuration(), states: AnimatorState[] = []) {
        const itemState = this.state;
        const selector = itemState[SELECTOR];

        if (!selector) {
            return "";
        }
        const originalDuration = this.getDuration();
        itemState[DURATION] = originalDuration;
        states.push(itemState);

        const reversedStates = toArray(states).reverse();
        const id = toId(getRealId(this));
        const superParent = states[0];
        const infiniteIndex = findIndex(reversedStates, state => {
            return state[ITERATION_COUNT] === INFINITE || !isFinite(state[DURATION]);
        }, states.length - 1);
        const finiteStates = reversedStates.slice(0, infiniteIndex);
        const duration = parentDuration || finiteStates.reduce((prev, cur) => {
            return (cur[DELAY] + prev * (cur[ITERATION_COUNT] as number)) / cur[PLAY_SPEED];
        }, originalDuration);
        const delay = reversedStates.slice(infiniteIndex).reduce((prev, cur) => {
            return (prev + cur[DELAY]) / cur[PLAY_SPEED];
        }, 0);
        const easingName = find(reversedStates, state => (state[EASING] && state[EASING_NAME]), itemState)[EASING_NAME];
        const iterationCount = reversedStates[infiniteIndex][ITERATION_COUNT];
        const fillMode = superParent[FILL_MODE];
        const direction = reversedStates[infiniteIndex][DIRECTION];
        const cssText = makeAnimationProperties({
            fillMode,
            direction,
            iterationCount,
            delay: `${delay}s`,
            name: `${PREFIX}KEYFRAMES_${id}`,
            duration: `${duration / superParent[PLAY_SPEED]}s`,
            timingFunction: easingName,
        });
        const selectors = splitComma(selector).map(sel => {
            const matches = /([\s\S]+)(:+[a-zA-Z]+)$/g.exec(sel);

            if (matches) {
                return [matches[1], matches[2]];
            } else {
                return [sel, ""];
            }
        });
        const className = playCondition.className;
        const selectorCallback = playCondition.selector;
        const preselector = isFunction(selectorCallback) ? selectorCallback(this, selector) : selectorCallback;

        return `
    ${preselector || selectors.map(([sel, peusdo]) => `${sel}.${className}${peusdo}`)} {${cssText}}
    ${selectors.map(([sel, peusdo]) => `${sel}.${PAUSE_ANIMATION}${peusdo}`)} {${ANIMATION}-play-state: paused;}
    @${KEYFRAMES} ${PREFIX}KEYFRAMES_${id}{${this._toKeyframes(duration, finiteStates, direction)}}`;
    }
    /**
     * Export the CSS of the items to the style.
     * @param - Add a selector or className to play.
     * @return {SceneItem} An instance itself
     */
    public exportCSS(
        playCondition?: PlayCondition,
        duration?: number, options?: AnimatorState[]) {
        if (!this.elements.length) {
            return "";
        }
        const css = this.toCSS(playCondition, duration, options);
        const isParent = options && !isUndefined(options[ITERATION_COUNT]);

        if (!isParent) {
            if (this.styledInjector) {
                this.styledInjector.destroy();
                this.styledInjector = null;
            }
            this.styled = styled(css);
            this.styledInjector = this.styled.inject(this.getAnimationElement(), { original: true });
        }
        return this;
    }
    public pause() {
        super.pause();
        isPausedCSS(this) && this.pauseCSS();
        return this;
    }
    public pauseCSS() {
        this.elements.forEach(element => {
            addClass(element, PAUSE_ANIMATION);
        });
        return this;
    }
    public endCSS() {
        this.elements.forEach(element => {
            removeClass(element, PAUSE_ANIMATION);
            removeClass(element, START_ANIMATION);
        });
        setPlayCSS(this, false);
        return this;
    }
    public end() {
        isEndedCSS(this) && this.endCSS();
        super.end();
        return this;
    }
    /**
      * Play using the css animation and keyframes.
      * @param - Check if you want to export css.
      * @param [playClassName="startAnimation"] - Add a class name to play.
      * @param - The shorthand properties for six of the animation properties.
      * @see {@link https://www.w3schools.com/cssref/css3_pr_animation.asp}
      * @example
  item.playCSS();
  item.playCSS(false, "startAnimation", {
      direction: "reverse",
      fillMode: "forwards",
  });
      */
    public playCSS(isExportCSS = true, playClassName?: string, properties: object = {}) {
        playCSS(this, isExportCSS, playClassName, properties);
        return this;
    }
    public getAnimationElement(): AnimateElement {
        return this.elements[0];
    }
    public addPlayClass(isPaused: boolean, playClassName?: string, properties: object = {}) {
        const elements = this.elements;
        const length = elements.length;
        const cssText = makeAnimationProperties(properties);

        if (!length) {
            return;
        }
        if (isPaused) {
            elements.forEach(element => {
                removeClass(element, PAUSE_ANIMATION);
            });
        } else {
            elements.forEach(element => {
                element.style.cssText += cssText;

                if (hasClass(element, START_ANIMATION)) {
                    removeClass(element, START_ANIMATION);
                }
            });
            elements.forEach(element => {
                element.clientWidth;
            });
            elements.forEach(element => {
                addClass(element, START_ANIMATION);
            });
        }
        return elements[0];
    }
    /**
      * Clear All Frames
      * @return {SceneItem} An instance itself
      */
    public clear() {
        this.times = [];
        this.items = {};
        this.nameMap = new OrderMap(NAME_SEPARATOR);

        if (this.styledInjector) {
            this.styledInjector.destroy();
        }
        this.styled = null;
        this.styledInjector = null;
        this.temp = null;
        this.needUpdate = true;
        return this;
    }
    public getNowValue(
        time: number,
        properties: NameType[],
        left?: number,
        right?: number,
        isAccurate?: boolean,
        easing?: EasingType,
        usePrevValue?: boolean,
    ) {
        const times = this.times;
        const length = times.length;

        let prevTime: number;
        let nextTime: number;
        let prevFrame: Frame;
        let nextFrame: Frame;
        const isUndefinedLeft = isUndefined(left);
        const isUndefinedRight = isUndefined(right);
        if (isUndefinedLeft || isUndefinedRight) {
            const indicies = getNearTimeIndex(times, time);
            isUndefinedLeft && (left = indicies[0]);
            isUndefinedRight && (right = indicies[1]);
        }

        for (let i = left; i >= 0; --i) {
            const frame = this.getFrame(times[i]);

            if (frame.has(...properties)) {
                prevTime = times[i];
                prevFrame = frame;
                break;
            }
        }
        const prevValue = prevFrame && prevFrame.raw(...properties);

        if (isAccurate && !isRole([properties[0]])) {
            return prevTime === time ? prevValue : undefined;
        }
        if (usePrevValue) {
            return prevValue;
        }
        for (let i = right; i < length; ++i) {
            const frame = this.getFrame(times[i]);

            if (frame.has(...properties)) {
                nextTime = times[i];
                nextFrame = frame;
                break;
            }
        }
        const nextValue = nextFrame && nextFrame.raw(...properties);

        if (!prevFrame || isUndefined(prevValue)) {
            return nextValue;
        }
        if (!nextFrame || isUndefined(nextValue) || prevValue === nextValue) {
            return prevValue;
        }
        return dotValue(time, Math.max(prevTime, 0), nextTime, prevValue, nextValue, easing);
    }
    private _toKeyframes(duration: number, states: AnimatorState[], direction: DirectionType) {
        const frames: IObject<string> = {};
        const times = this.times.slice();

        if (!times.length) {
            return "";
        }
        const originalDuration = this.getDuration();
        (!this.getFrame(0)) && times.unshift(0);
        (!this.getFrame(originalDuration)) && times.push(originalDuration);
        const entries = getEntries(times, states);
        const lastEntry = entries[entries.length - 1];

        // end delay time
        lastEntry[0] < duration && addEntry(entries, duration, lastEntry[1]);
        let prevTime = -1;

        return entries.map(([time, keytime]) => {
            if (!frames[keytime]) {
                frames[keytime] =
                    (!this.hasFrame(keytime) || keytime === 0 || keytime === originalDuration ?
                        this.getNowFrame(keytime) : this.getNowFrame(keytime, 0, true)).toCSSText();
            }

            let frameTime = time / duration * 100;

            if (frameTime - prevTime < THRESHOLD) {
                frameTime += THRESHOLD;
            }
            prevTime = frameTime;
            return `${Math.min(frameTime, 100)}%{
                ${time === 0 && !isDirectionReverse(0, 1, direction) ? "" : frames[keytime]}
            }`;
        }).join("");
    }
    private updateFrameOrders() {
        const nameMap = this.nameMap.orderMap;

        this.forEach(frame => {
            frame.setOrderObject(nameMap);
        });
    }
}

export default SceneItem;