// Load modules

var Any = require('./any');
var Cast = require('./cast');
var Errors = require('./errors');
var Hoek = require('hoek');


// Declare internals

var internals = {};


internals.fastSplice = function (arr, i) {

    var il = arr.length;
    var pos = i;

    while (pos < il) {
        arr[pos++] = arr[pos];
    }

    --arr.length;
};


internals.Array = function () {

    Any.call(this);
    this._type = 'array';
    this._inner.items = [];
    this._inner.inclusions = [];
    this._inner.exclusions = [];
    this._inner.requireds = [];
    this._flags.sparse = false;
};

Hoek.inherits(internals.Array, Any);


internals.Array.prototype._base = function (value, state, options) {

    var result = {
        value: value
    };

    if (typeof value === 'string' &&
        options.convert) {

        try {
            var converted = JSON.parse(value);
            if (Array.isArray(converted)) {
                result.value = converted;
            }
        }
        catch (e) { }
    }

    var isArray = Array.isArray(result.value);
    var wasArray = isArray;
    if (options.convert && this._flags.single && !isArray) {
        result.value = [result.value];
        isArray = true;
    }

    if (!isArray) {
        result.errors = Errors.create('array.base', null, state, options);
        return result;
    }

    if (this._inner.inclusions.length ||
        this._inner.exclusions.length ||
        !this._flags.sparse) {

        // Clone the array so that we don't modify the original
        if (wasArray) {
            result.value = result.value.slice(0);
        }

        result.errors = internals.checkItems.call(this, result.value, wasArray, state, options);

        if (result.errors && wasArray && options.convert && this._flags.single) {
            // Attempt a 2nd pass by putting the array inside one.
            var previousErrors = result.errors;

            result.value = [result.value];
            result.errors = internals.checkItems.call(this, result.value, wasArray, state, options);

            if (result.errors) {
                // Restore previous errors and value since this didn't validate either.
                result.errors = previousErrors;
                result.value = result.value[0];
            }
        }
    }

    return result;
};


internals.checkItems = function (items, wasArray, state, options) {

    var errors = [];
    var errored;

    var requireds = this._inner.requireds.slice();
    var inclusions = this._inner.inclusions.concat(requireds);

    for (var v = 0, vl = items.length; v < vl; ++v) {
        errored = false;
        var item = items[v];
        var isValid = false;
        var localState = { key: v, path: (state.path ? state.path + '.' : '') + v, parent: items, reference: state.reference };
        var res;

        // Sparse

        if (!this._flags.sparse && item === undefined) {
            errors.push(Errors.create('array.sparse', null, { key: state.key, path: localState.path }, options));

            if (options.abortEarly) {
                return errors;
            }

            continue;
        }

        // Exclusions

        for (var i = 0, il = this._inner.exclusions.length; i < il; ++i) {
            res = this._inner.exclusions[i]._validate(item, localState, {});                // Not passing options to use defaults
            if (!res.errors) {
                errors.push(Errors.create(wasArray ? 'array.excludes' : 'array.excludesSingle', { pos: v, value: item }, { key: state.key, path: localState.path }, options));
                errored = true;

                if (options.abortEarly) {
                    return errors;
                }

                break;
            }
        }

        if (errored) {
            continue;
        }

        // Requireds

        var requiredChecks = [];
        for (i = 0, il = requireds.length; i < il; ++i) {
            res = requiredChecks[i] = requireds[i]._validate(item, localState, options);
            if (!res.errors) {
                items[v] = res.value;
                isValid = true;
                internals.fastSplice(requireds, i);
                --i;
                --il;
                break;
            }
        }

        if (isValid) {
            continue;
        }

        // Inclusions

        for (i = 0, il = inclusions.length; i < il; ++i) {
            var inclusion = inclusions[i];

            // Avoid re-running requireds that already didn't match in the previous loop
            var previousCheck = requireds.indexOf(inclusion);
            if (previousCheck !== -1) {
                res = requiredChecks[previousCheck];
            }
            else {
                res = inclusion._validate(item, localState, options);

                if (!res.errors) {
                    if (inclusion._flags.strip) {
                        internals.fastSplice(items, v);
                        --v;
                        --vl;
                    }
                    else {
                        items[v] = res.value;
                    }
                    isValid = true;
                    break;
                }
            }

            if (options.stripUnknown) {
                internals.fastSplice(items, v);
                --v;
                --vl;
                isValid = true;
                break;
            }

            // Return the actual error if only one inclusion defined
            if (il === 1) {
                errors.push(Errors.create(wasArray ? 'array.includesOne' : 'array.includesOneSingle', { pos: v, reason: res.errors, value: item }, { key: state.key, path: localState.path }, options));
                errored = true;

                if (options.abortEarly) {
                    return errors;
                }

                break;
            }
        }

        if (errored) {
            continue;
        }

        if (this._inner.inclusions.length && !isValid) {
            errors.push(Errors.create(wasArray ? 'array.includes' : 'array.includesSingle', { pos: v, value: item }, { key: state.key, path: localState.path }, options));

            if (options.abortEarly) {
                return errors;
            }
        }
    }

    if (requireds.length) {
        internals.fillMissedErrors(errors, requireds, state, options);
    }

    return errors.length ? errors : null;
};


internals.fillMissedErrors = function (errors, requireds, state, options) {

    var knownMisses = [];
    var unknownMisses = 0;
    for (var i = 0, il = requireds.length; i < il; ++i) {
        var label = Hoek.reach(requireds[i], '_settings.language.label');
        if (label) {
            knownMisses.push(label);
        }
        else {
            ++unknownMisses;
        }
    }

    if (knownMisses.length) {
        if (unknownMisses) {
            errors.push(Errors.create('array.includesRequiredBoth', { knownMisses: knownMisses, unknownMisses: unknownMisses }, { key: state.key, path: state.patk }, options));
        }
        else {
            errors.push(Errors.create('array.includesRequiredKnowns', { knownMisses: knownMisses }, { key: state.key, path: state.path }, options));
        }
    }
    else {
        errors.push(Errors.create('array.includesRequiredUnknowns', { unknownMisses: unknownMisses }, { key: state.key, path: state.path }, options));
    }
};


internals.Array.prototype.describe = function () {

    var description = Any.prototype.describe.call(this);

    if (this._inner.items.length) {
        description.items = [];

        for (var i = 0, il = this._inner.items.length; i < il; ++i) {
            description.items.push(this._inner.items[i].describe());
        }
    }

    return description;
};


internals.Array.prototype.items = function () {

    var obj = this.clone();

    Hoek.flatten(Array.prototype.slice.call(arguments)).forEach(function (type) {

        type = Cast.schema(type);
        obj._inner.items.push(type);

        if (type._flags.presence === 'required') {
            obj._inner.requireds.push(type);
        }
        else if (type._flags.presence === 'forbidden') {
            obj._inner.exclusions.push(type.optional());
        }
        else {
            obj._inner.inclusions.push(type);
        }
    });

    return obj;
};


internals.Array.prototype.min = function (limit) {

    Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer');

    return this._test('min', limit, function (value, state, options) {

        if (value.length >= limit) {
            return null;
        }

        return Errors.create('array.min', { limit: limit, value: value }, state, options);
    });
};


internals.Array.prototype.max = function (limit) {

    Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer');

    return this._test('max', limit, function (value, state, options) {

        if (value.length <= limit) {
            return null;
        }

        return Errors.create('array.max', { limit: limit, value: value }, state, options);
    });
};


internals.Array.prototype.length = function (limit) {

    Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer');

    return this._test('length', limit, function (value, state, options) {

        if (value.length === limit) {
            return null;
        }

        return Errors.create('array.length', { limit: limit, value: value }, state, options);
    });
};


internals.Array.prototype.unique = function () {

    return this._test('unique', undefined, function (value, state, options) {

        var found = {
            string: {},
            number: {},
            undefined: {},
            boolean: {},
            object: [],
            function: []
        };

        for (var i = 0, il = value.length; i < il; ++i) {
            var item = value[i];
            var type = typeof item;
            var records = found[type];

            // All available types are supported, so it's not possible to reach 100% coverage without ignoring this line.
            // I still want to keep the test for future js versions with new types (eg. Symbol).
            if (/* $lab:coverage:off$ */ records /* $lab:coverage:on$ */) {
                if (Array.isArray(records)) {
                    for (var r = 0, rl = records.length; r < rl; ++r) {
                        if (Hoek.deepEqual(records[r], item)) {
                            return Errors.create('array.unique', { pos: i, value: item }, state, options);
                        }
                    }

                    records.push(item);
                }
                else {
                    if (records[item]) {
                        return Errors.create('array.unique', { pos: i, value: item }, state, options);
                    }

                    records[item] = true;
                }
            }
        }
    });
};


internals.Array.prototype.sparse = function (enabled) {

    var obj = this.clone();
    obj._flags.sparse = enabled === undefined ? true : !!enabled;
    return obj;
};


internals.Array.prototype.single = function (enabled) {

    var obj = this.clone();
    obj._flags.single = enabled === undefined ? true : !!enabled;
    return obj;
};


module.exports = new internals.Array();
