'use strict';

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

const Common = require('./common');
const Compile = require('./compile');
const Errors = require('./errors');
const Ref = require('./ref');


const internals = {
    types: {
        alternatives: require('./types/alternatives'),
        any: require('./types/any'),
        array: require('./types/array'),
        boolean: require('./types/boolean'),
        date: require('./types/date'),
        function: require('./types/function'),
        link: require('./types/link'),
        number: require('./types/number'),
        object: require('./types/object'),
        string: require('./types/string'),
        symbol: require('./types/symbol')
    },
    aliases: {
        alt: 'alternatives',
        bool: 'boolean',
        func: 'function'
    }
};


if (Buffer) {                                                           // $lab:coverage:ignore$
    internals.types.binary = require('./types/binary');
}


internals.root = function () {

    const root = {
        _types: new Set(Object.keys(internals.types))
    };

    // Types

    for (const type of root._types) {
        root[type] = function (...args) {

            Assert(!args.length || ['alternatives', 'link', 'object'].includes(type), 'The', type, 'type does not allow arguments');
            return internals.generate(this, internals.types[type], args);
        };
    }

    // Shortcuts

    for (const method of ['allow', 'custom', 'disallow', 'equal', 'exist', 'forbidden', 'invalid', 'not', 'only', 'optional', 'options', 'prefs', 'preferences', 'required', 'strip', 'valid', 'when']) {
        root[method] = function (...args) {

            return this.any()[method](...args);
        };
    }

    // Methods

    Object.assign(root, internals.methods);

    // Aliases

    for (const alias in internals.aliases) {
        const target = internals.aliases[alias];
        root[alias] = root[target];
    }

    return root;
};


internals.methods = {

    ValidationError: Errors.ValidationError,
    version: Common.version,

    assert(value, schema, ...args /* [message], [options] */) {

        internals.assert(value, schema, true, args);
    },

    attempt(value, schema, ...args /* [message], [options] */) {

        return internals.assert(value, schema, false, args);
    },

    checkPreferences(prefs) {

        Common.checkPreferences(prefs);
    },

    compile(schema, options) {

        return Compile.compile(this, schema, options);
    },

    isError: Errors.ValidationError.isError,
    isRef: Ref.isRef,
    isSchema: Common.isSchema,

    in(...args) {

        return Ref.in(...args);
    },

    override: Common.symbols.override,

    ref(...args) {

        return Ref.create(...args);
    }
};


// Helpers

internals.assert = function (value, schema, annotate, args /* [message], [options] */) {

    const message = args[0] instanceof Error || typeof args[0] === 'string' ? args[0] : null;
    const options = message ? args[1] : args[0];
    const result = schema.validate(value, Common.preferences({ errors: { stack: true } }, options || {}));

    let error = result.error;
    if (!error) {
        return result.value;
    }

    if (message instanceof Error) {
        throw message;
    }

    const display = annotate && typeof error.annotate === 'function' ? error.annotate() : error.message;

    if (error instanceof Errors.ValidationError === false) {
        error = Clone(error);
    }

    error.message = message ? `${message} ${display}` : display;
    throw error;
};


internals.generate = function (root, schema, args) {

    Assert(root, 'Must be invoked on a Joi instance.');

    schema.$_root = root;

    if (!schema._definition.args ||
        !args.length) {

        return schema;
    }

    return schema._definition.args(schema, ...args);
};


module.exports = internals.root();
