/**
 * Clone a value deeply if it's an object or array.
 * @param value the value to be cloned
 * @param key optional - indicates the key where this value was present in the parent object, if applicable
 * @returns a clone of value
 */
export function deepCloneValue<T>(value: T, key?: string): T {
    switch (typeof value) {
        case 'object':
            if (Array.isArray(value)) {
                // @ts-ignore
                return deepCloneArray(value);
            } else if (value === null) {
                // @ts-ignore
                return null;
            } else {
                // @ts-ignore
                return deepCloneObject(value);
            }
        case 'number':
        case 'bigint':
        case 'string':
        case 'symbol':
        case 'boolean':
            return value
        case 'function':
            if (key) {
                console.warn('deepCloneValue encountered a function value at', key)
            } else {
                console.warn('deepCloneValue encountered a function value!')
            }
            return value;
        case 'undefined':
            return value;
    }
    throw "unreachable"
}

export function deepCloneArray(data: unknown[]): typeof data {
    return data.map(v => deepCloneValue(v))
}

export function deepCloneObject<T extends object>(data: T): T {
    // @ts-ignore
    let clone: T = {};
    for (const [key, value] of Object.entries(data)) {
        // @ts-ignore
        clone[key] = deepCloneValue(value, key);
    }
    return clone;
}

export function isValueDifferent(value1: unknown, value2: unknown): boolean {
    if (typeof value1 !== typeof value2) {
        return true;
    }
    switch (typeof value1) {
        case 'number':
        case 'bigint':
        case 'string':
        case 'symbol':
        case 'boolean':
            return value1 !== value2;
        case 'object':
            if (Array.isArray(value1) && Array.isArray(value2)) {
                return isArrayDifferent(value1, value2);
            } else if (!Array.isArray(value1) && !Array.isArray(value2)) {
                // @ts-ignore
                return isObjectDifferent(value1, value2);
            } else {
                // one is array and one is object
                return true;
            }
        case 'function':
            return value1 !== value2;
        case 'undefined':
            return false;
    }
}

export function isArrayDifferent(value1: unknown[], value2: unknown[]): boolean {
    if (value1.length !== value2.length) {
        return true;
    }
    for (let index = 0; index < value1.length; index++) {
        if (isValueDifferent(value1[index], value2[index])) {
            return true;
        }
    }
    return false;
}

export function isObjectDifferent<T extends object>(data1: T, data2: T): boolean {
    for (const [key, value1] of Object.entries(data1)) {
        if (!(key in data2)) {
            return true;
        }
        // @ts-ignore
        const value2 = data2[key];
        if (isValueDifferent(value1, value2)) {
            return true;
        }
    }
    return false;
}
