'use strict';

const Ref = require('./ref');


const internals = {};


internals.extendedCheckForValue = function (value, insensitive) {

    const valueType = typeof value;

    if (valueType === 'object') {
        if (value instanceof Date) {
            return (item) => {

                return item instanceof Date && value.getTime() === item.getTime();
            };
        }
        if (Buffer.isBuffer(value)) {
            return (item) => {

                return Buffer.isBuffer(item) && value.length === item.length && value.toString('binary') === item.toString('binary');
            };
        }
    }
    else if (insensitive && valueType === 'string') {
        const lowercaseValue = value.toLowerCase();
        return (item) => {

            return typeof item === 'string' && lowercaseValue === item.toLowerCase();
        };
    }

    return null;
};


module.exports = class InternalSet {

    constructor(from) {

        this._set = new Set(from);
        this._hasRef = false;
    }

    add(value, refs) {

        const isRef = Ref.isRef(value);
        if (!isRef && this.has(value, null, null, false)) {

            return this;
        }

        if (refs !== undefined) { // If it's a merge, we don't have any refs
            Ref.push(refs, value);
        }

        this._set.add(value);

        this._hasRef |= isRef;

        return this;
    }

    merge(add, remove) {

        for (const item of add._set) {
            this.add(item);
        }

        for (const item of remove._set) {
            this.remove(item);
        }

        return this;
    }

    remove(value) {

        this._set.delete(value);
        return this;
    }

    has(value, state, options, insensitive) {

        if (!this._set.size) {
            return false;
        }

        const hasValue = this._set.has(value);
        if (hasValue) {
            return hasValue;
        }

        const extendedCheck = internals.extendedCheckForValue(value, insensitive);
        if (!extendedCheck) {
            if (state && this._hasRef) {
                for (let item of this._set) {
                    if (Ref.isRef(item)) {
                        item = item(state.reference || state.parent, options);
                        if (value === item || (Array.isArray(item) && item.includes(value))) {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        return this._has(value, state, options, extendedCheck);
    }

    _has(value, state, options, check) {

        const checkRef = !!(state && this._hasRef);

        const isReallyEqual = function (item) {

            if (value === item) {
                return true;
            }

            return check(item);
        };

        for (let item of this._set) {
            if (checkRef && Ref.isRef(item)) { // Only resolve references if there is a state, otherwise it's a merge
                item = item(state.reference || state.parent, options);

                if (Array.isArray(item)) {
                    if (item.find(isReallyEqual)) {
                        return true;
                    }
                    continue;
                }
            }

            if (isReallyEqual(item)) {
                return true;
            }
        }

        return false;
    }

    values(options) {

        if (options && options.stripUndefined) {
            const values = [];

            for (const item of this._set) {
                if (item !== undefined) {
                    values.push(item);
                }
            }

            return values;
        }

        return Array.from(this._set);
    }

    slice() {

        const set = new InternalSet(this._set);
        set._hasRef = this._hasRef;
        return set;
    }

    concat(source) {

        const set = new InternalSet([...this._set, ...source._set]);
        set._hasRef = !!(this._hasRef | source._hasRef);
        return set;
    }
};
