/**
 * 枚举类型
 * 注意1：子类声明后务必确保init()被调用一次，方可正常使用
 * 注意2：由于枚举常量作为对象可能被代理，所以枚举常量的比较不要使用===操作符，而应该用equals()方法
 */
import * as meta from '../api/meta.ts';

const DEFAULT_KEYS = ['ordinal', 'name', 'caption', 'scenes'];

export default abstract class Enum {

    /**
     * 枚举类型名。为避免压缩混淆后类型名丢失而设立，由子类覆写
     */
    static type = 'Enum';

    ordinal = 0;
    name = '';
    caption = '';
    scenes: Record<string, string> = {};

    /**
     * @param caption 显示名称
     * @param {Object|Array} [scenes] 场景集
     */
    protected constructor(caption: string, scenes?: string[] | Record<string, string>) {
        this.caption = caption;
        if (scenes) {
            if (Array.isArray(scenes)) {
                for (let scene of scenes) {
                    this.scenes[scene] = this.caption;
                }
            } else if (typeof scenes === 'object') {
                Object.assign(this.scenes, scenes);
            }
        }
    }

    static init(metaApi = meta): void {
        let sceneNameSet = new Set<string>();
        this.forEach((constant: Enum, ordinal: number, name: string) => {
            constant.ordinal = ordinal;
            constant.name = name;
            Object.keys(constant.scenes).forEach(sceneName => sceneNameSet.add(sceneName));
        });
        metaApi.addEnumType({
            name: this.type,
            items: this.getItems(),
        });
        for (let sceneName of sceneNameSet) {
            metaApi.addEnumType({
                name: this.type,
                subname: sceneName,
                items: this.getItems(sceneName),
            });
        }
    }

    static forEach(consumer: (constant: Enum, ordinal: number, name: string) => void): void {
        let names = Object.keys(this);
        let ordinal = 0;
        for (let name of names) {
            const constant = this[name];
            if (constant.constructor === this) {
                consumer(constant, ordinal++, name);
            }
        }
    }

    /**
     * @param {?String} [sceneName] 场景名称
     */
    static values(sceneName?: string) {
        let values: Enum[] = [];
        this.forEach((constant: Enum) => {
            if (!sceneName || constant.scenes[sceneName]) {
                values.push(constant);
            }
        });
        return values;
    }

    static valueOf(name: string): Enum | undefined {
        let result: Enum | undefined = undefined;
        this.forEach((constant: Enum) => {
            if (constant.name === name) {
                result = constant;
            }
        });
        return result;
    }

    /**
     * 校验指定常量合法性
     * @param {String|Enum} constant 常量或常量名
     * @param {?String} [sceneName] 场景名称
     * @return {Enum} 常量
     */
    static validate(constant: string | Enum, sceneName?: string): Enum | undefined {
        let result: Enum | undefined = undefined;
        if (constant) {
            if (typeof constant === 'string') {
                result = this.valueOf(constant);
            } else if (constant.constructor === this) {
                result = constant as Enum;
            } else if (typeof constant.name === 'string') {
                result = this.validate(constant.name);
            }
            if (result && sceneName) {
                if (!result.scenes[sceneName]) {
                    result = undefined;
                }
            }
        }
        if (!result) {
            let message = `无效的 ${this.name} 常量: `;
            if (constant && typeof constant['name'] === 'string') {
                message += constant['name'];
            } else {
                message += constant;
            }
            if (sceneName) {
                message += `，于场景 ${sceneName} 中`;
            }
            throw new Error(message);
        }
        return result;
    }

    /**
     * @param {?String} [sceneName] 场景名称
     */
    static getItems(sceneName?: string): meta.EnumItem[] {
        let items = [];
        this.forEach((constant: Enum) => {
            let caption = constant.caption;
            if (sceneName) {
                let sceneCaption = constant.scenes[sceneName];
                if (sceneCaption) {
                    caption = sceneCaption;
                } else {
                    return;
                }
            }
            let item: meta.EnumItem = {
                key: constant.name,
                caption: caption,
            };
            Object.keys(constant).forEach(key => {
                if (!DEFAULT_KEYS.includes(key)) {
                    item[key] = constant[key];
                }
            });
            items.push(item);
        });
        return items;
    }

    toString(): string {
        return this.name;
    }

    equals(other: Enum): boolean {
        return !!(other && other.constructor === this.constructor && other.name === this.name);
    }

    supports(sceneName: string): boolean {
        return !!this.scenes[sceneName];
    }

    localeCompare(other: Enum): number {
        if (this.ordinal < other.ordinal) {
            return -1;
        } else if (this.ordinal > other.ordinal) {
            return 1;
        } else {
            return 0;
        }
    }
}
