
import { Component, Director, director, isValid } from 'cc';
declare type GeneratorFunc = (...args: any[]) => Generator<any, any, any>
declare type GeneratorRecord = { generator: Generator<any, any, any>, gfunc?: GeneratorFunc, comp?: Component, waiter?: IWaiter };

interface IWaiter {
    isOver(dt: number): boolean;
}

class WaitForSecond implements IWaiter {
    private _endTime: number = 0;
    private _currTime: number = 0;

    constructor(endTime: number) {
        this._endTime = endTime;
    }

    isOver(dt: number) {
        this._currTime += dt;

        return this._currTime >= this._endTime;
    }
}

class WaitForFrame implements IWaiter {
    private _frameCount: number = 0;
    private _frame: number = 0;
    constructor(frameCount: number) {
        this._frameCount = frameCount;
    }

    isOver(dt: number) {
        return this._frame++ >= this._frameCount;
    }
}

export default class Coroutine {

    private static _instance: Coroutine = null;
    static get instance() {
        if (Coroutine._instance == null) {
            Coroutine._instance = new Coroutine();
        }
        return Coroutine._instance;
    }

    /**等待多少秒 */
    static createWaitForSecond(sec: number) {
        return new WaitForSecond(sec);
    }
    /**等待多少帧 */
    static createWaitForFrame(frameCount: number) {
        return new WaitForFrame(frameCount);
    }

    private _mapCoroutine: Map<any, GeneratorRecord[]> = new Map();
    private _getTimmer: () => number = null;
    private _lastTime: number = 0;

    private constructor() {
        if (typeof window?.performance?.now !== 'function') {
            this._getTimmer = () => {
                return new Date().getTime();
            }
        } else {
            this._lastTime = window.performance.now();
            this._getTimmer = () => window.performance.now();
        }
    }

    /**@summary start coroutiine
     * @param component cc component
     * @param func generator func or string
     * @param args your funcion params
    */
    start(component: Component, func: GeneratorFunc | string, ...args: any[]) {
        if (!(component instanceof Component)) {
            throw Error('component must a component');
        }
        const mapData: GeneratorRecord = {
            generator: null,
            comp: component
        };
        if (typeof func === 'string') {
            const gf: GeneratorFunc = (<any>component)[func];
            const generator = gf.call(component, ...args);
            if (typeof generator?.next === 'function') {
                mapData.generator = generator;
                mapData.gfunc = (<any>component)[func];
            } else {
                throw Error(func + ' is not a GeneratorFunction !!!');
            }
        } else {
            mapData.gfunc = func;
            mapData.generator = func.call(component, ...args);
        }
        const _processCount = this._mapCoroutine.size;
        if (mapData.generator) {
            const list = this._mapCoroutine.get(component);
            if (!list) {
                this._mapCoroutine.set(component, [mapData]);
            } else {
                list.push(mapData);
            }
        }
        if (_processCount == 0 && this._mapCoroutine.size > 0) {
            this._lastTime = this._getTimmer();
            director.on(Director.EVENT_AFTER_UPDATE, this._update, this);
        }
    }

    /**@summary stop coroutiine
     * @param component cc component
     * @param func if no input, that will stop all coroutine of component;
    */
    stop(component: Component, func?: GeneratorFunc | string) {
        if (!func) {
            this._mapCoroutine.delete(component);
        } else {
            const list = this._mapCoroutine.get(component);
            if (!list) return;
            let deleteIds: number[] = [];
            for (let i = 0; i < list.length; ++i) {
                if (!list[i]) {
                    list[i] = null;
                    deleteIds.push(i);
                }
                else if (typeof func === 'string' && list[i].gfunc === (<any>component)[func]) {
                    list[i] = null;
                    deleteIds.push(i);
                } else if (list[i].gfunc == func) {
                    list[i] = null;
                    deleteIds.push(i);
                }
            }
            if (deleteIds.length >= list.length) {
                this._mapCoroutine.delete(component);
                return;
            } else if (deleteIds.length >= 2) {
                this._mapCoroutine.set(component, list.filter((e) => !!e));
            } else if (deleteIds.length == 1) {
                list.splice(deleteIds[0], 1);
            }
        }


    }

    private _update() {
        const dt = (this._getTimmer() - this._lastTime) / 1000;
        this._mapCoroutine.forEach((gr, k) => {
            let deleteIds: number[] = [];
            let count = gr.length;
            for (let i = 0; i < count; ++i) {
                if (!(gr && isValid(gr[i]?.comp?.node, true))) {
                    deleteIds.push(i);
                    gr[i] = null;
                } else {
                    if (gr[i].waiter) {
                        if (!gr[i].waiter.isOver(dt)) {
                            continue;
                        }
                    }
                    const it = gr[i].generator.next();
                    if (it.value instanceof WaitForSecond) {
                        if (!it.value.isOver(dt)) {
                            gr[i].waiter = it.value;
                            continue;
                        }
                    } else if (it.value instanceof WaitForFrame) {
                        gr[i].waiter = it.value;
                        if (!it.value.isOver(dt)) {
                            gr[i].waiter = it.value;
                            continue;
                        }
                    }
                    if (it.done) {
                        deleteIds.push(i);
                        gr[i] = null;
                    }
                }
            }
            if (deleteIds.length >= gr.length) {
                this._mapCoroutine.delete(k);
                return;
            } else if (deleteIds.length >= 2) {
                this._mapCoroutine.set(k, gr.filter((e) => !!e));
            } else if (deleteIds.length == 1) {
                gr.splice(deleteIds[0], 1);
            }
        });
        this._lastTime = this._getTimmer();
        this._mapCoroutine.size == 0 && director.off(Director.EVENT_AFTER_UPDATE, this._update, this);
    }
}

