'use strict';

const Types = require('./types');
const Utils = require('./utils');


const internals = {
    needsProtoHack: new Set([Types.set, Types.map, Types.weakSet, Types.weakMap])
};


module.exports = internals.clone = function (obj, options = {}, _seen = null) {

    if (typeof obj !== 'object' ||
        obj === null) {

        return obj;
    }

    let clone = internals.clone;
    let seen = _seen;

    if (options.shallow) {
        if (options.shallow !== true) {
            return internals.cloneWithShallow(obj, options);
        }

        clone = (value) => value;
    }
    else {
        seen = seen || new Map();

        const lookup = seen.get(obj);
        if (lookup) {
            return lookup;
        }
    }

    const baseProto = Types.getInternalProto(obj);
    let newObj;

    switch (baseProto) {
        case Types.buffer:
            return Buffer && Buffer.from(obj);              // $lab:coverage:ignore$

        case Types.date:
            return new Date(obj.getTime());

        case Types.regex:
            return new RegExp(obj);

        case Types.array:
            newObj = [];
            break;

        default:
            if (options.prototype !== false) {              // Defaults to true
                const proto = Object.getPrototypeOf(obj);
                if (proto &&
                    proto.isImmutable) {

                    return obj;
                }

                if (internals.needsProtoHack.has(baseProto)) {
                    newObj = new proto.constructor();
                    if (proto !== baseProto) {
                        Object.setPrototypeOf(newObj, proto);
                    }
                }
                else {
                    newObj = Object.create(proto);
                }
            }
            else if (internals.needsProtoHack.has(baseProto)) {
                newObj = new baseProto.constructor();
            }
            else {
                newObj = {};
            }
    }

    if (seen) {
        seen.set(obj, newObj);                              // Set seen, since obj could recurse
    }

    if (baseProto === Types.set) {
        for (const value of obj) {
            newObj.add(clone(value, options, seen));
        }
    }
    else if (baseProto === Types.map) {
        for (const [key, value] of obj) {
            newObj.set(key, clone(value, options, seen));
        }
    }

    const keys = Utils.keys(obj, options);
    for (let i = 0; i < keys.length; ++i) {
        const key = keys[i];

        if (baseProto === Types.array &&
            key === 'length') {

            continue;
        }

        const descriptor = Object.getOwnPropertyDescriptor(obj, key);
        if (descriptor &&
            (descriptor.get || descriptor.set)) {

            Object.defineProperty(newObj, key, descriptor);
        }
        else {
            Object.defineProperty(newObj, key, {
                enumerable: descriptor ? descriptor.enumerable : true,
                writable: true,
                configurable: true,
                value: clone(obj[key], options, seen)
            });
        }
    }

    if (baseProto === Types.array) {
        newObj.length = obj.length;
    }

    return newObj;
};


internals.cloneWithShallow = function (source, options) {

    const keys = options.shallow;
    options = Object.assign({}, options);
    options.shallow = false;

    const storage = Utils.store(source, keys);    // Move shallow copy items to storage
    const copy = internals.clone(source, options);      // Deep copy the rest
    Utils.restore(copy, source, storage);         // Shallow copy the stored items and restore
    return copy;
};