/* @flow */

import { transform, once } from 'lodash-es';
import { extendDeep } from '../../utils/object';
import { batch, keyedMutex } from '../../utils/async';
import { apiToPromise } from '../utils/api';
import { sendMessage } from './messaging';

const __set = apiToPromise((items, callback) => chrome.storage.local.set(items, callback));
const _set = (key, value) => __set({ [key]: value });
const __get = apiToPromise((keys, callback) => chrome.storage.local.get(keys, callback));
const _get = async (key, defaultValue = null) => (await __get({ [key]: defaultValue }))[key];
const _delete = apiToPromise((keys, callback) => chrome.storage.local.remove(keys, callback));
const _clear = apiToPromise(callback => chrome.storage.local.clear(callback));

const withLockOn = keyedMutex(<T>(key: string, fn: () => T): T => fn());

export function get(key: string): Promise<any | null> {
	return withLockOn(key, () => _get(key, null));
}

export function getAll(): Promise<{ [string]: mixed }> {
	return __get(null);
}

export function getMultiple<T: string>(keys: T[]): Promise<{ [T]: any | null }> {
	const defaults = {};
	for (const k of keys) {
		defaults[k] = null;
	}
	return __get(defaults);
}

export function set(key: string, value: mixed): Promise<void> {
	return withLockOn(key, () => _set(key, value));
}

export function setMultiple(valueMap: { +[string]: mixed }): Promise<void> {
	return __set(valueMap);
}

function compareAndSet(key: string, defaultValue: mixed, oldValue: mixed, newValue: mixed): Promise<boolean> {
	// Redirect to the background instance as it allows for a common mutex,
	// preventing race conditions when several tabs invokes this function around the same time
	return sendMessage('storage-cas', [key, defaultValue, oldValue, newValue]);
}

/*
 * Deeply patches an object in storage, like extendDeep().
 */
function patch(key: string, value: { [string]: mixed }): Promise<void> {
	return withLockOn(key, async () => {
		const extended = extendDeep(await _get(key) || {}, value);
		return _set(key, extended);
	});
}

/*
 * Shallowly patches an object in storage, like Object.assign().
 */
function patchShallow(key: string, value: { [string]: mixed }): Promise<void> {
	return withLockOn(key, async () => {
		const extended = Object.assign(await _get(key) || {}, value);
		return _set(key, extended);
	});
}

/*
 * Deletes a property on a value in storage.
 * i.e. `deletePath('userTaggerStorageKey', 'username', 'tag')`
 * will `delete userTaggerStoredValue.username.tag`
 */
function deletePaths(key: string, paths: string[][]): Promise<void> {
	return withLockOn(key, async () => {
		const stored = await _get(key);
		if (!stored) return;
		for (const path of paths) {
			path.reduce((obj, key, i, { length }) => {
				if (!obj) return;
				if (i < length - 1) return obj[key];
				delete obj[key];
			}, stored);
		}
		return _set(key, stored);
	});
}

function delete_(key: string): Promise<void> {
	return withLockOn(key, () => _delete(key));
}
export { delete_ as delete };

export function deleteMultiple(keys: string[]): Promise<void> {
	return _delete(keys);
}

export function has(key: string): Promise<boolean> {
	return withLockOn(key, async () => {
		const sentinel = Math.random();
		return (await _get(key, sentinel)) !== sentinel;
	});
}

export async function keys(): Promise<string[]> {
	return Object.keys(await __get(null));
}

export function clear(): Promise<void> {
	return _clear();
}

class Wrapper<T> {
	_key: () => string;
	_default: T;

	constructor(key: $PropertyType<this, '_key'>, def: T) {
		this._key = key;
		this._default = def;
	}

	get(): Promise<T> {
		return get(this._key()).then(val => (val === null ? this._default : val));
	}

	set(value: T): Promise<void> {
		return set(this._key(), value);
	}

	patch(value: $Shape<T>): Promise<void> {
		return patch(this._key(), (value: any));
	}

	compareAndSet(oldValue: T, newValue: T): Promise<boolean> {
		return compareAndSet(this._key(), this._default, oldValue, newValue);
	}

	deletePath(...path: string[]): Promise<void> {
		return deletePaths(this._key(), [path]);
	}

	delete(): Promise<void> {
		return delete_(this._key());
	}

	has(): Promise<boolean> {
		return has(this._key());
	}
}

export function wrap<T>(key: string | () => string /* pure */, defaultValue: T): Wrapper<T> {
	const keyGenerator = typeof key === 'string' ? () => key : once(key);
	// $FlowIssue
	return new Wrapper(keyGenerator, defaultValue);
}

class PrefixWrapper<T> {
	_prefix: string;
	_keyMapper: (key: string) => string;
	_default: () => T;
	_get: *;

	constructor(prefix: string, def: () => T, keyMapper: (key: string) => string, batching: boolean) {
		this._prefix = prefix;
		this._default = def;
		this._keyMapper = keyMapper;

		if (batching) {
			this._get = batch(async keys => {
				const v = await this.getMultipleNullable(keys);
				return keys.map(key => v[this._keyMapper(key)]);
			}, { size: Infinity, delay: 0 });
		} else {
			this._get = key => get(this._keyGen(key));
		}
	}

	_keyGen(key: string): string {
		return this._prefix + this._keyMapper(key);
	}

	get(key: string): Promise<T> {
		return this._get(key).then(val => (val === null ? this._default() : val));
	}

	getNullable(key: string): Promise<T | null> {
		return this._get(key);
	}

	async getAll(): Promise<{ [string]: T }> {
		const everything = await getAll();
		return transform(everything, (acc, v, k) => {
			if (k.startsWith(this._prefix)) {
				acc[k.slice(this._prefix.length)] = v;
			}
		}, {});
	}

	async getMultiple(keys: string[]): Promise<{ [string]: T }> {
		const rawValues = await getMultiple(keys.map(k => this._keyGen(k)));
		return transform(rawValues, (acc, v, k) => {
			acc[k.slice(this._prefix.length)] = (v === null ? this._default() : v);
		}, {});
	}

	async getMultipleNullable(keys: string[]): Promise<{ [string]: T | null }> {
		const rawValues = await getMultiple(keys.map(k => this._keyGen(k)));
		return transform(rawValues, (acc, v, k) => {
			acc[k.slice(this._prefix.length)] = v;
		}, {});
	}

	set(key: string, value: T): Promise<void> {
		return set(this._keyGen(key), value);
	}

	patch(key: string, value: $Shape<T>): Promise<void> {
		return patch(this._keyGen(key), (value: any));
	}

	deletePath(key: string, ...path: string[]): Promise<void> {
		return deletePaths(this._keyGen(key), [path]);
	}

	delete(key: string): Promise<void> {
		return delete_(this._keyGen(key));
	}

	deleteMultiple(keys: string[]): Promise<void> {
		return deleteMultiple(keys.map(k => this._keyGen(k)));
	}

	has(key: string): Promise<boolean> {
		return has(this._keyGen(key));
	}
}

export function wrapPrefix<T>(prefix: string, defaultValue: () => T, destructiveKeyMapper: (key: string) => string = x => x, batching: boolean = false): PrefixWrapper<T> {
	return new PrefixWrapper(prefix, defaultValue, destructiveKeyMapper, batching);
}

// FIXME Flow seems to conflate the uses in `newCommentCount.js` and `readComments.js`
export function wrapPrefix2<T>(prefix: string, defaultValue: () => T, destructiveKeyMapper: (key: string) => string = x => x, batching: boolean = false): PrefixWrapper<T> {
	return new PrefixWrapper(prefix, defaultValue, destructiveKeyMapper, batching);
}

class BlobWrapper<T> {
	_rootKey: string;
	_default: () => T;

	constructor(rootKey: string, def: () => T) {
		this._rootKey = rootKey;
		this._default = def;
	}

	get(key: string): Promise<T> {
		return get(this._rootKey).then(val => (
			(val === null || val[key] === undefined) ? this._default() : val[key]
		));
	}

	getNullable(key: string): Promise<T | null> {
		return get(this._rootKey).then(val => (
			(val === null || val[key] === undefined) ? null : val[key]
		));
	}

	getAll(): Promise<{ [string]: T }> {
		return get(this._rootKey).then(val => (val === null ? {} : val));
	}

	async getMultiple(keys: string[]): Promise<{ [string]: T }> {
		const rawValues = (await get(this._rootKey)) || {};
		return transform(keys, (acc, key) => {
			acc[key] = rawValues[key] === undefined ? this._default() : rawValues[key];
		}, {});
	}

	async getMultipleNullable(keys: string[]): Promise<{ [string]: T | null }> {
		const rawValues = (await get(this._rootKey)) || {};
		return transform(keys, (acc, key) => {
			acc[key] = rawValues[key] === undefined ? null : rawValues[key];
		}, {});
	}

	set(key: string, value: T): Promise<void> {
		return patchShallow(this._rootKey, { [key]: value });
	}

	patch(key: string, value: $Shape<T>): Promise<void> {
		return patch(this._rootKey, { [key]: value });
	}

	deletePath(key: string, ...path: string[]): Promise<void> {
		return deletePaths(this._rootKey, [[key, ...path]]);
	}

	delete(key: string): Promise<void> {
		return deletePaths(this._rootKey, [[key]]);
	}

	deleteMultiple(keys: string[]): Promise<void> {
		return deletePaths(this._rootKey, keys.map(k => [k]));
	}

	has(key: string): Promise<boolean> {
		return get(this._rootKey).then(val => (val !== null && val[key] !== undefined));
	}

	clear(): Promise<void> {
		return delete_(this._rootKey);
	}
}

export function wrapBlob<T>(rootKey: string, defaultValue: () => T): BlobWrapper<T> {
	return new BlobWrapper(rootKey, defaultValue);
}
