'use strict';

const Clone = require('@hapi/hoek/lib/clone');
const Reach = require('@hapi/hoek/lib/reach');

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


const internals = {
    value: Symbol('value')
};


module.exports = internals.State = class {

    constructor(path, ancestors, state) {

        this.path = path;
        this.ancestors = ancestors;                 // [parent, ..., root]

        this.mainstay = state.mainstay;
        this.schemas = state.schemas;               // [current, ..., root]
        this.debug = null;
    }

    localize(path, ancestors = null, schema = null) {

        const state = new internals.State(path, ancestors, this);

        if (schema &&
            state.schemas) {

            state.schemas = [internals.schemas(schema), ...state.schemas];
        }

        return state;
    }

    nest(schema, debug) {

        const state = new internals.State(this.path, this.ancestors, this);
        state.schemas = state.schemas && [internals.schemas(schema), ...state.schemas];
        state.debug = debug;
        return state;
    }

    shadow(value, reason) {

        this.mainstay.shadow = this.mainstay.shadow || new internals.Shadow();
        this.mainstay.shadow.set(this.path, value, reason);
    }

    snapshot() {

        if (this.mainstay.shadow) {
            this._snapshot = Clone(this.mainstay.shadow.node(this.path));
        }

        this.mainstay.snapshot();
    }

    restore() {

        if (this.mainstay.shadow) {
            this.mainstay.shadow.override(this.path, this._snapshot);
            this._snapshot = undefined;
        }

        this.mainstay.restore();
    }

    commit() {

        if (this.mainstay.shadow) {
            this.mainstay.shadow.override(this.path, this._snapshot);
            this._snapshot = undefined;
        }

        this.mainstay.commit();
    }
};


internals.schemas = function (schema) {

    if (Common.isSchema(schema)) {
        return { schema };
    }

    return schema;
};


internals.Shadow = class {

    constructor() {

        this._values = null;
    }

    set(path, value, reason) {

        if (!path.length) {                                     // No need to store root value
            return;
        }

        if (reason === 'strip' &&
            typeof path[path.length - 1] === 'number') {        // Cannot store stripped array values (due to shift)

            return;
        }

        this._values = this._values || new Map();

        let node = this._values;
        for (let i = 0; i < path.length; ++i) {
            const segment = path[i];
            let next = node.get(segment);
            if (!next) {
                next = new Map();
                node.set(segment, next);
            }

            node = next;
        }

        node[internals.value] = value;
    }

    get(path) {

        const node = this.node(path);
        if (node) {
            return node[internals.value];
        }
    }

    node(path) {

        if (!this._values) {
            return;
        }

        return Reach(this._values, path, { iterables: true });
    }

    override(path, node) {

        if (!this._values) {
            return;
        }

        const parents = path.slice(0, -1);
        const own = path[path.length - 1];
        const parent = Reach(this._values, parents, { iterables: true });

        if (node) {
            parent.set(own, node);
            return;
        }

        if (parent) {
            parent.delete(own);
        }
    }
};
