import { deepClone } from './deepClone';
import { isObject } from './isObject';
import type { DeepRequired } from './utilityTypes';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/ban-types
type NonNullObject = {};
/**
* Deep merges 2 objects. Properties from the second parameter are applied to the first.
* @remark `overwrites` is also mutated!
* @remark If the value of a key in `overwrites` is `undefined` then the value of that same key in `base` is used instead!
* @remark This is essentially `{ ...base, ...overwrites }` but recursively
* @param base Base object
* @param overwrites Overwrites to apply
* @example
* ```ts
* const base = { a: 0, b: 1 };
* const overwrites = {}; // will be { a: 0, b: 1 } after merge
* mergeDefault(base, overwrites) // { a: 0, b: 1 }
* ```
* @example
* ```ts
* const base = { a: 0, b: 1 };
* const overwrites = { a: 2, i: 3 };
* mergeDefault(base, overwrites) // { a: 2, i: 3, b: 1 };
* ```
* @example
* ```ts
* const base = { a: 0, b: 1 };
* const overwrites = { a: null };
* mergeDefault(base, overwrites) // { a: null, b: 1 };
* ```
* @example
* ```ts
* const base = { a: 0, b: 1 };
* const overwrites = { a: undefined };
* mergeDefault(base, overwrites) // { a: 0, b: 1 };
* ```
* @example
* ```ts
* const base = { a: null };
* const overwrites = { a: { b: 5 } };
* mergeDefault(base, overwrites) // { a: { b: 5 } };
* ```
*/
export function mergeDefault>(base: A, overwrites?: B): DeepRequired {
// If no overwrites are specified then deep clone the base
if (!overwrites) return deepClone(base) as DeepRequired;
for (const [baseKey, baseValue] of Object.entries(base)) {
const overwritesValueAtBaseKey = Reflect.get(overwrites, baseKey);
if (typeof overwritesValueAtBaseKey === 'undefined') {
Reflect.set(overwrites, baseKey, deepClone(baseValue));
} else if (isObject(overwritesValueAtBaseKey)) {
Reflect.set(overwrites, baseKey, mergeDefault((baseValue ?? {}) as NonNullObject, overwritesValueAtBaseKey));
}
}
return overwrites as DeepRequired;
}