/**
 * @aedart/contracts
 * 
 * BSD-3-Clause, Copyright (c) 2023-present Alin Eugen Deac <aedart@gmail.com>.
 */

import { Throwable } from '@aedart/contracts/support/exceptions';
import { ArrayMergeOptions } from '@aedart/contracts/support/arrays';

/**
 * Cloneable
 */
interface Cloneable {
    /**
     * Returns a clone (new instance) of this object
     *
     * @return {this}
     *
     * @throws {Error}
     */
    clone(): this;
}

/**
 * Populatable
 *
 * Able to be populated (hydrated) with data
 */
interface Populatable {
    /**
     * Populate this component with data
     *
     * **Note**: _When no `data` is provided, then nothing is populated_
     *
     * @param {any} [data] E.g. key-value pair (object), array, or any other kind
     *                     of data.
     *
     * @throws {TypeError} When unable to populate with given data. Or, if type of data is
     *                     not supported.
     */
    populate(data?: any): this;
}

/**
 * Merge Exception
 *
 * To be thrown when two or more objects are unable to be merged.
 */
interface MergeException extends Throwable {
}

/**
 * Merge Source Info
 *
 * Contains information about what key-value must be merged from a target source object,
 * along with other meta information.
 */
interface MergeSourceInfo {
    /**
     * The resulting object (relative to object depth)
     *
     * @type {object}
     */
    result: object;
    /**
     * The target property key in source object to
     *
     * @type {PropertyKey}
     */
    key: PropertyKey;
    /**
     * Value of the property in source object
     *
     * @type {any}
     */
    value: any;
    /**
     * The source object that holds the property key and value
     *
     * @type {object}
     */
    source: object;
    /**
     * Source object's index (relative to object depth)
     *
     * @type {number}
     */
    sourceIndex: number;
    /**
     * The current recursion depth
     */
    depth: number;
}

/**
 * Callback to perform the merging of nested objects.
 * Invoking this callback results in the merge callback to
 * be repeated, for the given source objects.
 *
 * @type {function}
 */
type NextCallback = (
/**
 * The nested objects to be merged
 *
 * @type {object[]}
 */
sources: object[], 
/**
 * The merge options to be applied
 *
 * @type {Readonly<MergeOptions>}
 */
options: Readonly<MergeOptions>, 
/**
 * The next recursion depth number
 *
 * @type {number}
 */
nextDepth: number) => any;
/**
 * Merge callback function
 *
 * The callback is responsible for resolving the value to be merged into the resulting object.
 *
 * **Note**: _[Skipped keys]{@link MergeOptions.skip} are NOT provided to the callback._
 *
 * **Note**: _The callback is responsible for respecting the given [options]{@link MergeOptions},
 * ([keys to be skipped]{@link MergeOptions.skip} and [depth]{@link MergeOptions.depth} excluded)_
 */
type MergeCallback = (
/**
 * Source target information
 *
 * @type {MergeSourceInfo}
 */
target: MergeSourceInfo, 
/**
 * Callback to invoke for merging nested objects
 *
 * @type {function}
 */
next: NextCallback, 
/**
 * The merge options to be applied
 *
 * @type {Readonly<MergeOptions>}
 */
options: Readonly<MergeOptions>) => any;
/**
 * A callback that determines if property key should be skipped
 *
 * If the callback returns `true`, then the given key will NOT be merged
 * from the given source object.
 *
 * @see {MergeOptions}
 */
type SkipKeyCallback = (key: PropertyKey, source: object, result: object) => boolean;

/**
 * Merge Options
 */
interface MergeOptions {
    /**
     * The maximum merge depth
     *
     * **Note**: _Value must be greater than or equal zero._
     *
     * **Note**: _Defaults to [DEFAULT_MAX_MERGE_DEPTH]{@link import('@aedart/contracts/support/objects').DEFAULT_MAX_MERGE_DEPTH}
     * when not specified._
     *
     * @type {number}
     */
    depth?: number;
    /**
     * Property Keys that must not be merged.
     *
     * **Note**: [DANGEROUS_PROPERTIES]{@link import('@aedart/contracts/support/objects').DANGEROUS_PROPERTIES}
     * are always skipped, regardless of specified keys._
     *
     * **Callback**: _A callback can be specified to determine if a given key,
     * in a source object should be skipped._
     *
     * **Example:**
     * ```js
     * const a = { 'foo': true };
     * const b = { 'bar': true, 'zar': true };
     *
     * merge().using({ skip: [ 'zar' ] }).of(a, b); // { 'foo': true, 'bar': true }
     *
     * merge().using({ skip: (key, source) => {
     *      return key === 'bar' && Reflect.has(source, key);
     * } }).of(a, b); // { 'foo': true, 'zar': true }
     * ```
     *
     * @type {PropertyKey[] | SkipKeyCallback}
     */
    skip?: PropertyKey[] | SkipKeyCallback;
    /**
     * Flag, overwrite property values with `undefined`.
     *
     * **When `true` (_default behaviour_)**: _If an existing property value is not `undefined`, it will be overwritten
     * with new value, even if the new value is `undefined`._
     *
     * **When `false`**: _If an existing property value is not `undefined`, it will NOT be overwritten
     * with new value, if the new value is `undefined`._
     *
     * **Example:**
     * ```js
     * const a = { 'foo': true };
     * const b = { 'foo': undefined };
     *
     * merge(a, b); // { 'foo': undefined }
     *
     * merge().using({ overwriteWithUndefined: false }).of(a, b) // { 'foo': true }
     * ```
     *
     * @type {boolean}
     */
    overwriteWithUndefined?: boolean;
    /**
     * Flag, if source object is [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable}, then the
     * resulting object from the `clone()` method is used.
     *
     * **When `true` (_default behaviour_)**: _If source object is cloneable then the resulting object from `clone()`
     * method is used. Its properties are then iterated by the merge function._
     *
     * **When `false`**: _Cloneable objects are treated like any other objects, the `clone()` method is ignored._
     *
     * **Example:**
     * ```js
     * const a = { 'foo': { 'name': 'John Doe' } };
     * const b = { 'foo': {
     *      'name': 'Jane Doe',
     *      clone() {
     *          return {
     *              'name': 'Rick Doe',
     *              'age': 26
     *          }
     *      }
     * } };
     *
     * merge(a, b); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } }
     *
     * merge().using({ useCloneable: false }).of(a, b); // { 'foo': { 'name': 'Jane Doe', clone() {...} } }
     * ```
     *
     * @see [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable}
     *
     * @type {boolean}
     */
    useCloneable?: boolean;
    /**
     * Flag, whether to merge array, array-like, and [concat spreadable]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable}
     * properties or not.
     *
     * **When `true`**: _existing property is merged with new property value._
     *
     * **When `false` (_default behaviour_)**: _existing property is overwritten with new property value_
     *
     * **Example:**
     * ```js
     * const a = { 'foo': [ 1, 2, 3 ] };
     * const b = { 'foo': [ 4, 5, 6 ] };
     *
     * merge(a, b); // { 'foo': [ 4, 5, 6 ] }
     * merge().using({ mergeArrays: true }).of(a, b); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] }
     * ```
     *
     * **Note**: _`String()` (object) and [Typed Arrays]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray}
     * are not merged, even though they are considered to be "array-like" (they offer a `length` property).
     * You need to manually handle these, via a custom [callback]{@link MergeCallback}, if such value types must be merged._
     *
     * @see [merge (array)]{@link import('@aedart/support/arrays').merge}
     *
     * @type {boolean}
     */
    mergeArrays?: boolean;
    /**
     * Merge Options for arrays
     *
     * @type {ArrayMergeOptions}
     */
    arrayMergeOptions?: ArrayMergeOptions;
    /**
     * The merge callback that must be applied
     *
     * **Note**: _When no callback is provided, then the merge function's default
     * callback is used._
     *
     * @type {MergeCallback}
     */
    callback?: MergeCallback;
}

/**
 * Objects Merger
 *
 * Able to merge (deep merge) multiple source objects into a single new object.
 */
interface ObjectsMerger {
    /**
     * Use the following merge options or merge callback
     *
     * @param {MergeCallback | MergeOptions} [options] Merge callback or merge options.
     *
     * @return {this}
     *
     * @throws {MergeException}
     */
    using(options?: MergeCallback | MergeOptions): this;
    /**
     * Returns a merger of given source objects
     *
     * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
     * of all given sources._
     *
     * @template SourceA extends object
     *
     * @param {SourceA} a
     *
     * @returns {SourceA}
     *
     * @throws {MergeException}
     */
    of<SourceA extends object>(a: SourceA): SourceA;
    /**
     * Returns a merger of given source objects
     *
     * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
     * of all given sources._
     *
     * @template SourceA extends object
     * @template SourceB extends object
     *
     * @param {SourceA} a
     * @param {SourceB} b
     *
     * @returns {SourceA & SourceB}
     *
     * @throws {MergeException}
     */
    of<SourceA extends object, SourceB extends object>(a: SourceA, b: SourceB): SourceA & SourceB;
    /**
     * Returns a merger of given source objects
     *
     * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
     * of all given sources._
     *
     * @template SourceA extends object
     * @template SourceB extends object
     * @template SourceC extends object
     *
     * @param {SourceA} a
     * @param {SourceB} b
     * @param {SourceC} c
     *
     * @returns {SourceA & SourceB & SourceC}
     *
     * @throws {MergeException}
     */
    of<SourceA extends object, SourceB extends object, SourceC extends object>(a: SourceA, b: SourceB, c: SourceC): SourceA & SourceB & SourceC;
    /**
     * Returns a merger of given source objects
     *
     * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
     * of all given sources._
     *
     * @template SourceA extends object
     * @template SourceB extends object
     * @template SourceC extends object
     * @template SourceD extends object
     *
     * @param {SourceA} a
     * @param {SourceB} b
     * @param {SourceC} c
     * @param {SourceD} d
     *
     * @returns {SourceA & SourceB & SourceC & SourceD}
     *
     * @throws {MergeException}
     */
    of<SourceA extends object, SourceB extends object, SourceC extends object, SourceD extends object>(a: SourceA, b: SourceB, c: SourceC, d: SourceD): SourceA & SourceB & SourceC & SourceD;
    /**
     * Returns a merger of given source objects
     *
     * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
     * of all given sources._
     *
     * @template SourceA extends object
     * @template SourceB extends object
     * @template SourceC extends object
     * @template SourceD extends object
     * @template SourceE extends object
     *
     * @param {SourceA} a
     * @param {SourceB} b
     * @param {SourceC} c
     * @param {SourceD} d
     * @param {SourceE} e
     *
     * @returns {SourceA & SourceB & SourceC & SourceD & SourceE}
     *
     * @throws {MergeException}
     */
    of<SourceA extends object, SourceB extends object, SourceC extends object, SourceD extends object, SourceE extends object>(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE): SourceA & SourceB & SourceC & SourceD & SourceE;
    /**
     * Returns a merger of given source objects
     *
     * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
     * of all given sources._
     *
     * @template SourceA extends object
     * @template SourceB extends object
     * @template SourceC extends object
     * @template SourceD extends object
     * @template SourceE extends object
     * @template SourceF extends object
     *
     * @param {SourceA} a
     * @param {SourceB} b
     * @param {SourceC} c
     * @param {SourceD} d
     * @param {SourceE} e
     * @param {SourceF} f
     *
     * @returns {SourceA & SourceB & SourceC & SourceD & SourceE & SourceF}
     *
     * @throws {MergeException}
     */
    of<SourceA extends object, SourceB extends object, SourceC extends object, SourceD extends object, SourceE extends object, SourceF extends object>(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE, f: SourceF): SourceA & SourceB & SourceC & SourceD & SourceE & SourceF;
    /**
     * Returns a merger of given source objects
     *
     * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
     * of all given sources._
     *
     * @param {object[]} sources
     *
     * @returns {object}
     *
     * @throws {MergeException}
     */
    of(...sources: object[]): object;
}

/**
 * Default maximum merge depth
 *
 * @type {number}
 */
declare const DEFAULT_MAX_MERGE_DEPTH: number;

/**
 * Callback that returns properties to be selected from the source object.
 * The target is the object intended to have source properties merged or assigned
 * into.
 */
type SourceKeysCallback<SourceObj extends object = object, TargetObj extends object = object> = (source: SourceObj, target: TargetObj) => PropertyKey | PropertyKey[];

/**
 * Support Objects identifier
 *
 * @type {Symbol}
 */
declare const SUPPORT_OBJECTS: unique symbol;
/**
 * Properties that are considered dangerous and should be avoided when merging
 * objects or assigning properties.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf#description
 * @see https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html
 * @see https://medium.com/@king.amit95/prototype-pollution-a-deeper-inspection-82a226796966
 *
 * @type {PropertyKey[]}
 */
declare const DANGEROUS_PROPERTIES: PropertyKey[];

export { type Cloneable, DANGEROUS_PROPERTIES, DEFAULT_MAX_MERGE_DEPTH, type MergeCallback, type MergeException, type MergeOptions, type MergeSourceInfo, type NextCallback, type ObjectsMerger, type Populatable, SUPPORT_OBJECTS, type SkipKeyCallback, type SourceKeysCallback };
