/*
 * Copyright (c) 2015-2018, IGN France.
 * Copyright (c) 2018-2026, Giro3D team.
 * SPDX-License-Identifier: MIT
 */

import { LRUCache } from 'lru-cache';

import type MemoryUsage from './MemoryUsage';

import { isMemoryUsage, type GetMemoryUsageContext } from './MemoryUsage';

/**
 * The options for a cache entry.
 */
export interface CacheOptions {
    /**
     * The time to live of this entry, in milliseconds.
     */
    ttl?: number;
    /**
     * The entry size, in bytes. It does not have to be an exact value, but
     * it helps the cache determine when to remove entries to save memory.
     */
    size?: number;
    /**
     * A optional callback called when the entry is deleted from the cache.
     */
    onDelete?: (entry: unknown) => void;
}

/**
 * The default max number of entries.
 */
export const DEFAULT_MAX_ENTRIES = 8192;

/**
 * The default TTL (time to live), in milliseconds.
 */
export const DEFAULT_TTL: number = 240_000; // 240 seconds

/**
 * The default capacity, in bytes.
 */
export const DEFAULT_CAPACITY: number = 536_870_912; // 512 MB

export interface CacheConfiguration {
    /**
     * The default TTL (time to live) of entries, in milliseconds.
     * Can be overriden for each entry (see {@link CacheOptions}).
     * @defaultValue {@link DEFAULT_TTL}
     */
    ttl?: number;
    /**
     * The capacity, in bytes, of the cache.
     * @defaultValue {@link DEFAULT_CAPACITY}
     */
    byteCapacity?: number;
    /**
     * The capacity, in number of entries, of the cache.
     * @defaultValue {@link DEFAULT_MAX_ENTRIES}
     */
    maxNumberOfEntries?: number;
}

/**
 * The cache.
 *
 */
export class Cache implements MemoryUsage {
    public readonly isMemoryUsage = true as const;
    private readonly _deleteHandlers: Map<string, (entry: object) => void>;
    private _lru: LRUCache<string, object>;
    private _enabled: boolean;

    /**
     * Constructs a cache.
     *
     * @param opts - The options.
     */
    public constructor(opts?: CacheConfiguration) {
        this._deleteHandlers = new Map();

        this._enabled = true;
        this._lru = this.createLRUCache(opts);
    }

    private createLRUCache(opts?: CacheConfiguration): LRUCache<string, object> {
        return new LRUCache<string, object>({
            ttl: opts?.ttl ?? DEFAULT_TTL,
            ttlResolution: 1000, // 1 second
            updateAgeOnGet: true,
            maxSize: opts?.byteCapacity ?? DEFAULT_CAPACITY,
            max: opts?.maxNumberOfEntries ?? DEFAULT_MAX_ENTRIES,
            allowStale: false,
            dispose: (value, key): void => {
                this.onDisposed(key, value);
            },
        });
    }

    /**
     * Configure the cache with the specified configuration. The cache must be
     * empty otherwise this method will throw an error.
     */
    public configure(config: CacheConfiguration): void {
        if (this.count > 0) {
            throw new Error('cannot configure the cache as it is not empty.');
        }
        this._lru = this.createLRUCache(config);
    }

    public getMemoryUsage(context: GetMemoryUsageContext): void {
        this._lru.forEach(e => {
            if (isMemoryUsage(e)) {
                e.getMemoryUsage(context);
            }
        });
    }

    /**
     * Enables or disables the cache.
     */
    public get enabled(): boolean {
        return this._enabled;
    }

    public set enabled(v: boolean) {
        this._enabled = v;
    }

    /**
     * Gets or sets the default TTL (time to live) of the cache.
     */
    public get defaultTtl(): number {
        return this._lru.ttl;
    }

    public set defaultTtl(v: number) {
        this._lru.ttl = v;
    }

    /**
     * Gets the maximum size of the cache, in bytes.
     */
    public get maxSize(): number {
        return this._lru.maxSize;
    }

    /**
     * Gets the maximum number of entries.
     */
    public get capacity(): number {
        return this._lru.max;
    }

    /**
     * Gets the number of entries.
     */
    public get count(): number {
        return this._lru.size;
    }

    /**
     * Gets the size of entries, in bytes
     */
    public get size(): number {
        return this._lru.calculatedSize;
    }

    /**
     * Returns an array of entries.
     */
    public entries(): Array<unknown> {
        return [...this._lru.entries()];
    }

    private onDisposed(key: string, value: object): void {
        const handler = this._deleteHandlers.get(key);
        if (handler) {
            this._deleteHandlers.delete(key);
            handler(value);
        }
    }

    /**
     * Removes stale entries.
     */
    public purge(): void {
        this._lru.purgeStale();
    }

    /**
     * Returns the entry with the specified key, or `undefined` if no entry matches this key.
     *
     * @param key - The entry key.
     * @returns The entry, or `undefined`.
     */
    public get(key: string): unknown | undefined {
        if (!this.enabled) {
            return undefined;
        }

        return this._lru.get(key);
    }

    /**
     * Stores an entry in the cache, or replaces an existing entry with the same key.
     *
     * @param key - The key.
     * @param value - The value.
     * @param options - The options.
     */
    public set<T extends object>(key: string, value: T, options: CacheOptions = {}): T {
        if (!this.enabled) {
            return value;
        }

        if (typeof key !== 'string') {
            throw new Error('the cache expects strings as keys.');
        }

        this._lru.set(key, value, {
            ttl: options.ttl ?? this.defaultTtl,
            size: options.size ?? 1024, // Use a default size if not provided
        });

        if (options.onDelete) {
            this._deleteHandlers.set(key, options.onDelete);
        }

        return value;
    }

    /**
     * Deletes an entry.
     *
     * @param key - The key.
     * @returns `true` if the entry was deleted, `false` otherwise.
     */
    public delete(key: string): boolean {
        return this._lru.delete(key);
    }

    /**
     * Clears the cache.
     *
     */
    public clear(): void {
        this._lru.clear();
    }
}

/**
 * A global singleton cache.
 */
export const GlobalCache: Cache = new Cache();
