'use strict';

const Assert = require('@hapi/hoek/lib/assert');
const DeepEqual = require('@hapi/hoek/lib/deepEqual');

const Common = require('./common');


const internals = {};


module.exports = internals.Values = class {

    constructor(values, refs) {

        this._values = new Set(values);
        this._refs = new Set(refs);
        this._lowercase = internals.lowercases(values);

        this._override = false;
    }

    get length() {

        return this._values.size + this._refs.size;
    }

    add(value, refs) {

        // Reference

        if (Common.isResolvable(value)) {
            if (!this._refs.has(value)) {
                this._refs.add(value);

                if (refs) {                     // Skipped in a merge
                    refs.register(value);
                }
            }

            return;
        }

        // Value

        if (!this.has(value, null, null, false)) {
            this._values.add(value);

            if (typeof value === 'string') {
                this._lowercase.set(value.toLowerCase(), value);
            }
        }
    }

    static merge(target, source, remove) {

        target = target || new internals.Values();

        if (source) {
            if (source._override) {
                return source.clone();
            }

            for (const item of [...source._values, ...source._refs]) {
                target.add(item);
            }
        }

        if (remove) {
            for (const item of [...remove._values, ...remove._refs]) {
                target.remove(item);
            }
        }

        return target.length ? target : null;
    }

    remove(value) {

        // Reference

        if (Common.isResolvable(value)) {
            this._refs.delete(value);
            return;
        }

        // Value

        this._values.delete(value);

        if (typeof value === 'string') {
            this._lowercase.delete(value.toLowerCase());
        }
    }

    has(value, state, prefs, insensitive) {

        return !!this.get(value, state, prefs, insensitive);
    }

    get(value, state, prefs, insensitive) {

        if (!this.length) {
            return false;
        }

        // Simple match

        if (this._values.has(value)) {
            return { value };
        }

        // Case insensitive string match

        if (typeof value === 'string' &&
            value &&
            insensitive) {

            const found = this._lowercase.get(value.toLowerCase());
            if (found) {
                return { value: found };
            }
        }

        if (!this._refs.size &&
            typeof value !== 'object') {

            return false;
        }

        // Objects

        if (typeof value === 'object') {
            for (const item of this._values) {
                if (DeepEqual(item, value)) {
                    return { value: item };
                }
            }
        }

        // References

        if (state) {
            for (const ref of this._refs) {
                const resolved = ref.resolve(value, state, prefs, null, { in: true });
                if (resolved === undefined) {
                    continue;
                }

                const items = !ref.in || typeof resolved !== 'object'
                    ? [resolved]
                    : Array.isArray(resolved) ? resolved : Object.keys(resolved);

                for (const item of items) {
                    if (typeof item !== typeof value) {
                        continue;
                    }

                    if (insensitive &&
                        value &&
                        typeof value === 'string') {

                        if (item.toLowerCase() === value.toLowerCase()) {
                            return { value: item, ref };
                        }
                    }
                    else {
                        if (DeepEqual(item, value)) {
                            return { value: item, ref };
                        }
                    }
                }
            }
        }

        return false;
    }

    override() {

        this._override = true;
    }

    values(options) {

        if (options &&
            options.display) {

            const values = [];

            for (const item of [...this._values, ...this._refs]) {
                if (item !== undefined) {
                    values.push(item);
                }
            }

            return values;
        }

        return Array.from([...this._values, ...this._refs]);
    }

    clone() {

        const set = new internals.Values(this._values, this._refs);
        set._override = this._override;
        return set;
    }

    concat(source) {

        Assert(!source._override, 'Cannot concat override set of values');

        const set = new internals.Values([...this._values, ...source._values], [...this._refs, ...source._refs]);
        set._override = this._override;
        return set;
    }

    describe() {

        const normalized = [];

        if (this._override) {
            normalized.push({ override: true });
        }

        for (const value of this._values.values()) {
            normalized.push(value && typeof value === 'object' ? { value } : value);
        }

        for (const value of this._refs.values()) {
            normalized.push(value.describe());
        }

        return normalized;
    }
};


internals.Values.prototype[Common.symbols.values] = true;


// Aliases

internals.Values.prototype.slice = internals.Values.prototype.clone;


// Helpers

internals.lowercases = function (from) {

    const map = new Map();

    if (from) {
        for (const value of from) {
            if (typeof value === 'string') {
                map.set(value.toLowerCase(), value);
            }
        }
    }

    return map;
};