const INTENTIONAL_ABORT_VALUES = ["Outdated", "UserCancelled"] as const;
export type IntentionalAbort = (typeof INTENTIONAL_ABORT_VALUES)[number];

export function wasIntentionalAbort(value: string): value is IntentionalAbort {
    return INTENTIONAL_ABORT_VALUES.includes(value as IntentionalAbort);
}

export class Aborted extends Error {
    reason: string;
    constructor(message?: string) {
        super(message || "The operation was aborted.");
        this.name = "Aborted";
        this.reason = message || "Aborted";
    }
}

export interface Abortable {
    abort: (reason?: string) => void;
}

interface ExecutorFunction<T> {
    (resolve: (value: PromiseLike<T> | T) => void, reject: (reason: any) => void): void;
}

interface AbortableExecutorFunction<T> {
    (resolve: (value: PromiseLike<T> | T) => void, reject: (reason: any) => void, abortSignal: AbortSignal): void;
}

export class AbortablePromise<T> extends Promise<T> implements Abortable {
    public abort: Abortable["abort"];
    public reason: string = "Aborted";

    constructor(executor: AbortableExecutorFunction<T>, controller?: AbortController) {
        const abortController = controller || new AbortController();
        const abortSignal = abortController.signal;

        const normalExecutor: ExecutorFunction<T> = (resolve, reject) => {
            abortSignal.addEventListener("abort", () => {
                reject(new Aborted(this.reason));
            });

            executor(resolve, reject, abortSignal);
        };

        super(normalExecutor);

        this.abort = reason => {
            this.reason = reason || "Aborted";
            abortController.abort();
        };
    }

    catch<TResult = never>(
        onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
    ): AbortablePromise<T | TResult> {
        const originalPromise = super.catch(onrejected);
        return AbortablePromise.from(originalPromise);
    }

    then<TResult1 = T, TResult2 = never>(
        onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
        onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
    ): AbortablePromise<TResult1 | TResult2> {
        const originalPromise = super.then(onfulfilled, onrejected);
        return AbortablePromise.from(originalPromise);
    }

    finally(onfinally?: (() => void) | undefined | null): AbortablePromise<T> {
        const originalPromise = super.finally(onfinally);
        return AbortablePromise.from(originalPromise);
    }

    static from = <T>(promise: Promise<T>, abortController?: AbortController): AbortablePromise<T> => {
        if (promise instanceof AbortablePromise) {
            return promise;
        }

        return new AbortablePromise<T>((resolve, reject) => {
            promise.then(resolve).catch(reject);
        }, abortController);
    };
}
