
import { Cache } from '../Cache';
import { CommonCacheOptions } from '../CommonCacheOptions';
import { KeyType } from '../KeyType';
import { WrappedCache } from '../WrappedCache';

import { Loader } from './Loader';
import { LoadingCache } from './LoadingCache';

const DATA = Symbol('loadingData');

/**
 * Options available for a loading cache.
 */
export interface LoadingCacheOptions<K extends KeyType, V> extends CommonCacheOptions<K, V> {
	loader?: Loader<K, V> | undefined | null;

	parent: Cache<K, V>;
}

interface LoadingCacheData<K extends KeyType, V> {
	promises: Map<K, Promise<V>>;

	loader: Loader<K, V> | null;
}

/**
 * Extension to another cache that will load items if they are not cached.
 */
export class DefaultLoadingCache<K extends KeyType, V> extends WrappedCache<K, V> implements LoadingCache<K, V> {
	private [DATA]: LoadingCacheData<K, V>;

	public constructor(options: LoadingCacheOptions<K, V>) {
		super(options.parent, options.removalListener || null);

		this[DATA] = {
			promises: new Map(),
			loader: options.loader || null
		};
	}

	/**
	 * Get cached value or load it if not currently cached. Updates the usage
	 * of the key.
	 *
	 * @param key -
	 *   key to get
	 * @param loader -
	 *   optional loader to use for loading the object
	 * @returns
	 *   promise that resolves to the loaded value
	 */
	public get(key: K, loader?: Loader<K, V>): Promise<V> {
		const currentValue = this.getIfPresent(key);
		if(currentValue !== null) {
			return Promise.resolve(currentValue);
		}

		const data = this[DATA];

		// First check if we are already loading this value
		let promise = data.promises.get(key);
		if(promise) return promise;

		// Create the initial promise if we are not already loading
		if(typeof loader !== 'undefined') {
			if(typeof loader !== 'function') {
				throw new Error('If loader is used it must be a function that returns a value or a Promise');
			}
			promise = Promise.resolve(loader(key));
		} else if(data.loader) {
			promise = Promise.resolve(data.loader(key));
		}

		if(! promise) {
			throw new Error('No way to load data for key: ' + key);
		}

		// Enhance with handler that will remove promise and set value if success
		const resolve = () => data.promises.delete(key);
		promise = promise.then(result => {
			this.set(key, result);
			resolve();
			return result;
		}).catch(err => {
			resolve();
			throw err;
		});

		data.promises.set(key, promise);

		return promise;
	}
}
