'use strict';

const Joi = require('./index');


const internals = {};


// Preferences

internals.wrap = Joi.string()
    .min(1)
    .max(2)
    .allow(false);


exports.preferences = Joi.object({
    allowUnknown: Joi.boolean(),
    abortEarly: Joi.boolean(),
    artifacts: Joi.boolean(),
    cache: Joi.boolean(),
    context: Joi.object(),
    convert: Joi.boolean(),
    dateFormat: Joi.valid('date', 'iso', 'string', 'time', 'utc'),
    debug: Joi.boolean(),
    errors: {
        escapeHtml: Joi.boolean(),
        label: Joi.valid('path', 'key', false),
        language: [
            Joi.string(),
            Joi.object().ref()
        ],
        render: Joi.boolean(),
        stack: Joi.boolean(),
        wrap: {
            label: internals.wrap,
            array: internals.wrap,
            string: internals.wrap
        }
    },
    externals: Joi.boolean(),
    messages: Joi.object(),
    noDefaults: Joi.boolean(),
    nonEnumerables: Joi.boolean(),
    presence: Joi.valid('required', 'optional', 'forbidden'),
    skipFunctions: Joi.boolean(),
    stripUnknown: Joi.object({
        arrays: Joi.boolean(),
        objects: Joi.boolean()
    })
        .or('arrays', 'objects')
        .allow(true, false),
    warnings: Joi.boolean()
})
    .strict();


// Extensions

internals.nameRx = /^[a-zA-Z0-9]\w*$/;


internals.rule = Joi.object({
    alias: Joi.array().items(Joi.string().pattern(internals.nameRx)).single(),
    args: Joi.array().items(
        Joi.string(),
        Joi.object({
            name: Joi.string().pattern(internals.nameRx).required(),
            ref: Joi.boolean(),
            assert: Joi.alternatives([
                Joi.function(),
                Joi.object().schema()
            ])
                .conditional('ref', { is: true, then: Joi.required() }),
            normalize: Joi.function(),
            message: Joi.string().when('assert', { is: Joi.function(), then: Joi.required() })
        })
    ),
    convert: Joi.boolean(),
    manifest: Joi.boolean(),
    method: Joi.function().allow(false),
    multi: Joi.boolean(),
    validate: Joi.function()
});


exports.extension = Joi.object({
    type: Joi.alternatives([
        Joi.string(),
        Joi.object().regex()
    ])
        .required(),
    args: Joi.function(),
    cast: Joi.object().pattern(internals.nameRx, Joi.object({
        from: Joi.function().maxArity(1).required(),
        to: Joi.function().minArity(1).maxArity(2).required()
    })),
    base: Joi.object().schema()
        .when('type', { is: Joi.object().regex(), then: Joi.forbidden() }),
    coerce: [
        Joi.function().maxArity(3),
        Joi.object({ method: Joi.function().maxArity(3).required(), from: Joi.array().items(Joi.string()).single() })
    ],
    flags: Joi.object().pattern(internals.nameRx, Joi.object({
        setter: Joi.string(),
        default: Joi.any()
    })),
    manifest: {
        build: Joi.function().arity(2)
    },
    messages: [Joi.object(), Joi.string()],
    modifiers: Joi.object().pattern(internals.nameRx, Joi.function().minArity(1).maxArity(2)),
    overrides: Joi.object().pattern(internals.nameRx, Joi.function()),
    prepare: Joi.function().maxArity(3),
    rebuild: Joi.function().arity(1),
    rules: Joi.object().pattern(internals.nameRx, internals.rule),
    terms: Joi.object().pattern(internals.nameRx, Joi.object({
        init: Joi.array().allow(null).required(),
        manifest: Joi.object().pattern(/.+/, [
            Joi.valid('schema', 'single'),
            Joi.object({
                mapped: Joi.object({
                    from: Joi.string().required(),
                    to: Joi.string().required()
                })
                    .required()
            })
        ])
    })),
    validate: Joi.function().maxArity(3)
})
    .strict();


exports.extensions = Joi.array().items(Joi.object(), Joi.function().arity(1)).strict();


// Manifest

internals.desc = {

    buffer: Joi.object({
        buffer: Joi.string()
    }),

    func: Joi.object({
        function: Joi.function().required(),
        options: {
            literal: true
        }
    }),

    override: Joi.object({
        override: true
    }),

    ref: Joi.object({
        ref: Joi.object({
            type: Joi.valid('value', 'global', 'local'),
            path: Joi.array().required(),
            separator: Joi.string().length(1).allow(false),
            ancestor: Joi.number().min(0).integer().allow('root'),
            map: Joi.array().items(Joi.array().length(2)).min(1),
            adjust: Joi.function(),
            iterables: Joi.boolean(),
            in: Joi.boolean(),
            render: Joi.boolean()
        })
            .required()
    }),

    regex: Joi.object({
        regex: Joi.string().min(3)
    }),

    special: Joi.object({
        special: Joi.valid('deep').required()
    }),

    template: Joi.object({
        template: Joi.string().required(),
        options: Joi.object()
    }),

    value: Joi.object({
        value: Joi.alternatives([Joi.object(), Joi.array()]).required()
    })
};


internals.desc.entity = Joi.alternatives([
    Joi.array().items(Joi.link('...')),
    Joi.boolean(),
    Joi.function(),
    Joi.number(),
    Joi.string(),
    internals.desc.buffer,
    internals.desc.func,
    internals.desc.ref,
    internals.desc.regex,
    internals.desc.special,
    internals.desc.template,
    internals.desc.value,
    Joi.link('/')
]);


internals.desc.values = Joi.array()
    .items(
        null,
        Joi.boolean(),
        Joi.function(),
        Joi.number().allow(Infinity, -Infinity),
        Joi.string().allow(''),
        Joi.symbol(),
        internals.desc.buffer,
        internals.desc.func,
        internals.desc.override,
        internals.desc.ref,
        internals.desc.regex,
        internals.desc.template,
        internals.desc.value
    );


internals.desc.messages = Joi.object()
    .pattern(/.+/, [
        Joi.string(),
        internals.desc.template,
        Joi.object().pattern(/.+/, [Joi.string(), internals.desc.template])
    ]);


exports.description = Joi.object({
    type: Joi.string().required(),
    flags: Joi.object({
        cast: Joi.string(),
        default: Joi.any(),
        description: Joi.string(),
        empty: Joi.link('/'),
        failover: internals.desc.entity,
        id: Joi.string(),
        label: Joi.string(),
        only: true,
        presence: ['optional', 'required', 'forbidden'],
        result: ['raw', 'strip'],
        strip: Joi.boolean(),
        unit: Joi.string()
    })
        .unknown(),
    preferences: {
        allowUnknown: Joi.boolean(),
        abortEarly: Joi.boolean(),
        artifacts: Joi.boolean(),
        cache: Joi.boolean(),
        convert: Joi.boolean(),
        dateFormat: ['date', 'iso', 'string', 'time', 'utc'],
        errors: {
            escapeHtml: Joi.boolean(),
            label: ['path', 'key'],
            language: [
                Joi.string(),
                internals.desc.ref
            ],
            wrap: {
                label: internals.wrap,
                array: internals.wrap
            }
        },
        externals: Joi.boolean(),
        messages: internals.desc.messages,
        noDefaults: Joi.boolean(),
        nonEnumerables: Joi.boolean(),
        presence: ['required', 'optional', 'forbidden'],
        skipFunctions: Joi.boolean(),
        stripUnknown: Joi.object({
            arrays: Joi.boolean(),
            objects: Joi.boolean()
        })
            .or('arrays', 'objects')
            .allow(true, false),
        warnings: Joi.boolean()
    },
    allow: internals.desc.values,
    invalid: internals.desc.values,
    rules: Joi.array().min(1).items({
        name: Joi.string().required(),
        args: Joi.object().min(1),
        keep: Joi.boolean(),
        message: [
            Joi.string(),
            internals.desc.messages
        ],
        warn: Joi.boolean()
    }),

    // Terms

    keys: Joi.object().pattern(/.*/, Joi.link('/')),
    link: internals.desc.ref
})
    .pattern(/^[a-z]\w*$/, Joi.any());