'use strict';

// Load modules

const Any = require('../any');
const Ref = require('../../ref');
const Hoek = require('hoek');


// Declare internals

const internals = {};

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)?)?)?)?$/;
internals.invalidDate = new Date('');
internals.isIsoDate = (() => {

    const isoString = internals.isoDate.toString();

    return (date) => {

        return date && (date.toString() === isoString);
    };
})();

internals.Date = class extends Any {

    constructor() {

        super();
        this._type = 'date';
    }

    _base(value, state, options) {

        const result = {
            value: (options.convert && internals.Date.toDate(value, this._flags.format, this._flags.timestamp, this._flags.multiplier)) || value
        };

        if (result.value instanceof Date && !isNaN(result.value.getTime())) {
            result.errors = null;
        }
        else if (!options.convert) {
            result.errors = this.createError('date.strict', { value }, state, options);
        }
        else {
            let type;
            if (internals.isIsoDate(this._flags.format)) {
                type = 'isoDate';
            }
            else if (this._flags.timestamp) {
                type = `timestamp.${this._flags.timestamp}`;
            }
            else {
                type = 'base';
            }

            result.errors = this.createError(`date.${type}`, { value }, state, options);
        }

        return result;
    }

    static toDate(value, format, timestamp, multiplier) {

        if (value instanceof Date) {
            return value;
        }

        if (typeof value === 'string' ||
            (typeof value === 'number' && !isNaN(value) && isFinite(value))) {

            const isIsoDate = format && internals.isIsoDate(format);
            if (!isIsoDate &&
                typeof value === 'string' &&
                /^[+-]?\d+(\.\d+)?$/.test(value)) {

                value = parseFloat(value);
            }

            let date;
            if (isIsoDate) {
                date = format.test(value) ? new Date(value.toString()) : internals.invalidDate;
            }
            else if (timestamp) {
                date = /^\s*$/.test(value) ? internals.invalidDate : new Date(value * multiplier);
            }
            else {
                date = new Date(value);
            }

            if (!isNaN(date.getTime())) {
                return date;
            }
        }

        return null;
    }

    iso() {

        if (this._flags.format === internals.isoDate) {
            return this;
        }

        const obj = this.clone();
        obj._flags.format = internals.isoDate;
        return obj;
    }

    timestamp(type = 'javascript') {

        const allowed = ['javascript', 'unix'];
        Hoek.assert(allowed.includes(type), '"type" must be one of "' + allowed.join('", "') + '"');

        if (this._flags.timestamp === type) {
            return this;
        }

        const obj = this.clone();
        obj._flags.timestamp = type;
        obj._flags.multiplier = type === 'unix' ? 1000 : 1;
        return obj;
    }

    _isIsoDate(value) {

        return internals.isoDate.test(value);
    }

};

internals.compare = function (type, compare) {

    return function (date) {

        const isNow = date === 'now';
        const isRef = Ref.isRef(date);

        if (!isNow && !isRef) {
            date = internals.Date.toDate(date);
        }

        Hoek.assert(date, 'Invalid date format');

        return this._test(type, date, function (value, state, options) {

            let compareTo;
            if (isNow) {
                compareTo = Date.now();
            }
            else if (isRef) {
                const refValue = date(state.reference || state.parent, options);
                compareTo = internals.Date.toDate(refValue);

                if (!compareTo) {
                    return this.createError('date.ref', { ref: date, value: refValue }, state, options);
                }

                compareTo = compareTo.getTime();
            }
            else {
                compareTo = date.getTime();
            }

            if (compare(value.getTime(), compareTo)) {
                return value;
            }

            return this.createError('date.' + type, { limit: new Date(compareTo), value }, state, options);
        });
    };
};


internals.Date.prototype.min = internals.compare('min', (value, date) => value >= date);
internals.Date.prototype.max = internals.compare('max', (value, date) => value <= date);
internals.Date.prototype.greater = internals.compare('greater', (value, date) => value > date);
internals.Date.prototype.less = internals.compare('less', (value, date) => value < date);


module.exports = new internals.Date();
