/*	Date
	Date object
	MONTHS are 1-BASED, not 0-BASED (different from Javascript date objects)
================================================== */
import { TLClass } from "../core/TLClass"
import { Language } from "../language/Language"
import TLError from "../core/TLError"

import { mergeData, trim, trace } from "../core/Util"

//
// Class for human dates
//

export const SCALES = [
    // ( name, units_per_tick, flooring function )
    ["millisecond", 1, function(d) {}],
    [
        "second",
        1000,
        function(d) {
            d.setMilliseconds(0);
        }
    ],
    [
        "minute",
        1000 * 60,
        function(d) {
            d.setSeconds(0);
        }
    ],
    [
        "hour",
        1000 * 60 * 60,
        function(d) {
            d.setMinutes(0);
        }
    ],
    [
        "day",
        1000 * 60 * 60 * 24,
        function(d) {
            d.setHours(0);
        }
    ],
    [
        "month",
        1000 * 60 * 60 * 24 * 30,
        function(d) {
            d.setDate(1);
        }
    ],
    [
        "year",
        1000 * 60 * 60 * 24 * 365,
        function(d) {
            d.setMonth(0);
        }
    ],
    [
        "decade",
        1000 * 60 * 60 * 24 * 365 * 10,
        function(d) {
            var real_year = d.getFullYear();
            d.setFullYear(real_year - (real_year % 10));
        }
    ],
    [
        "century",
        1000 * 60 * 60 * 24 * 365 * 100,
        function(d) {
            var real_year = d.getFullYear();
            d.setFullYear(real_year - (real_year % 100));
        }
    ],
    [
        "millennium",
        1000 * 60 * 60 * 24 * 365 * 1000,
        function(d) {
            var real_year = d.getFullYear();
            d.setFullYear(real_year - (real_year % 1000));
        }
    ]
]

// Date parts from highest to lowest precision
const DATE_PARTS = [
    "millisecond",
    "second",
    "minute",
    "hour",
    "day",
    "month",
    "year"
];

const ISO8601_SHORT_PATTERN = /^([\+-]?\d+?)(-\d{2}?)?(-\d{2}?)?$/;

// regex below from
// http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
const ISO8601_PATTERN = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([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\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;

/* For now, rather than extract parts from regexp, lets trust the browser.
 * Famous last words...
 * What about UTC vs local time?
 * see also http://stackoverflow.com/questions/10005374/ecmascript-5-date-parse-results-for-iso-8601-test-cases
 */
function parseISODate(str) {
    var d = new Date(str); // this is a true JavaScript date, not a TLDate
    if (isNaN(d)) {
        throw new TLError("invalid_date_err", str);
    }
    return {
        year: d.getFullYear(),
        month: d.getMonth() + 1,
        day: d.getDate(),
        hour: d.getHours(),
        minute: d.getMinutes(),
        second: d.getSeconds(),
        millisecond: d.getMilliseconds()
    };
};


const BEST_DATEFORMATS = {
    base: {
        millisecond: "time_short",
        second: "time",
        minute: "time_no_seconds_small_date",
        hour: "time_no_seconds_small_date",
        day: "full",
        month: "month",
        year: "year",
        decade: "year",
        century: "year",
        millennium: "year",
        age: "fallback",
        epoch: "fallback",
        era: "fallback",
        eon: "fallback",
        eon2: "fallback"
    },

    short: {
        millisecond: "time_short",
        second: "time_short",
        minute: "time_no_seconds_short",
        hour: "time_no_minutes_short",
        day: "full_short",
        month: "month_short",
        year: "year",
        decade: "year",
        century: "year",
        millennium: "year",
        age: "fallback",
        epoch: "fallback",
        era: "fallback",
        eon: "fallback",
        eon2: "fallback"
    }
};

export const TLDate = TLClass.extend({
    // @data = ms, JS Date object, or JS dictionary with date properties
    initialize: function(data, format, format_short) {
        if (typeof data == "number") {
            this.data = {
                format: "yyyy mmmm",
                date_obj: new Date(data)
            };
        } else if (Date == data.constructor) {
            this.data = {
                format: "yyyy mmmm",
                date_obj: data
            };
        } else {
            this.data = JSON.parse(JSON.stringify(data)); // clone don't use by reference.
            this._createDateObj();
        }

        if (data.format && !format) {
            format = data.format
        }
        this._setFormat(format, format_short);
    },

    setDateFormat: function(format) {
        this.data.format = format;
    },
    /**
     * Return a string representation of this date. If this date has been created with a `display_date` property,
     * that value is always returned, regardless of arguments to the method invocation. Otherwise,
     * the given `Language` is asked to create a string representation based on this Date's data, passing through the provided
     * `format` String, or, if that is undefined, this date's default format string.
     * @param {Language} language
     * @param {String} format
     * @returns {String} formattedDate
     */
    getDisplayDate: function(language, format) {
        if (this.data.display_date) {
            return this.data.display_date;
        }
        if (!language) {
            language = Language.fallback;
        }
        if (language.constructor != Language) {
            trace(
                "First argument to getDisplayDate must be type Language"
            );
            language = Language.fallback;
        }

        var format_key = format || this.data.format;
        return language.formatDate(this.data.date_obj, format_key);
    },

    getMillisecond: function() {
        return this.getTime();
    },

    getTime: function() {
        return this.data.date_obj.getTime();
    },

    isBefore: function(other_date) {
        if (!this.data.date_obj.constructor ==
            other_date.data.date_obj.constructor
        ) {
            throw new TLError("date_compare_err"); // but should be able to compare 'cosmological scale' dates once we get to that...
        }
        if ("isBefore" in this.data.date_obj) {
            return this.data.date_obj["isBefore"](
                other_date.data.date_obj
            );
        }
        return this.data.date_obj < other_date.data.date_obj;
    },

    isAfter: function(other_date) {
        if (!this.data.date_obj.constructor ==
            other_date.data.date_obj.constructor
        ) {
            throw new TLError("date_compare_err"); // but should be able to compare 'cosmological scale' dates once we get to that...
        }
        if ("isAfter" in this.data.date_obj) {
            return this.data.date_obj["isAfter"](
                other_date.data.date_obj
            );
        }
        return this.data.date_obj > other_date.data.date_obj;
    },

    // Return a new TLDate which has been 'floored' at the given scale.
    // @scale = string value from SCALES
    floor: function(scale) {
        var d = new Date(this.data.date_obj.getTime());
        for (var i = 0; i < SCALES.length; i++) {
            // for JS dates, we iteratively apply flooring functions
            SCALES[i][2](d);
            if (SCALES[i][0] == scale) return new TLDate(d);
        }

        throw new TLError("invalid_scale_err", scale);
    },

    /*	Private Methods
	================================================== */

    _getDateData: function() {
        var _date = {
            year: 0,
            month: 1, // stupid JS dates
            day: 1,
            hour: 0,
            minute: 0,
            second: 0,
            millisecond: 0
        };

        // Merge data
        mergeData(_date, this.data);

        // Make strings into numbers
        for (var ix in DATE_PARTS) {
            var x = trim(_date[DATE_PARTS[ix]]);
            if (!x.match(/^-?\d*$/)) {
                throw new TLError(
                    "invalid_date_err",
                    DATE_PARTS[ix] + " = '" + _date[DATE_PARTS[ix]] + "'"
                );
            }

            var parsed = parseInt(_date[DATE_PARTS[ix]]);
            if (isNaN(parsed)) {
                parsed = ix == 4 || ix == 5 ? 1 : 0; // month and day have diff baselines
            }
            _date[DATE_PARTS[ix]] = parsed;
        }

        if (_date.month > 0 && _date.month <= 12) {
            // adjust for JS's weirdness
            _date.month = _date.month - 1;
        }

        return _date;
    },

    _createDateObj: function() {
        var _date = this._getDateData();
        this.data.date_obj = new Date(
            _date.year,
            _date.month,
            _date.day,
            _date.hour,
            _date.minute,
            _date.second,
            _date.millisecond
        );
        if (this.data.date_obj.getFullYear() != _date.year) {
            // Javascript has stupid defaults for two-digit years
            this.data.date_obj.setFullYear(_date.year);
        }
    },

    /*  Find Best Format
     * this may not work with 'cosmologic' dates, or with TLDate if we
     * support constructing them based on JS Date and time
    ================================================== */
    findBestFormat: function(variant) {
        var eval_array = DATE_PARTS,
            format = "";

        for (var i = 0; i < eval_array.length; i++) {
            if (this.data[eval_array[i]]) {
                if (variant) {
                    if (!(variant in BEST_DATEFORMATS)) {
                        variant = "short"; // legacy
                    }
                } else {
                    variant = "base";
                }
                return BEST_DATEFORMATS[variant][eval_array[i]];
            }
        }
        return "";
    },
    _setFormat: function(format, format_short) {
        if (format) {
            this.data.format = format;
        } else if (!this.data.format) {
            this.data.format = this.findBestFormat();
        }

        if (format_short) {
            this.data.format_short = format_short;
        } else if (!this.data.format_short) {
            this.data.format_short = this.findBestFormat(true);
        }
    },
    /**
     * Get the year-only representation of this date. Ticks need this to layout
     * the time axis, and this needs to work isomorphically for TLDate and BigDate 
     * @returns {Number}
     */
    getFullYear: function() {
        return this.data.date_obj.getFullYear()
    }
});

// offer something that can figure out the right date class to return
export function makeDate(data) {
    var date = new TLDate(data);
    if (!isNaN(date.getTime())) {
        return date;
    }
    return new BigDate(data);
}

export function parseDate(str) {
    if (str.match(ISO8601_SHORT_PATTERN)) {
        // parse short specifically to avoid timezone offset confusion
        // most browsers assume short is UTC, not local time.
        var parts = str.match(ISO8601_SHORT_PATTERN).slice(1);
        var d = { year: parts[0].replace("+", "") }; // year can be negative
        if (parts[1]) {
            d["month"] = parts[1].replace("-", "");
        }
        if (parts[2]) {
            d["day"] = parts[2].replace("-", "");
        }
        return d;
    }

    if (str.match(ISO8601_PATTERN)) {
        return cls.parseISODate(str);
    }

    if (str.match(/^\-?\d+$/)) {
        return { year: str };
    }

    var parsed = {};
    if (str.match(/\d+\/\d+\/\d+/)) {
        // mm/yy/dddd
        var date = str.match(/\d+\/\d+\/\d+/)[0];
        str = trim(str.replace(date, ""));
        var date_parts = date.split("/");
        parsed.month = date_parts[0];
        parsed.day = date_parts[1];
        parsed.year = date_parts[2];
    }

    if (str.match(/\d+\/\d+/)) {
        // mm/yy
        var date = str.match(/\d+\/\d+/)[0];
        str = trim(str.replace(date, ""));
        var date_parts = date.split("/");
        parsed.month = date_parts[0];
        parsed.year = date_parts[1];
    }

    if (str.match(":")) {
        var time_parts = str.split(":");
        parsed.hour = time_parts[0];
        parsed.minute = time_parts[1];
        if (time_parts[2]) {
            let second_parts = time_parts[2].split(".");
            parsed.second = second_parts[0];
            parsed.millisecond = second_parts[1];
        }
    }
    return parsed;
};

export const BigYear = TLClass.extend({
    initialize: function(year) {
        this.year = parseInt(year);
        if (isNaN(this.year)) {
            throw new TLError("invalid_year_err", year);
        }
    },

    isBefore: function(that) {
        return this.year < that.year;
    },

    isAfter: function(that) {
        return this.year > that.year;
    },

    getTime: function() {
        return this.year;
    }
});


//
// Class for cosmological dates
//

// cosmo units are years, not millis
const AGE = 1000000;
const EPOCH = AGE * 10;
const ERA = EPOCH * 10;
const EON = ERA * 10;

function Floorer(unit) {
    return function(a_big_year) {
        var year = a_big_year.getTime();
        return new BigYear(Math.floor(year / unit) * unit);
    }
}

// cosmological scales
export const BIG_DATE_SCALES = [ // ( name, units_per_tick, flooring function )
    ['year', 1, new Floorer(1)],
    ['decade', 10, new Floorer(10)],
    ['century', 100, new Floorer(100)],
    ['millennium', 1000, new Floorer(1000)],
    ['age', AGE, new Floorer(AGE)], // 1M years
    ['epoch', EPOCH, new Floorer(EPOCH)], // 10M years
    ['era', ERA, new Floorer(ERA)], // 100M years
    ['eon', EON, new Floorer(EON)] // 1B years
];


export const BigDate = TLDate.extend({
    // @data = BigYear object or JS dictionary with date properties
    initialize: function(data, format, format_short) {
        if (BigYear == data.constructor) {
            this.data = {
                date_obj: data
            };
        } else {
            this.data = JSON.parse(JSON.stringify(data));
            this._createDateObj();
        }

        if (data.format && !format) {
            format = data.format
        }

        this._setFormat(format, format_short);
    },

    // Create date_obj
    _createDateObj: function() {
        var _date = this._getDateData();
        this.data.date_obj = new BigYear(_date.year);
    },

    // Return a new BigDate which has been 'floored' at the given scale.
    // @scale = string value from BIG_DATE_SCALES
    floor: function(scale) {
        for (var i = 0; i < BIG_DATE_SCALES.length; i++) {
            if (BIG_DATE_SCALES[i][0] == scale) {
                var floored = BIG_DATE_SCALES[i][2](this.data.date_obj);
                return new BigDate(floored);
            }
        }

        throw new TLError("invalid_scale_err", scale);
    },
    /**
     * Get the year-only representation of this date. Ticks need this to layout
     * the time axis, and this needs to work isomorphically for TLDate and BigDate 
     * @returns {Number}
     */
    getFullYear: function() {
        return this.data.date_obj.getTime()
    }
});