'use strict';

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

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


const internals = {};



exports.Ids = internals.Ids = class {

    constructor() {

        this._byId = new Map();
        this._byKey = new Map();
        this._schemaChain = false;
    }

    clone() {

        const clone = new internals.Ids();
        clone._byId = new Map(this._byId);
        clone._byKey = new Map(this._byKey);
        clone._schemaChain = this._schemaChain;
        return clone;
    }

    concat(source) {

        if (source._schemaChain) {
            this._schemaChain = true;
        }

        for (const [id, value] of source._byId.entries()) {
            Assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id);
            this._byId.set(id, value);
        }

        for (const [key, value] of source._byKey.entries()) {
            Assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key);
            this._byKey.set(key, value);
        }
    }

    fork(path, adjuster, root) {

        const chain = this._collect(path);
        chain.push({ schema: root });
        const tail = chain.shift();
        let adjusted = { id: tail.id, schema: adjuster(tail.schema) };

        Assert(Common.isSchema(adjusted.schema), 'adjuster function failed to return a joi schema type');

        for (const node of chain) {
            adjusted = { id: node.id, schema: internals.fork(node.schema, adjusted.id, adjusted.schema) };
        }

        return adjusted.schema;
    }

    labels(path, behind = []) {

        const current = path[0];
        const node = this._get(current);
        if (!node) {
            return [...behind, ...path].join('.');
        }

        const forward = path.slice(1);
        behind = [...behind, node.schema._flags.label || current];
        if (!forward.length) {
            return behind.join('.');
        }

        return node.schema._ids.labels(forward, behind);
    }

    reach(path, behind = []) {

        const current = path[0];
        const node = this._get(current);
        Assert(node, 'Schema does not contain path', [...behind, ...path].join('.'));

        const forward = path.slice(1);
        if (!forward.length) {
            return node.schema;
        }

        return node.schema._ids.reach(forward, [...behind, current]);
    }

    register(schema, { key } = {}) {

        if (!schema ||
            !Common.isSchema(schema)) {

            return;
        }

        if (schema.$_property('schemaChain') ||
            schema._ids._schemaChain) {

            this._schemaChain = true;
        }

        const id = schema._flags.id;
        if (id) {
            const existing = this._byId.get(id);
            Assert(!existing || existing.schema === schema, 'Cannot add different schemas with the same id:', id);
            Assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id);

            this._byId.set(id, { schema, id });
        }

        if (key) {
            Assert(!this._byKey.has(key), 'Schema already contains key:', key);
            Assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key);

            this._byKey.set(key, { schema, id: key });
        }
    }

    reset() {

        this._byId = new Map();
        this._byKey = new Map();
        this._schemaChain = false;
    }

    _collect(path, behind = [], nodes = []) {

        const current = path[0];
        const node = this._get(current);
        Assert(node, 'Schema does not contain path', [...behind, ...path].join('.'));

        nodes = [node, ...nodes];

        const forward = path.slice(1);
        if (!forward.length) {
            return nodes;
        }

        return node.schema._ids._collect(forward, [...behind, current], nodes);
    }

    _get(id) {

        return this._byId.get(id) || this._byKey.get(id);
    }
};


internals.fork = function (schema, id, replacement) {

    const each = (item, { key }) => {

        if (id === (item._flags.id || key)) {
            return replacement;
        }
    };

    const obj = exports.schema(schema, { each, ref: false });
    return obj ? obj.$_mutateRebuild() : schema;
};


exports.schema = function (schema, options) {

    let obj;

    for (const name in schema._flags) {
        if (name[0] === '_') {
            continue;
        }

        const result = internals.scan(schema._flags[name], { source: 'flags', name }, options);
        if (result !== undefined) {
            obj = obj || schema.clone();
            obj._flags[name] = result;
        }
    }

    for (let i = 0; i < schema._rules.length; ++i) {
        const rule = schema._rules[i];
        const result = internals.scan(rule.args, { source: 'rules', name: rule.name }, options);
        if (result !== undefined) {
            obj = obj || schema.clone();
            const clone = Object.assign({}, rule);
            clone.args = result;
            obj._rules[i] = clone;

            const existingUnique = obj._singleRules.get(rule.name);
            if (existingUnique === rule) {
                obj._singleRules.set(rule.name, clone);
            }
        }
    }

    for (const name in schema.$_terms) {
        if (name[0] === '_') {
            continue;
        }

        const result = internals.scan(schema.$_terms[name], { source: 'terms', name }, options);
        if (result !== undefined) {
            obj = obj || schema.clone();
            obj.$_terms[name] = result;
        }
    }

    return obj;
};


internals.scan = function (item, source, options, _path, _key) {

    const path = _path || [];

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

        return;
    }

    let clone;

    if (Array.isArray(item)) {
        for (let i = 0; i < item.length; ++i) {
            const key = source.source === 'terms' && source.name === 'keys' && item[i].key;
            const result = internals.scan(item[i], source, options, [i, ...path], key);
            if (result !== undefined) {
                clone = clone || item.slice();
                clone[i] = result;
            }
        }

        return clone;
    }

    if (options.schema !== false && Common.isSchema(item) ||
        options.ref !== false && Ref.isRef(item)) {

        const result = options.each(item, { ...source, path, key: _key });
        if (result === item) {
            return;
        }

        return result;
    }

    for (const key in item) {
        if (key[0] === '_') {
            continue;
        }

        const result = internals.scan(item[key], source, options, [key, ...path], _key);
        if (result !== undefined) {
            clone = clone || Object.assign({}, item);
            clone[key] = result;
        }
    }

    return clone;
};