import { CacheAlgorithm } from '~/models/cache-algorithm.model';
import { CacheRecord } from '~/models/cache-record.model';
import type { ICacheOptions } from '~/models/cache-options.model';
import { time } from '~/utilities/time.utility';

export type ICache <T> = {
	has: (key: string) => boolean;
	get: (key: string) => T | null;
	set: (key: string, value: T) => T;
	delete: (key: string) => T | null;
	clear: () => void;
};

export class Cache <T> implements ICache<T> {
	static readonly DEFAULT_OPTIONS: ICacheOptions = {
		algorithm: CacheAlgorithm.LRU,
		maxSize: Infinity,
		ttl: time(1).days
	};

	readonly #algorithm: CacheAlgorithm;
	readonly #cache = new Map<string, CacheRecord<T>>();
	readonly #maxSize: number;
	readonly #ttl: number;

	constructor ({
		algorithm = Cache.DEFAULT_OPTIONS.algorithm,
		maxSize = Cache.DEFAULT_OPTIONS.maxSize,
		ttl = Cache.DEFAULT_OPTIONS.ttl
	}: Readonly<Partial<ICacheOptions>> = { ...Cache.DEFAULT_OPTIONS }) {
		this.#algorithm = algorithm;
		this.#maxSize = maxSize;
		this.#ttl = ttl;
	}

	has (key: string): boolean {
		return this.#cache.has(key);
	}

	get (key: string): T | null {
		const record = this.#cache.get(key);

		if (!record) return null;

		const now = Date.now();

		if (now - record.modified.getTime() > this.#ttl) {
			this.#cache.delete(key);

			return null;
		}

		return record.contents;
	}

	set (key: string, value: Readonly<T>): T {
		const existingRecord = this.#cache.get(key);

		if (existingRecord) {
			existingRecord.contents = value;

			this.#cache.set(key, existingRecord);

			return value;
		}

		const record = new CacheRecord(value);

		this.#cache.set(key, record);

		this.#enforceMaxSize();

		return value;
	}

	delete (key: string): T | null {
		const record = this.#cache.get(key);

		if (!record) return null;

		this.#cache.delete(key);

		return record.contents;
	}

	clear (): void {
		this.#cache.clear();
	}

	#enforceMaxSize (): void {
		if (this.#cache.size <= this.#maxSize) return;

		const purgeLRU = (): void => {
			const oldestKey = [...this.#cache.entries()]
				// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
				.sort(([, { read: readA }], [, { read: readB }]) => {
					if (readA > readB) return 1;
					if (readA < readB) return -1;

					return 0;
				})[0][0];

			this.delete(oldestKey);
		};

		switch (this.#algorithm) {
			case CacheAlgorithm.LRU:
			default:
				while (this.#cache.size > this.#maxSize) purgeLRU();
		}
	}
}
