packages/scenejs/src/utils.ts

import {
    ROLES, MAXIMUM, FIXED, ALIAS,
    RUNNING, PLAY, ENDED, PLAY_CSS, CURRENT_TIME,
    START_ANIMATION, EASINGS, NAME_SEPARATOR
} from "./consts";
import PropertyObject from "./PropertyObject";
import Scene from "./Scene";
import SceneItem from "./SceneItem";
import {
    isArray, ANIMATION, ARRAY, OBJECT,
    PROPERTY, STRING, NUMBER, IS_WINDOW, IObject,
    $, isObject, addEvent, removeEvent, isString,
    splitComma, splitBracket, IArrayFormat,
} from "@daybrush/utils";
import { EasingType, EasingFunction, NameType, SelectorAllType, AnimateElement } from "./types";
import { toPropertyObject } from "./utils/property";
import { bezier, steps } from "./easing";
import Animator from "./Animator";
import Frame from "./Frame";

export function setAlias(name: string, alias: string[]) {
    ALIAS[name] = alias;
}
export function setRole(names: string[], isProperty?: boolean, isFixedProperty?: boolean) {
    const length = names.length;
    let roles: any = ROLES;
    let fixed: any = FIXED;

    for (let i = 0; i < length - 1; ++i) {
        !roles[names[i]] && (roles[names[i]] = {});
        roles = roles[names[i]];
        if (isFixedProperty) {
            !fixed[names[i]] && (fixed[names[i]] = {});
            fixed = fixed[names[i]];
        }
    }
    isFixedProperty && (fixed[names[length - 1]] = true);
    roles[names[length - 1]] = isProperty ? true : {};
}
export function getType(value: any) {
    const type = typeof value;

    if (type === OBJECT) {
        if (isArray(value)) {
            return ARRAY;
        } else if (isPropertyObject(value)) {
            return PROPERTY;
        }
    } else if (type === STRING || type === NUMBER) {
        return "value";
    }
    return type;
}
export function isPureObject(obj: any): obj is object {
    return isObject(obj) && obj.constructor === Object;
}
export function getNames(names: IObject<any>, stack: string[]) {
    let arr: string[][] = [];

    if (isPureObject(names)) {
        for (const name in names) {
            stack.push(name);
            arr = arr.concat(getNames(names[name], stack));
            stack.pop();
        }
    } else {
        arr.push(stack.slice());
    }
    return arr;
}
export function updateFrame(names: IObject<any>, properties: IObject<any>) {
    for (const name in properties) {
        const value = properties[name];

        if (!isPureObject(value)) {
            names[name] = true;
            continue;
        }
        if (!isObject(names[name])) {
            names[name] = {};
        }
        updateFrame(names[name], properties[name]);
    }
    return names;
}
export function toFixed(num: number) {
    return Math.round(num * MAXIMUM) / MAXIMUM;
}
export function getValueByNames(
    names: Array<string | number>,
    properties: IObject<any>, length: number = names.length) {
    let value = properties;

    for (let i = 0; i < length; ++i) {
        if (!isObject(value) || value == null) {
            return undefined;
        }
        value = value[names[i]];
    }
    return value;
}
export function isInProperties(roles: IObject<any>, args: NameType[], isLast?: boolean) {
    const length = args.length;
    let role: any = roles;

    if (length === 0) {
        return false;
    }
    for (let i = 0; i < length; ++i) {
        if (role === true) {
            return false;
        }
        role = role[args[i]];
        if (!role || (!isLast && role === true)) {
            return false;
        }
    }
    return true;
}
/**
 * @memberof Scene
 * @param - Property names
 * @param - Whether the property is the last property that cannot be an object (non-partitionable)
 */
export function isRole(args: NameType[], isLast?: boolean): boolean {
    return isInProperties(ROLES, args, isLast);
}
export function isFixed(args: NameType[]) {
    return isInProperties(FIXED, args, true);
}

export interface IterationInterface {
    currentTime: number;
    iterationCount: number;
    elapsedTime: number;
}
export function setPlayCSS(item: Animator, isActivate: boolean) {
    item.state[PLAY_CSS] = isActivate;
}
export function isPausedCSS(item: Scene | SceneItem) {
    return item.state[PLAY_CSS] && item.isPaused();
}
export function isEndedCSS(item: Scene | SceneItem) {
    return !item.isEnded() && item.state[PLAY_CSS];
}

export function makeId(selector?: boolean) {
    for (; ;) {
        const id = `${Math.floor(Math.random() * 10000000)}`;

        if (!IS_WINDOW || !selector) {
            return id;
        }
        const checkElement = $(`[data-scene-id="${id}"]`);

        if (!checkElement) {
            return id;
        }
    }
}
export function getRealId(item: Scene | SceneItem) {
    return item.getId() || item.setId(makeId(false)).getId();
}
export function toId(text: number | string) {
    return `${text}`.match(/[0-9a-zA-Z]+/g).join("");
}
export function playCSS(
    item: Scene | SceneItem, isExportCSS?: boolean,
    playClassName?: string, properties: object = {}) {
    if (!ANIMATION || item.getPlayState() === RUNNING) {
        return;
    }
    const className = playClassName || START_ANIMATION;

    if (isPausedCSS(item)) {
        item.addPlayClass(true, className, properties);
    } else {
        if (item.isEnded()) {
            item.setTime(0);
        }
        isExportCSS && item.exportCSS({ className });
        const el = item.addPlayClass(false, className, properties);

        if (!el) {
            return;
        }
        addAnimationEvent(item, el);
        setPlayCSS(item, true);
    }
    item.setPlayState(RUNNING);
}

export function addAnimationEvent(item: Animator, el: Element) {
    const state = item.state;
    const duration = item.getDuration();
    const isZeroDuration = !duration || !isFinite(duration);
    const animationend = () => {
        setPlayCSS(item, false);
        item.finish();
    };
    const animationstart = () => {
        item.trigger(PLAY);

        addEvent(el, "animationcancel", animationend);
        addEvent(el, "animationend", animationend);
        addEvent(el, "animationiteration", animationiteration);
    };
    item.once(ENDED, () => {
        removeEvent(el, "animationcancel", animationend);
        removeEvent(el, "animationend", animationend);
        removeEvent(el, "animationiteration", animationiteration);
        removeEvent(el, "animationstart", animationstart);
    });
    const animationiteration = ({ elapsedTime }: any) => {
        const currentTime = elapsedTime;
        const iterationCount = isZeroDuration ? 0 : (currentTime / duration);

        state[CURRENT_TIME] = currentTime;
        item.setIteration(iterationCount);
    };
    addEvent(el, "animationstart", animationstart);
}

export function getEasing(curveArray: string | number[] | EasingFunction): EasingType {
    let easing: EasingType;

    if (isString(curveArray)) {
        if (curveArray in EASINGS) {
            easing = EASINGS[curveArray];
        } else {
            const obj = toPropertyObject(curveArray);

            if (isString(obj)) {
                return 0;
            } else {
                if (obj.model === "cubic-bezier") {
                    curveArray = obj.value.map(v => parseFloat(v));
                    easing = bezier(curveArray[0], curveArray[1], curveArray[2], curveArray[3]);
                } else if (obj.model === "steps") {
                    easing = steps(parseFloat(obj.value[0]), obj.value[1]);
                } else {
                    return 0;
                }
            }
        }
    } else if (isArray(curveArray)) {
        easing = bezier(curveArray[0], curveArray[1], curveArray[2], curveArray[3]);
    } else {
        easing = curveArray;
    }

    return easing;
}

export function isPropertyObject(value: any): value is PropertyObject {
    if (!value) {
        return false;
    }
    const prototype = (value.constructor as typeof PropertyObject).prototype;

    return !!(prototype.clone && prototype.get && prototype.setOptions);
}

export function isScene(value: any): value is Scene {
    return value && !!(value.constructor as typeof Scene).prototype.getItem;
}
export function isSceneItem(value: any): value is SceneItem {
    return (
        value && !!(value.constructor as typeof SceneItem).prototype.getFrame
    );
}
export function isFrame(value: any): value is Frame {
    return value && !!(value.constructor as typeof Frame).prototype.toCSSText;
}
export function isAnimator(value: any): value is Animator {
    return value && !!(value.constructor as typeof Animator).prototype.getActiveDuration;
}
export function flatSceneObject(obj: IObject<any>, seperator: string): Record<string, Frame> {
    const newObj = {};

    for (const name in obj) {
        const value = obj[name];

        if (isFrame(value)) {
            newObj[name] = value;
        } else if (isObject(value)) {
            const nextObj = flatSceneObject(value, seperator);

            for (const nextName in nextObj) {
                newObj[`${name}${seperator}${nextName}`] = nextObj[nextName];
            }
        }
    }
    return newObj;
}

export function selectorAll(
    callback: (index: number, element: AnimateElement) => any,
    defaultCount = 0,
): SelectorAllType {
    const nextCallback = callback.bind({}) as SelectorAllType;
    nextCallback.defaultCount = defaultCount;

    return nextCallback;
}

export function rgbaToHexa(rgba: string) {
    const hexInfo = rgbaToHexWithOpacity(rgba);
    const hex = hexInfo.hex;

    if (!hexInfo.hex) {
        return "";
    }
    const opacityHex = Math.floor(hexInfo.opacity * 255).toString(16);

    return `${hex}${opacityHex}`;
}

export function rgbaToHexWithOpacity(rgba: string) {
    const rgbaInfo = splitBracket(rgba);

    if ((rgbaInfo.prefix || "").indexOf("rgb") !== 0) {
        return {
            hex: "",
            opacity: 1,
        };
    }

    const rgbaArr = splitComma(rgbaInfo.value);
    const rgbaNums = rgbaArr.slice(0, 3).map(num => {
        const dec = parseInt(num, 10);

        return dec.toString(16);
    });

    return {
        hex: `#${rgbaNums.join("")}`,
        opacity: rgbaArr[3] ? parseFloat(rgbaArr[3]) : 1,
    };
}

export function isArrayLike(el: any): el is IArrayFormat<any> {
    return "length" in el && el.length >= 0;
}