import {AbortError} from './abort_error.ts';
import {offscreenCanvasSupported} from './offscreen_canvas_supported.ts';
import {isOffscreenCanvasDistorted} from './offscreen_canvas_distorted.ts';
import {subscribe} from './util.ts';

let linkEl;

let reducedMotionQuery: MediaQueryList;
let reducedMotionOverride: boolean | undefined;

/** */
export const browser = {
    /**
     * Schedules a callback to be invoked on the next animation frame.
     * @param abortController - Controller to abort the scheduled frame.
     * @param fn - Callback to invoke with the paint start timestamp.
     * @param reject - Callback to invoke if the frame is aborted.
     * @param targetWindow - Optional window to use for requestAnimationFrame.
     *   When the map is rendered in a popup window or iframe, pass the owning
     *   window to ensure animation frames continue even when the main window
     *   is not focused.
     */
    frame(abortController: AbortController, fn: (paintStartTimestamp: number) => void, reject: (error: Error) => void, targetWindow?: Window): void {
        const win = targetWindow || window;
        const frameId = win.requestAnimationFrame((paintStartTimestamp)=>{
            unsubscribe();
            fn(paintStartTimestamp);
        });

        const {unsubscribe} = subscribe(abortController.signal, 'abort', () => {
            unsubscribe();
            win.cancelAnimationFrame(frameId);
            reject(new AbortError(abortController.signal.reason));
        }, false);
    },

    /**
     * Returns a promise that resolves on the next animation frame.
     * @param abortController - Controller to abort the scheduled frame.
     * @param targetWindow - Optional window to use for requestAnimationFrame.
     * @see {@link browser.frame}
     */
    frameAsync(abortController: AbortController, targetWindow?: Window): Promise<number> {
        return new Promise((resolve, reject) => {
            this.frame(abortController, resolve, reject, targetWindow);
        });
    },

    getImageData(img:  HTMLImageElement | ImageBitmap, padding: number = 0): ImageData {
        const context = this.getImageCanvasContext(img);
        return context.getImageData(-padding, -padding, img.width + 2 * padding, img.height + 2 * padding);
    },

    /**
     * Draws an image into a 2D canvas whose pixels can be read back cheaply. An `OffscreenCanvas`
     * is used where the browser has one that reads back faithfully: drawing a GPU-backed
     * `ImageBitmap` into a document canvas opened with `willReadFrequently` stalls the main thread
     * for tens of milliseconds on GPU-accelerated browsers, an `OffscreenCanvas` does not.
     */
    getImageCanvasContext(img: HTMLImageElement | ImageBitmap): CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D {
        let context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;
        if (offscreenCanvasSupported() && !isOffscreenCanvasDistorted()) {
            context = new OffscreenCanvas(img.width, img.height).getContext('2d', {willReadFrequently: true});
        } else {
            const canvas = window.document.createElement('canvas');
            canvas.width = img.width;
            canvas.height = img.height;
            context = canvas.getContext('2d', {willReadFrequently: true});
        }
        if (!context) {
            throw new Error('failed to create canvas 2d context');
        }
        context.drawImage(img, 0, 0, img.width, img.height);
        return context;
    },

    resolveURL(path: string): string {
        linkEl ||= document.createElement('a');
        linkEl.href = path;
        return linkEl.href;
    },

    get hardwareConcurrency(): number {
        return (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 4;
    },

    get prefersReducedMotion(): boolean {
        if (reducedMotionOverride !== undefined) return reducedMotionOverride;
        // In case your test crashes when checking matchMedia, call setMatchMedia from 'src/util/test/util'
        if (!matchMedia) return false;
        //Lazily initialize media query
        reducedMotionQuery ??= matchMedia('(prefers-reduced-motion: reduce)');
        return reducedMotionQuery.matches;
    },

    set prefersReducedMotion(value: boolean) {
        reducedMotionOverride = value;
    }
};
