packages/scenejs/src/Frame.ts

import {
    ALIAS, TIMING_FUNCTION, TRANSFORM_NAME, EASING_NAME, NAME_SEPARATOR
} from "./consts";
import { isRole, getType, isPropertyObject, getValueByNames, isFixed, getNames, getEasing, isFrame } from "./utils";
import { toPropertyObject, splitStyle, toObject } from "./utils/property";
import {
    isObject, isArray, isString, getKeys,
    ANIMATION, TRANSFORM, FILTER, PROPERTY, FUNCTION, ARRAY, OBJECT, IObject, isUndefined,
    sortOrders,
    decamelize,
    camelize,
} from "@daybrush/utils";
import { NameType, KeyValueChildren, FrameEvents } from "./types";
import OrderMap from "order-map";
import { Observe } from "@cfcs/core";
import EventEmitter from "@scena/event-emitter";

function toInnerProperties(obj: IObject<string>, orders: NameType[] = []) {
    if (!obj) {
        return "";
    }
    const arrObj = [];

    const keys = getKeys(obj);

    sortOrders(keys, orders);

    keys.forEach(name => {
        arrObj.push(`${name.replace(/\d$/g, "")}(${obj[name]})`);
    });

    return arrObj.join(" ");
}

/* eslint-disable */
function clone(target: IObject<any>, toValue = false) {
    return merge({}, target, toValue);
}
function merge(to: IObject<any>, from: IObject<any>, toValue = false) {
    for (const name in from) {
        const value = from[name];
        const type = getType(value);

        if (type === PROPERTY) {
            to[name] = toValue ? value.toValue() : value.clone();
        } else if (type === FUNCTION) {
            to[name] = toValue ? getValue([name], value) : value;
        } else if (type === ARRAY) {
            to[name] = value.slice();
        } else if (type === OBJECT) {
            if (isObject(to[name]) && !isPropertyObject(to[name])) {
                merge(to[name], value, toValue);
            } else {
                to[name] = clone(value, toValue);
            }
        } else {
            to[name] = from[name];
        }
    }
    return to;
}
/* eslint-enable */

function getPropertyName(args: NameType[]) {
    return args[0] in ALIAS ? ALIAS[args[0]] : args;
}
function getValue(names: NameType[], value: any): any {
    const type = getType(value);

    if (type === PROPERTY) {
        return value.toValue();
    } else if (type === FUNCTION) {
        if (names[0] !== TIMING_FUNCTION) {
            return getValue(names, value());
        }
    } else if (type === OBJECT) {
        return clone(value, true);
    }
    return value;
}

/**
* Animation's Frame
*/
class Frame extends EventEmitter<FrameEvents> {
    public properties: IObject<any> = {};
    public orderMap: OrderMap = new OrderMap(NAME_SEPARATOR);
    /**
     * @param - properties
     * @example
  const frame = new Scene.Frame({
      display: "none"
      transform: {
          translate: "50px",
          scale: "5, 5",
      }
  });
     */
    constructor(properties: string | Record<string, any> = {}) {
        super();
        this.properties = {};
        // this.orders = [];
        this.set(properties);
    }
    /**
      * get property value
      * @param {...Number|String|PropertyObject} args - property name or value
      * @example
      frame.get("display") // => "none", "block", ....
      frame.get("transform", "translate") // => "10px,10px"
      */
    public get(...args: NameType[]) {
        const value = this.raw(...args);

        return getValue(getPropertyName(args), value);
    }
    /**
      * get properties orders
      * @param - property names
      * @example
      frame.getOrders(["display"]) // => []
      frame.getOrders(["transform"]) // => ["translate", "scale"]
      */
    public getOrders(names: NameType[]): NameType[] | undefined {
        return this.orderMap.get(names);
    }
    /**
      * set properties orders
      * @param - property names
      * @param - orders
      * @example
      frame.getOrders(["transform"]) // => ["translate", "scale"]
      frame.setOrders(["transform"], ["scale", "tralsate"])
      */
    public setOrders(names: NameType[], orders: NameType[]): NameType[] {
        const result = this.orderMap.set(names, orders);

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

    /**
      * get property keys
      * @param - property names
      * @example
      frame.gets("display") // => []
      frame.gets("transform") // => ["translate"]
      */
    public getKeys(...args: NameType[]): string[] {
        const value = this.raw(...args);
        const keys = getType(value) === OBJECT ? getKeys(value) : [];

        sortOrders(keys, this.orderMap.get(args));
        return keys;
    }
    /**
      * get properties array
      * @param - property names
      * @example
      frame.gets("display") // => []
      frame.gets("transform") // => [{ key: "translate", value: "10px, 10px", children: [] }]
      */
    public gets(...args: NameType[]): KeyValueChildren[] {
        const values = this.get(...args);
        const keys = this.getKeys(...args);

        return keys.map(key => {
            const nextValue = values[key];
            return { key, value: nextValue, children: this.gets(...args, key) };
        });
    }

    public raw(...args: NameType[]) {
        return getValueByNames(getPropertyName(args), this.properties);
    }
    /**
      * remove property value
      * @param {...String} args - property name
      * @return {Frame} An instance itself
      * @example
      frame.remove("display")
      */
    public remove(...args: NameType[]) {
        const params = getPropertyName(args);
        const length = params.length;

        if (!length) {
            return this;
        }
        this.orderMap.remove(params);
        const value = getValueByNames(params, this.properties, length - 1);

        if (isObject(value)) {
            delete value[params[length - 1]];
        }
        this._update();
        return this;
    }
    /**
      * set property
      * @param {...Number|String|PropertyObject} args - property names or values
      * @return {Frame} An instance itself
      * @example
  // one parameter
  frame.set({
      display: "none",
      transform: {
          translate: "10px, 10px",
          scale: "1",
      },
      filter: {
          brightness: "50%",
          grayscale: "100%"
      }
  });

  // two parameters
  frame.set("transform", {
      translate: "10px, 10px",
      scale: "1",
  });

  // three parameters
  frame.set("transform", "translate", "50px");
    */
    public set(...args: any[]) {
        this._set(...args);
        this._update();
        return this;
    }
    /**
      * Gets the names of properties.
      * @return the names of properties.
      * @example
  // one parameter
  frame.set({
      display: "none",
      transform: {
          translate: "10px, 10px",
          scale: "1",
      },
  });

  // [["display"], ["transform", "translate"], ["transform", "scale"]]
  console.log(frame.getNames());
    */
    public getNames(): string[][] {
        return getNames(this.properties, []);
    }
    /**
      * check that has property.
      * @param {...String} args - property name
      * @example
      frame.has("property", "display") // => true or false
      */
    public has(...args: NameType[]) {
        const params = getPropertyName(args);
        const length = params.length;

        if (!length) {
            return false;
        }
        return !isUndefined(getValueByNames(params, this.properties, length));
    }
    /**
      * clone frame.
      * @return {Frame} An instance of clone
      * @example
      frame.clone();
      */
    public clone() {
        const frame = new Frame();

        frame.setOrderObject(this.orderMap.orderMap);
        return frame.merge(this);
    }
    /**
      * merge one frame to other frame.
      * @param - target frame.
      * @return {Frame} An instance itself
      * @example
      frame.merge(frame2);
      */
    public merge(frame: Frame) {
        const properties = this.properties;
        const frameProperties = frame.properties;

        if (frameProperties) {
            merge(properties, frameProperties);
        }
        return this;
    }
    /**
      * Specifies an css object that coverted the frame.
      * @param - If you want to return camel case name like css property or react, use the following parameter
      * @return {object} cssObject
      */
    public toCSSObject(useCamelCase?: boolean) {
        const properties = this.get();
        const cssObject: IObject<string> = {};

        for (let name in properties) {
            if (isRole([name], true)) {
                continue;
            }
            let value = properties[name];

            if (name === TIMING_FUNCTION) {
                name = TIMING_FUNCTION.replace("animation", ANIMATION);
                value = (isString(value) ? value : value[EASING_NAME]) || "initial";
            }
            if (useCamelCase) {
                name = camelize(name.replace(/^[-]+/g, ""));
            }
            cssObject[name] = value;
        }
        const transform = toInnerProperties(properties[TRANSFORM_NAME], this.orderMap.get([TRANSFORM_NAME]));
        const filter = toInnerProperties(properties.filter, this.orderMap.get([FILTER]));

        TRANSFORM && transform && (cssObject[TRANSFORM] = transform);
        FILTER && filter && (cssObject[FILTER] = filter);
        return cssObject;
    }
    /**
      * Specifies an css text that coverted the frame.
      * @return {string} cssText
      */
    public toCSSText() {
        const cssObject = this.toCSSObject();
        const cssArray = [];
        const keys = getKeys(cssObject);

        sortOrders(keys, this.orderMap.get([]));
        keys.forEach(name => {
            cssArray.push(`${decamelize(name, "-")}:${cssObject[name]};`);
        });
        return cssArray.join("");
    }
    /**
      * Specifies an css text that coverted the frame.
      * Use `toCSSText()` method.
      * @deprecated
      * @return {string} cssText
      */
    public toCSS() {
        const cssObject = this.toCSSObject();
        const cssArray = [];
        const keys = getKeys(cssObject);

        sortOrders(keys, this.orderMap.get([]));
        keys.forEach(name => {
            cssArray.push(`${name}:${cssObject[name]};`);
        });
        return cssArray.join("");
    }
    /**
      * Remove All Properties
      * @return {Frame} An instance itself
      */
    public clear() {
        this.properties = {};
        this.orderMap.clear();
        return this;
    }
    private _set(...args: any[]) {
        const self = this;
        const length = args.length;
        const params = args.slice(0, -1);
        const value = args[length - 1];
        const firstParam = params[0];

        if (length === 1 && isFrame(value)) {
            self.merge(value);
        } else if (firstParam in ALIAS) {
            self._setByPath(ALIAS[firstParam], value);
        } else if (length === 2 && isArray(firstParam)) {
            self._setByPath(firstParam, value);
        } else if (isPropertyObject(value)) {
            if (isRole(params)) {
                self._set(...params, toObject(value));
            } else {
                self._setByPath(params, value);
            }
        } else if (isArray(value)) {
            self._setByPath(params, value);
        } else if (isObject(value)) {
            if (!self.has(...params) && isRole(params)) {
                self._setByPath(params, {});
            }
            for (const name in value) {
                self._set(...params, name, value[name]);
            }
        } else if (isString(value)) {
            if (isRole(params, true)) {
                if (isFixed(params) || !isRole(params)) {
                    this._setByPath(params, value);
                } else {
                    const obj = toPropertyObject(value);

                    if (isObject(obj)) {
                        self._set(...params, obj);
                    }
                }
                return this;
            } else {
                const { styles, length: stylesLength } = splitStyle(value);

                for (const name in styles) {
                    self._set(...params, name, styles[name]);
                }
                if (stylesLength) {
                    return this;
                }
            }
            self._setByPath(params, value);
        } else {
            self._setByPath(params, value);
        }
    }
    private _setByPath(path: NameType[], value: any) {
        let properties = this.properties;
        const length = path.length;

        for (let i = 0; i < length - 1; ++i) {
            const name = path[i];

            !(name in properties) && (properties[name] = {});
            properties = properties[name];
        }
        if (!length) {
            return;
        }
        const lastParam = path[length - 1];

        this.orderMap.add(path);
        if (length === 1 && lastParam === TIMING_FUNCTION) {
            properties[lastParam] = getEasing(value);
        } else {
            properties[lastParam] = isString(value) && !isFixed(path)
                ? toPropertyObject(value, lastParam)
                : value;
        }
    }
    private _update() {
        this.emit("update");
    }
}
export default Frame;