'use strict';

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

const Pkg = require('../package.json');

let Messages;
let Schemas;


const internals = {
    isoDate: /^(?:[-+]\d{2})?(?:\d{4}(?!\d{2}\b))(?:(-?)(?:(?:0[1-9]|1[0-2])(?:\1(?:[12]\d|0[1-9]|3[01]))?|W(?:[0-4]\d|5[0-2])(?:-?[1-7])?|(?:00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[1-6])))(?![T]$|[T][\d]+Z$)(?:[T\s](?:(?:(?:[01]\d|2[0-3])(?:(:?)[0-5]\d)?|24\:?00)(?:[.,]\d+(?!:))?)(?:\2[0-5]\d(?:[.,]\d+)?)?(?:[Z]|(?:[+-])(?:[01]\d|2[0-3])(?::?[0-5]\d)?)?)?)?$/
};


exports.version = Pkg.version;


exports.defaults = {
    abortEarly: true,
    allowUnknown: false,
    artifacts: false,
    cache: true,
    context: null,
    convert: true,
    dateFormat: 'iso',
    errors: {
        escapeHtml: false,
        label: 'path',
        language: null,
        render: true,
        stack: false,
        wrap: {
            label: '"',
            array: '[]'
        }
    },
    externals: true,
    messages: {},
    nonEnumerables: false,
    noDefaults: false,
    presence: 'optional',
    skipFunctions: false,
    stripUnknown: false,
    warnings: false
};


exports.symbols = {
    any: Symbol.for('@hapi/joi/schema'),            // Used to internally identify any-based types (shared with other joi versions)
    arraySingle: Symbol('arraySingle'),
    deepDefault: Symbol('deepDefault'),
    errors: Symbol('errors'),
    literal: Symbol('literal'),
    override: Symbol('override'),
    parent: Symbol('parent'),
    prefs: Symbol('prefs'),
    ref: Symbol('ref'),
    template: Symbol('template'),
    values: Symbol('values')
};


exports.assertOptions = function (options, keys, name = 'Options') {

    Assert(options && typeof options === 'object' && !Array.isArray(options), 'Options must be of type object');
    const unknownKeys = Object.keys(options).filter((k) => !keys.includes(k));
    Assert(unknownKeys.length === 0, `${name} contain unknown keys: ${unknownKeys}`);
};


exports.checkPreferences = function (prefs) {

    Schemas = Schemas || require('./schemas');

    const result = Schemas.preferences.validate(prefs);

    if (result.error) {
        throw new AssertError([result.error.details[0].message]);
    }
};


exports.compare = function (a, b, operator) {

    switch (operator) {
        case '=': return a === b;
        case '>': return a > b;
        case '<': return a < b;
        case '>=': return a >= b;
        case '<=': return a <= b;
    }
};


exports.default = function (value, defaultValue) {

    return value === undefined ? defaultValue : value;
};


exports.isIsoDate = function (date) {

    return internals.isoDate.test(date);
};


exports.isNumber = function (value) {

    return typeof value === 'number' && !isNaN(value);
};


exports.isResolvable = function (obj) {

    if (!obj) {
        return false;
    }

    return obj[exports.symbols.ref] || obj[exports.symbols.template];
};


exports.isSchema = function (schema, options = {}) {

    const any = schema && schema[exports.symbols.any];
    if (!any) {
        return false;
    }

    Assert(options.legacy || any.version === exports.version, 'Cannot mix different versions of joi schemas');
    return true;
};


exports.isValues = function (obj) {

    return obj[exports.symbols.values];
};


exports.limit = function (value) {

    return Number.isSafeInteger(value) && value >= 0;
};


exports.preferences = function (target, source) {

    Messages = Messages || require('./messages');

    target = target || {};
    source = source || {};

    const merged = Object.assign({}, target, source);
    if (source.errors &&
        target.errors) {

        merged.errors = Object.assign({}, target.errors, source.errors);
        merged.errors.wrap = Object.assign({}, target.errors.wrap, source.errors.wrap);
    }

    if (source.messages) {
        merged.messages = Messages.compile(source.messages, target.messages);
    }

    delete merged[exports.symbols.prefs];
    return merged;
};


exports.tryWithPath = function (fn, key, options = {}) {

    try {
        return fn();
    }
    catch (err) {
        if (err.path !== undefined) {
            err.path = key + '.' + err.path;
        }
        else {
            err.path = key;
        }

        if (options.append) {
            err.message = `${err.message} (${err.path})`;
        }

        throw err;
    }
};


exports.validateArg = function (value, label, { assert, message }) {

    if (exports.isSchema(assert)) {
        const result = assert.validate(value);
        if (!result.error) {
            return;
        }

        return result.error.message;
    }
    else if (!assert(value)) {
        return label ? `${label} ${message}` : message;
    }
};


exports.verifyFlat = function (args, method) {

    for (const arg of args) {
        Assert(!Array.isArray(arg), 'Method no longer accepts array arguments:', method);
    }
};
