'use strict';

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

const Any = require('./any');
const Common = require('../common');
const Compile = require('../compile');
const Errors = require('../errors');


const internals = {};


module.exports = Any.extend({

    type: 'link',

    properties: {
        schemaChain: true
    },

    terms: {

        link: { init: null, manifest: 'single', register: false }
    },

    args(schema, ref) {

        return schema.ref(ref);
    },

    validate(value, { schema, state, prefs, error }) {

        Assert(schema.$_terms.link, 'Uninitialized link schema');

        const linked = internals.generate(schema, value, state, prefs);
        const ref = schema.$_terms.link[0].ref;

        try {
            return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs);
        }
        catch (err) {
            /* $lab:coverage:off$ */
            if (!(err instanceof RangeError)) {
                throw err;
            }
            /* $lab:coverage:on$ */

            return { value, errors: error('link.depth') };
        }
    },

    generate(schema, value, state, prefs) {

        return internals.generate(schema, value, state, prefs);
    },

    rules: {

        ref: {
            method(ref) {

                Assert(!this.$_terms.link, 'Cannot reinitialize schema');

                ref = Compile.ref(ref);

                Assert(ref.type === 'value' || ref.type === 'local', 'Invalid reference type:', ref.type);
                Assert(ref.type === 'local' || ref.ancestor === 'root' || ref.ancestor > 0, 'Link cannot reference itself');

                const obj = this.clone();
                obj.$_terms.link = [{ ref }];
                return obj;
            }
        },

        relative: {
            method(enabled = true) {

                return this.$_setFlag('relative', enabled);
            }
        }
    },

    overrides: {

        concat(source) {

            Assert(this.$_terms.link, 'Uninitialized link schema');
            Assert(Common.isSchema(source), 'Invalid schema object');
            Assert(source.type !== 'link', 'Cannot merge type link with another link');

            const obj = this.clone();

            if (!obj.$_terms.whens) {
                obj.$_terms.whens = [];
            }

            obj.$_terms.whens.push({ concat: source });
            return obj.$_mutateRebuild();
        }
    },

    messages: {
        'link.depth': '{{#label}} exceeds maximum recursion depth supported by the runtime'
    },

    manifest: {

        build(obj, desc) {

            Assert(desc.link, 'Invalid link description missing link');
            return obj.ref(desc.link);
        }
    }
});


// Helpers

internals.generate = function (schema, value, state, prefs) {

    let linked = state.mainstay.links.get(schema);
    if (linked) {
        return linked._generate(value, state, prefs).schema;
    }

    const ref = schema.$_terms.link[0].ref;
    const { perspective, path } = internals.perspective(ref, state);
    internals.assert(perspective, 'which is outside of schema boundaries', ref, schema, state, prefs);

    try {
        linked = path.length ? perspective.$_reach(path) : perspective;
    }
    catch (ignoreErr) {
        internals.assert(false, 'to non-existing schema', ref, schema, state, prefs);
    }

    internals.assert(linked.type !== 'link', 'which is another link', ref, schema, state, prefs);

    if (!schema._flags.relative) {
        state.mainstay.links.set(schema, linked);
    }

    return linked._generate(value, state, prefs).schema;
};


internals.perspective = function (ref, state) {

    if (ref.type === 'local') {
        for (const { schema, key } of state.schemas) {                              // From parent to root
            const id = schema._flags.id || key;
            if (id === ref.path[0]) {
                return { perspective: schema, path: ref.path.slice(1) };
            }

            if (schema.$_terms.shared) {
                for (const shared of schema.$_terms.shared) {
                    if (shared._flags.id === ref.path[0]) {
                        return { perspective: shared, path: ref.path.slice(1) };
                    }
                }
            }
        }

        return { perspective: null, path: null };
    }

    if (ref.ancestor === 'root') {
        return { perspective: state.schemas[state.schemas.length - 1].schema, path: ref.path };
    }

    return { perspective: state.schemas[ref.ancestor] && state.schemas[ref.ancestor].schema, path: ref.path };
};


internals.assert = function (condition, message, ref, schema, state, prefs) {

    if (condition) {                // Manual check to avoid generating error message on success
        return;
    }

    Assert(false, `"${Errors.label(schema._flags, state, prefs)}" contains link reference "${ref.display}" ${message}`);
};
