import { CancellationAggregate } from "./CancellationAggregate";
import { ICancellation, IDestroyable } from "./interfaces";
import { argumentNotNull, destroyed } from "./safe";

export class Cancellation implements ICancellation {
    private _reason: any;
    private _cbs: Array<(e: any) => void> | undefined;

    constructor(action: (cancel: (e?: any) => void) => void) {
        argumentNotNull(action, "action");

        action(this._cancel.bind(this));
    }

    isSupported(): boolean {
        return true;
    }
    throwIfRequested(): void {
        if (this._reason)
            throw this._reason;
    }

    isRequested(): boolean {
        return !!this._reason;
    }

    register(cb: (e: any) => void): IDestroyable {
        argumentNotNull(cb, "cb");

        if (this._reason) {
            cb(this._reason);
            return destroyed;
        } else {
            if (!this._cbs)
                this._cbs = [cb];
            else
                this._cbs.push(cb);

            const me = this;
            return {
                destroy() {
                    me._unregister(cb);
                }
            };
        }
    }

    private _unregister(cb: any) {
        if (this._cbs) {
            const i = this._cbs.indexOf(cb);
            if (i >= 0)
                this._cbs.splice(i, 1);
        }
    }

    private _cancel(reason: any) {
        if (this._reason)
            return;

        this._reason = (reason = reason || new Error("Operation cancelled"));

        if (this._cbs) {
            this._cbs.forEach(cb => cb(reason));
            this._cbs = undefined;
        }
    }

    static readonly none: ICancellation = {
        isSupported(): boolean {
            return false;
        },

        throwIfRequested(): void {
        },

        isRequested(): boolean {
            return false;
        },

        register(_cb: (e: any) => void): IDestroyable {
            return destroyed;
        }
    };

    /**
     * Combines multiple cancellation tokens to the single aggregated token.
     * 
     * Aggregated token will be considered as signalled when some tokens are
     * signalled. The cancellation callback can be registered with the `register`
     * method, it will be fired once with the first signalled token, all other
     * tokens will be ignored.
     * 
     * The tokens which don't support cancellation are filtered out, if there are
     * no tokens left in the list the method returns `Cancellation.none`.
     * 
     * @param args The list of cancellation tokens to combine
     * @returns 
     */
    static combine(...args: ICancellation[]) {
        const tokens = args.filter(ct => ct.isSupported());
        return tokens.length > 1 ?
            new CancellationAggregate(tokens) :
            tokens.length == 1 ? tokens[0] :
                this.none;
    }
}
