import isMergeableObject from 'is-mergeable-object';

function emptyTarget(val)
{
	return Array.isArray(val) ? [] : {}
}

function cloneUnlessOtherwiseSpecified(value, optionsArgument: IOptions, tmp?: ICache)
{
	const clone = !optionsArgument || optionsArgument.clone !== false;

	const bool = clone && _isMergeableObject(value, optionsArgument, tmp);

	let ret = (bool)
		? deepmerge(emptyTarget(value), value, optionsArgument)
		: value;

	if (optionsArgument?.keyValueOrMode && !bool && tmp && ('key' in tmp))
	{
		if (tmp.destination)
		{
			//console.log('destination', tmp.destination[tmp.key], ret, tmp.key);
			ret = tmp.destination[tmp.key] || ret;
		}

		if (tmp.target)
		{
			//console.log('target', tmp.target[tmp.key], ret, tmp.key);
			ret = tmp.target[tmp.key] || ret;
		}

		if (tmp.source)
		{
			//console.log('source', tmp.source[tmp.key], ret, tmp.key);
			ret = tmp.source[tmp.key] || ret;
		}
	}

	return ret;
}

export function _isMergeableObject(value, optionsArgument: IOptions, tmp?: ICache): boolean
{
	let ret = optionsArgument?.isMergeableObject?.(value, isMergeableObject, optionsArgument, tmp) as any;

	if (ret === null || typeof ret === 'undefined')
	{
		if ((typeof value?.[SYMBOL_IS_MERGEABLE] == 'boolean'))
		{
			ret = value[SYMBOL_IS_MERGEABLE];
		}
		else
		{
			ret = isMergeableObject(value);
		}
	}
	return ret
}

function defaultArrayMerge(target, source, optionsArgument: IOptions)
{
	return target.concat(source).map(function (element, index, array)
	{
		return cloneUnlessOtherwiseSpecified(element, optionsArgument, {
			key: index,
		})
	})
}

function mergeObject(target, source, optionsArgument: IOptions)
{
	let destination = {};
	if (_isMergeableObject(target, optionsArgument))
	{
		Object.keys(target).forEach(function (key)
		{
			destination[key] = cloneUnlessOtherwiseSpecified(target[key], optionsArgument, {
				key,
				source,
				target,
				destination,
			})
		})
	}
	Object.keys(source).forEach(function (key)
	{
		if (!_isMergeableObject(source[key], optionsArgument, {
				key,
				source,
				target,
			}) || !target[key])
		{
			destination[key] = cloneUnlessOtherwiseSpecified(source[key], optionsArgument, {
				key,
				source,
				target,
			})
		}
		else
		{
			destination[key] = deepmerge(target[key], source[key], optionsArgument)
		}
	});
	return destination
}

export function deepmerge<T1, T2>(x: T1, y: T2, options?: IOptions): Partial<T1 & T2>
export function deepmerge<T>(x: Partial<T>, y: Partial<T>, options?: IOptions): Partial<T>
export function deepmerge(target, source, optionsArgument)
{
	const sourceIsArray = Array.isArray(source);
	const targetIsArray = Array.isArray(target);
	const options = optionsArgument || { arrayMerge: defaultArrayMerge };
	const sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;

	if (!sourceAndTargetTypesMatch)
	{
		return cloneUnlessOtherwiseSpecified(source, optionsArgument, {
			target,
			source,
		});
	}
	else if (sourceIsArray)
	{
		let arrayMerge = options.arrayMerge || defaultArrayMerge;
		return arrayMerge(target, source, optionsArgument);
	}
	else
	{
		return mergeObject(target, source, optionsArgument);
	}
}

export interface ICache
{
	key?
	source?
	target?
	destination?
}

export interface IOptions
{
	clone?: boolean;

	arrayMerge?(destination: any[], source: any[], options?: IOptions): any[];

	isMergeableObject?(value, isMergeableObject: typeof isMergeable, optionsArgument?: IOptions, key?): void;
	isMergeableObject?(value, isMergeableObject: typeof isMergeable, optionsArgument?: IOptions, key?): boolean;

	/**
	 * (val = old || new) mode
	 */
	keyValueOrMode?: boolean,
}

export function isMergeable(value: any): boolean
{
	return isMergeableObject(value)
}

const SYMBOL_IS_MERGEABLE = Symbol.for('SYMBOL_IS_MERGEABLE');

export { SYMBOL_IS_MERGEABLE }

export function deepmergeAll<T, T2 = any>(array: Array<Partial<T2 & T>>, optionsArgument?: IOptions): T2 & T
{
	if (!Array.isArray(array))
	{
		throw new Error('first argument should be an array')
	}

	// @ts-ignore
	return array.reduce(function (prev, next)
	{
		return deepmerge(prev, next, optionsArgument)
	}, {})
}

export { deepmergeAll as all }

export default deepmerge

// @ts-ignore
if (process.env.TSDX_FORMAT !== 'esm')
{
	Object.defineProperty(deepmerge, "__esModule", { value: true });

	Object.defineProperty(deepmerge, 'deepmerge', { value: deepmerge });
	Object.defineProperty(deepmerge, 'default', { value: deepmerge });

	Object.defineProperty(deepmerge, 'isMergeable', { value: isMergeable });
	Object.defineProperty(deepmerge, 'SYMBOL_IS_MERGEABLE', { value: SYMBOL_IS_MERGEABLE });
	Object.defineProperty(deepmerge, 'deepmergeAll', { value: deepmergeAll });
	Object.defineProperty(deepmerge, 'all', { value: deepmergeAll });

	Object.defineProperty(deepmerge, '_isMergeableObject', { value: _isMergeableObject });
}
