import { DeepImmutable, Scalar } from './loader/babylonjs-import';
import { asWriteable } from './Types';

/**
 * Cached information from a `DOMRect`.
 */
export interface DOMRectCached extends DeepImmutable<DOMRect> {
    /** Same as `HTMLElement.scrollLeft` */
    readonly scrollLeft: number;
    /** Same as `HTMLElement.scrollTop` */
    readonly scrollTop: number;

    /**
     * Last time, in ms, {@link refresh} was called.
     */
    readonly lastRefreshInMs: number;

    readonly document: {
        readonly scrollLeft: number;
        readonly scrollTop: number;
    };

    /**
     * Refreshes cached client rect information (by calling wrapped `HTMElement.getBoundingClientRect()`).
     * Updates {@link lastRefreshInMs} when called.
     */
    refresh(): void;
}

class BoundingClientRectCacheElement implements DOMRectCached {
    private _lastRefreshInMs: number;

    public readonly height: number;
    public readonly width: number;
    public readonly x: number;
    public readonly y: number;
    public readonly bottom: number;
    public readonly left: number;
    public readonly right: number;
    public readonly top: number;
    public readonly scrollLeft: number;
    public readonly scrollTop: number;
    public readonly document: DOMRectCached['document'] = {
        scrollLeft: 0,
        scrollTop: 0
    };

    public constructor(public element: HTMLElement) {
        this.refresh();
    }

    public get lastRefreshInMs(): number {
        return this._lastRefreshInMs;
    }

    // eslint-disable-next-line @typescript-eslint/no-empty-function
    public toJSON(): void {}

    public refresh(): void {
        this._lastRefreshInMs = performance.now();
        const rect = this.element.getBoundingClientRect();
        const writable = asWriteable(this);
        writable.height = rect.height;
        writable.width = rect.width;
        writable.x = rect.x;
        writable.y = rect.y;
        writable.bottom = rect.bottom;
        writable.left = rect.left;
        writable.right = rect.right;
        writable.top = rect.top;
        writable.scrollLeft = this.element.scrollLeft;
        writable.scrollTop = this.element.scrollTop;
        writable.document.scrollLeft = document.documentElement.scrollLeft;
        writable.document.scrollTop = document.documentElement.scrollTop;
    }
}

/**
 * Cache for bounding client rectangles. This is useful when cost of `HTMLElement.getBoundingClientRect()`
 * becomes a problem (it causes browser reflow which in some situations can be very costly). By caching
 * the information from the `DOMRect`, this cost can be avoided.
 */
export class BoundingClientRectCache {
    private readonly _map = new WeakMap<HTMLElement, BoundingClientRectCacheElement>();

    /**
     * Get or add a {@link DOMRectCached} instance for a specified `element`.
     * @param element HTMLElement to get a {@link DOMRectCached} instance for.
     * @param timeInMsToNextRefresh Optional. Defaults to 300. If the instance, returned by this method, already exists
     * in the cache. Then {@link DOMRectCached.refresh} method on the instance is called automatically if
     *
     * 1. `timeInMsToNextRefresh = 0`. Useful when {@link DOMRectCached} should be refreshed immediatly.
     * 2. `timeInMsToNextRefresh > 0 && performance.now() - instance.lastRefreshInMs > timeInMsToNextRefresh`
     *
     * This means that if `timeInMsToNextRefresh < 0` then {@link DOMRectCached.refresh} will never be called. This can be useful
     * when we never want to refresh the {@link DOMRectCached} instance.
     * @returns A {@link DOMRectCached} element.
     */
    public getOrAdd(element: HTMLElement, timeInMsToNextRefresh = 300): DOMRectCached {
        let i = this._map.get(element);
        if (i === undefined) {
            i = new BoundingClientRectCacheElement(element);
            this._map.set(element, i);
        } else {
            timeInMsToNextRefresh = timeInMsToNextRefresh ?? 300;
            const hasExpired =
                Scalar.WithinEpsilon(timeInMsToNextRefresh, 0) ||
                (timeInMsToNextRefresh > 0 && performance.now() - i.lastRefreshInMs > timeInMsToNextRefresh);
            if (hasExpired) {
                i.refresh();
            }
        }

        return i;
    }
}

/**
 * Predefined cache for bounding client rectangles. This is useful when cost of `HTMLElement.getBoundingClientRect()`
 * becomes a problem (it causes browser reflow which in some situations can be very costly.). By caching
 * the information we get from the `DOMRect` we can avoid this cost.
 */
export const boundingClientRectCache = new BoundingClientRectCache();
