function isPlainObject(obj: any) {
    if (typeof obj !== "object" || obj === null) {
        return false;
    }
    let proto = obj;
    while (Object.getPrototypeOf(proto) !== null) {
        proto = Object.getPrototypeOf(proto);
    }
    return Object.getPrototypeOf(obj) === proto;
}

/**
 * Inspired by `nanoclone` but with some modifications to keep references intact and
 * using `structuredClone` where possible.
 */
function deepClone<T>(src: T, seen = new Map()): T {
    if (seen.has(src)) {
        return seen.get(src);
    }

    let copy: any;
    if (
        "structuredClone" in window &&
        (src instanceof Date || src instanceof RegExp || src instanceof Map || src instanceof Set)
    ) {
        copy = structuredClone(src);
        seen.set(src, copy);
    } else if (Array.isArray(src)) {
        copy = new Array(src.length);
        seen.set(src, copy);
        for (let i = 0; i < src.length; i++) {
            copy[i] = deepClone(src[i], seen);
        }
    } else if (src instanceof Map) {
        copy = new Map();
        seen.set(src, copy);
        for (const [k, v] of src.entries()) {
            copy.set(k, deepClone(v, seen));
        }
    } else if (src instanceof Set) {
        copy = new Set();
        seen.set(src, copy);
        for (const v of src) {
            copy.add(deepClone(v, seen));
        }
    } else if (/*src instanceof Object*/ isPlainObject(src)) {
        copy = {};
        seen.set(src, copy);
        for (const [k, v] of Object.entries(src)) {
            copy[k] = deepClone(v, seen);
        }
    } else {
        // Keep reference intact, also for e.g. symbols and functions
        return src;
    }

    return copy;
}

export { deepClone };
