import { KeyCode } from '@motajs/client-base';
import {
    deleteWith,
    generateBinary,
    keycode,
    spliceBy
} from '@motajs/legacy-common';
import { EventEmitter } from 'eventemitter3';
import { isNil } from 'lodash-es';

// todo: 按下加速节流触发

interface HotkeyEvent {
    set: [id: string, key: KeyCode, assist: number];
    emit: [key: KeyCode, assist: number, type: KeyEmitType];
    press: [key: KeyCode];
    release: [key: KeyCode];
}

type KeyEmitType =
    | 'up'
    | 'down'
    | 'down-repeat'
    | 'down-throttle'
    // todo: | 'down-accelerate'
    | 'down-timeout';

type KeyEventType = 'up' | 'down';

interface AssistHoykey {
    ctrl: boolean;
    shift: boolean;
    alt: boolean;
}

interface RegisterHotkeyData extends Partial<AssistHoykey> {
    id: string;
    name: string;
    defaults: KeyCode;
}

export interface HotkeyData extends Required<RegisterHotkeyData> {
    key: KeyCode;
    emits: Map<symbol, HotkeyEmitData>;
    group?: string;
}

/**
 * @param id 此处的id包含数字后缀
 * @returns 返回 `@void` 时，表示此次触发没有包含副作用，不会致使 `preventDefault` 被执行
 */
type HotkeyFunc = (
    id: string,
    code: KeyCode,
    assist: number,
    ev: KeyboardEvent
) => void | '@void';

interface HotkeyEmitData {
    func: HotkeyFunc;
    onUp?: HotkeyFunc;
    config?: HotkeyEmitConfig;
}

export interface HotkeyJSON {
    key: KeyCode;
    assist: number;
}

export interface HotkeyEmitConfig {
    type: KeyEmitType;
    throttle?: number;
    accelerate?: number;
    timeout?: number;
}

export class Hotkey extends EventEmitter<HotkeyEvent> {
    static list: Hotkey[];

    id: string;
    name: string;
    data: Record<string, HotkeyData> = {};
    keyMap: Map<KeyCode, HotkeyData[]> = new Map();
    /** id to name */
    groupName: Record<string, string> = {
        none: '未分类按键'
    };
    /** id to keys */
    groups: Record<string, string[]> = {
        none: []
    };
    enabled: boolean = false;
    conditionMap: Map<symbol, () => boolean> = new Map();

    /** 当前作用域 */
    scope: symbol = Symbol();
    private scopeStack: symbol[] = [];
    private grouping: string = 'none';

    /** 当前正在按下的按键 */
    private pressed: Set<KeyCode> = new Set();
    /** 有哪些单次按下的按键已经触发 */
    private downEmitted: Set<KeyCode> = new Set();
    /** 按键按下时的时间 */
    private pressTime: Map<KeyCode, number> = new Map();
    /** 按键节流时间 */
    private throttleMap: Map<KeyCode, number> = new Map();

    constructor(id: string, name: string) {
        super();
        this.id = id;
        this.name = name;
    }

    /**
     * 注册一个按键，id可以包含数字后缀，可以显示为同一个按键操作拥有多个按键可以触发
     * @param data 要注册的按键信息
     */
    register(data: RegisterHotkeyData): this {
        const d: HotkeyData = {
            ...data,
            ctrl: !!data.ctrl,
            shift: !!data.shift,
            alt: !!data.alt,
            key: data.defaults,
            emits: new Map(),
            group: this.grouping
        };
        this.ensureMap(d.key);
        if (d.id in this.data) {
            // console.warn(`已存在id为${d.id}的按键，已将其覆盖`);
        }
        this.data[d.id] = d;
        const arr = this.keyMap.get(d.key)!;
        arr.push(d);
        this.groups[this.grouping].push(d.id);
        return this;
    }

    /**
     * 实现一个按键按下时的操作
     * @param id 要实现的按键id，可以不包含数字后缀
     * @param func 按键按下时执行的函数
     * @param config 按键的配置信息
     */
    realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig): this {
        const toSet = Object.values(this.data).filter(v => {
            const split = v.id.split('_');
            const last = !isNaN(Number(split.at(-1)));
            return v.id === id || (last && split.slice(0, -1).join('_') === id);
        });
        if (toSet.length === 0) {
            throw new Error(`Realize nonexistent key '${id}'.`);
        }
        for (const key of toSet) {
            const data = key.emits.get(this.scope);
            if (!data) {
                key.emits.set(this.scope, { func, config });
            } else {
                // 同时注册抬起和按下
                const dataType = data.config?.type ?? 'up';
                const configType = config?.type ?? 'up';
                if (dataType === 'up' && configType !== 'up') {
                    data.onUp = func;
                    data.config ??= { type: configType };
                    data.config.type = configType;
                } else if (dataType !== 'up' && configType === 'up') {
                    data.onUp = func;
                } else {
                    data.config = config;
                    data.func = func;
                }
            }
        }
        return this;
    }

    /**
     * 使用一个symbol作为当前作用域，之后调用{@link realize}所实现的按键功能将会添加至此作用域
     * @param symbol 当前作用域的symbol
     */
    use(symbol: symbol): void {
        if (symbol === this.scope) return;
        this.dispose(symbol);
        this.scopeStack.push(symbol);
        this.scope = symbol;
        this.conditionMap.set(symbol, () => true);
    }

    /**
     * 释放一个作用域，释放后作用域将退回至删除的作用域的上一级
     * @param symbol 要释放的作用域的symbol
     */
    dispose(symbol: symbol = this.scopeStack.at(-1) ?? Symbol()): void {
        const disposed = spliceBy(this.scopeStack, symbol);
        disposed.forEach(v => {
            for (const key of Object.values(this.data)) {
                key.emits.delete(v);
            }
        });
        this.scope = this.scopeStack.at(-1) ?? Symbol();
    }

    /**
     * 设置一个按键信息
     * @param id 要设置的按键的id
     * @param key 要设置成的按键
     * @param assist 辅助按键，三位二进制数据，从低到高依次为`ctrl` `shift` `alt`
     * @param emit 是否触发set事件，当且仅当从fromJSON方法调用时为false
     */
    set(id: string, key: KeyCode, assist: number, emit: boolean = true): void {
        const { ctrl, shift, alt } = unwarpBinary(assist);
        const data = this.data[id];
        if (!data) return;
        const before = this.keyMap.get(data?.key ?? KeyCode.Unknown)! ?? [];
        deleteWith(before, data);
        this.ensureMap(key);
        const after = this.keyMap.get(key)!;
        after.push(data);
        data.key = key;
        data.ctrl = ctrl;
        data.shift = shift;
        data.alt = alt;
        if (emit) this.emit('set', id, key, assist);
    }

    /**
     * 判断指定按键和辅助按键是否可能会触发给定 id 的按键操作
     * @param key 按键
     * @param assist 辅助按键
     * @param id 注册的按键操作 id
     */
    willEmit(key: KeyCode, assist: number, id: string) {
        const emittable = this.keyMap.get(key);
        if (!emittable) return false;
        const { ctrl, shift, alt } = unwarpBinary(assist);
        return emittable.some(v => {
            if (
                v.id === id &&
                v.ctrl === ctrl &&
                v.shift === shift &&
                v.alt === alt
            ) {
                return true;
            } else {
                return false;
            }
        });
    }

    /**
     * 触发一个按键
     * @param key 要触发的按键
     * @param assist 辅助按键，三位二进制数据，从低到高依次为`ctrl` `shift` `alt`
     * @returns 是否有按键被触发
     */
    emitKey(
        key: KeyCode,
        assist: number,
        type: KeyEventType,
        ev: KeyboardEvent
    ): boolean {
        // 检查全局启用情况
        if (!this.enabled) return false;
        const when = this.conditionMap.get(this.scope);
        if (type === 'up') this.checkPressEnd(key);
        else if (type === 'down') this.checkPress(key);
        if (when && !when()) return false;
        const toEmit = this.keyMap.get(key);
        if (!toEmit) return false;

        // 进行按键初始处理
        const { ctrl, shift, alt } = unwarpBinary(assist);

        // 真正开始触发按键
        let emitted = false;

        toEmit.forEach(v => {
            if (ctrl === v.ctrl && shift === v.shift && alt === v.alt) {
                const data = v.emits.get(this.scope);
                if (!data) return;

                if (type === 'up' && data.onUp) {
                    data.onUp(v.id, key, assist, ev);
                    emitted = true;
                    return;
                }
                if (!this.canEmit(v.id, key, type, data)) return;
                const res = data.func(v.id, key, assist, ev);
                if (res !== '@void') emitted = true;
            }
        });
        this.emit('emit', key, assist, type);

        if (type === 'down') {
            this.downEmitted.add(key);
        }

        return emitted;
    }

    /**
     * 检查按键按下情况，如果没有按下则添加
     * @param keyCode 按下的按键
     */
    private checkPress(keyCode: KeyCode): void {
        if (this.pressed.has(keyCode)) return;
        this.pressed.add(keyCode);
        this.pressTime.set(keyCode, Date.now());
        this.emit('press', keyCode);
    }

    /**
     * 当按键松开时，移除相应的按下配置
     * @param keyCode 松开的按键
     */
    private checkPressEnd(keyCode: KeyCode): void {
        if (!this.pressed.has(keyCode)) return;
        this.pressed.delete(keyCode);
        this.pressTime.delete(keyCode);
        this.downEmitted.delete(keyCode);
        this.emit('release', keyCode);
    }

    /**
     * 检查一个按键是否可以被触发
     * @param id 触发的按键id
     * @param keyCode 按键码
     */
    private canEmit(
        _id: string,
        keyCode: KeyCode,
        type: KeyEventType,
        data: HotkeyEmitData
    ) {
        const config = data?.config;
        // 这时默认为抬起触发，始终可触发
        if (type === 'up') {
            if (!config || config.type === 'up') return true;
            else return false;
        }
        if (!config) return false;
        // 按下单次触发
        if (config.type === 'down') return !this.downEmitted.has(keyCode);
        // 按下重复触发
        if (config.type === 'down-repeat') return true;
        if (config.type === 'down-timeout') {
            const time = this.pressTime.get(keyCode);
            if (isNil(time) || isNil(config.timeout)) return false;
            return Date.now() - time >= config.timeout;
        }
        if (config.type === 'down-throttle') {
            const thorttleTime = this.throttleMap.get(keyCode);
            if (isNil(config.throttle)) return false;
            if (isNil(thorttleTime)) {
                this.throttleMap.set(keyCode, Date.now());
                return true;
            }
            if (Date.now() - thorttleTime >= config.throttle) {
                this.throttleMap.set(keyCode, Date.now());
                return true;
            }
            return false;
        }
        return false;
    }

    /**
     * 按键分组，执行后 register 的按键将会加入此分组
     * @param id 组的id
     * @param name 组的名称
     */
    group(id: string, name: string, keys?: RegisterHotkeyData[]): this {
        this.grouping = id;
        this.groupName[id] = name;
        this.groups[id] ??= [];
        keys?.forEach(v => this.register(v));
        return this;
    }

    /**
     * 启用这个按键控制器
     */
    enable(): void {
        this.enabled = true;
    }

    /**
     * 禁用这个按键控制器
     */
    disable(): void {
        this.enabled = false;
    }

    /**
     * 在当前作用域下，满足什么条件时触发按键
     * @param fn 条件函数
     */
    when(fn: () => boolean): this {
        this.conditionMap.set(this.scope, fn);
        return this;
    }

    toJSON(): string {
        const res: Record<string, HotkeyJSON> = {};
        for (const [key, data] of Object.entries(this.data)) {
            res[key] = {
                key: data.key,
                assist: generateBinary([data.ctrl, data.shift, data.alt])
            };
        }
        return JSON.stringify(res);
    }

    fromJSON(data: string): void {
        const json: Record<string, HotkeyJSON> = JSON.parse(data);
        for (const [key, data] of Object.entries(json)) {
            this.set(key, data.key, data.assist, false);
        }
    }

    private ensureMap(key: KeyCode) {
        if (!this.keyMap.has(key)) {
            this.keyMap.set(key, []);
        }
    }

    /**
     * 根据id获取hotkey实例
     * @param id 要获取的hotkey实例的id
     */
    static get(id: string): Hotkey | undefined {
        return this.list.find(v => v.id === id);
    }
}

export function unwarpBinary(bin: number): AssistHoykey {
    return {
        ctrl: !!(bin & (1 << 0)),
        shift: !!(bin & (1 << 1)),
        alt: !!(bin & (1 << 2))
    };
}

export function checkAssist(bin: number, key: KeyCode): boolean {
    return (
        isAssist(key) &&
        !!(
            (1 << (key === KeyCode.Ctrl ? 0 : key === KeyCode.Shift ? 1 : 2)) &
            bin
        )
    );
}

export function isAssist(
    key: KeyCode
): key is KeyCode.Ctrl | KeyCode.Shift | KeyCode.Alt {
    return key === KeyCode.Ctrl || key === KeyCode.Shift || key === KeyCode.Alt;
}

export const gameKey = new Hotkey('gameKey', '游戏按键');

// ----- Listening
document.addEventListener('keyup', e => {
    const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
    const code = keycode(e.keyCode);
    if (
        code === KeyCode.Alt ||
        code === KeyCode.Shift ||
        code === KeyCode.Ctrl
    ) {
        e.preventDefault();
    }
    if (gameKey.emitKey(code, assist, 'up', e)) {
        e.preventDefault();
        if (core.status.holdingKeys) {
            deleteWith(core.status.holdingKeys, e.keyCode);
        }
    } else {
        // polyfill样板
        if (main.dom.inputDiv.style.display === 'block') {
            if (e.keyCode === 13) {
                setTimeout(function () {
                    main.dom.inputYes.click();
                }, 50);
            } else if (e.keyCode === 27) {
                setTimeout(function () {
                    main.dom.inputNo.click();
                }, 50);
            }
            return;
        }
        if (
            core &&
            core.isPlaying &&
            core.status &&
            (core.isPlaying() || core.status.lockControl)
        )
            core.onkeyUp(e);
    }
});
document.addEventListener('keydown', e => {
    const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
    const code = keycode(e.keyCode);
    if (gameKey.emitKey(code, assist, 'down', e)) {
        e.preventDefault();
    }
});
