/*
 * Copyright (C) 2014 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration. All Rights Reserved.
 */
/**
 * @exports PeriodicTimeSequence
 * @version $Id: PeriodicTimeSequence.js 3362 2015-07-31 19:29:12Z tgaskins $
 */
define([
        '../error/ArgumentError',
        '../util/Logger'
    ],
    function (ArgumentError,
              Logger) {
        "use strict";

        /**
         * Constructs a time sequence from an ISO 8601 string.
         * @alias PeriodicTimeSequence
         * @constructor
         * @classdesc Represents a time sequence described as an ISO 8601 time-format string as required by WMS.
         * The string must be in the form start/end/period, where start and end are ISO 8601 time values and
         * period is an ISO 8601 period specification. This class provides iteration over the sequence in steps
         * specified by the period. If the start and end dates are different, iteration will start at the start
         * date and end at the end date. If the start and end dates are the same, iteration will start at the
         * specified date and will never end.
         * @param {String} sequenceString The string describing the time sequence.
         * @throws {ArgumentError} If the specified intervalString is null, undefined or not a valid time interval
         * string.
         */
        var PeriodicTimeSequence = function (sequenceString) {
            if (!sequenceString) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "PeriodicTimeSequence", "constructor", "missingString"));
            }

            var intervalParts = sequenceString.split("/");
            if (intervalParts.length !== 3) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "PeriodicTimeSequence", "constructor",
                        "The interval string " + sequenceString + " does not contain 3 elements."));
            }

            /**
             * This sequence's sequence string, as specified to the constructor.
             * @type {String}
             * @readonly
             */
            this.sequenceString = sequenceString;

            /**
             * This sequence's start time.
             * @type {Date}
             * @readonly
             */
            this.startTime = new Date(intervalParts[0]);

            /**
             * This sequence's end time.
             * @type {Date}
             * @readonly
             */
            this.endTime = new Date(intervalParts[1]);

            // Intentionally not documented.
            this.intervalMilliseconds = this.endTime.getTime() - this.startTime.getTime();

            // Documented with property accessor below.
            this._currentTime = this.startTime;

            /**
             * Indicates whether this sequence is an infinite sequence -- the start and end dates are the same.
             * @type {Boolean}
             * @readonly
             */
            this.infiniteInterval = this.startTime.getTime() == this.endTime.getTime();

            // Intentionally not documented. The array of sequence increments:
            // year, month, week, day, hours, minutes, seconds
            this.period = PeriodicTimeSequence.parsePeriodString(intervalParts[2], false);
        };

        Object.defineProperties(PeriodicTimeSequence.prototype, {
            /**
             * This sequence's current time.
             * @type {Date}
             * @default This sequence's start time.
             * @memberof PeriodicTimeSequence.prototype
             */
            currentTime: {
                get: function () {
                    return this._currentTime;
                },
                set: function (value) {
                    this._currentTime = value;
                }
            },
            /**
             * Indicates the position of this sequence's current time relative to the sequence's total interval,
             * in the range [0, 1]. A value of 0 indicates this sequence's start time. A value of 1 indicates
             * this sequence's end time. A value of 0.5 indicates a current time that's exactly mid-way between
             * this sequence's start time and end time.
             * @type {Number}
             * @memberof PeriodicTimeSequence.prototype
             */
            scaleForCurrentTime: {
                get: function () {
                    if (!this.currentTime) {
                        return 1;
                    } else {
                        return (this.currentTime.getTime() - this.startTime.getTime()) / this.intervalMilliseconds;
                    }
                }
            }
        });

        /**
         * Sets this sequence's current time to the next time in the sequence and returns that time.
         * @returns {Date|null} The next time of this sequence, or null if no more times are in the sequence.
         * Use [reset]{@link PeriodicTimeSequence#reset} to re-start this sequence.
         * Use [previous]{@link PeriodicTimeSequence#previous} to step backwards through this sequence.
         */
        PeriodicTimeSequence.prototype.next = function () {
            if (!this.currentTime) {
                this.currentTime = this.startTime;
            } else if ((this.currentTime.getTime() >= this.endTime.getTime()) && !this.infiniteInterval) {
                this.currentTime = null;
            } else {
                this.currentTime = PeriodicTimeSequence.incrementTime(this.currentTime, this.period);
            }

            return this.currentTime;
        };

        /**
         * Sets this sequence's current time to the previous time in the sequence and returns that time.
         * @returns {Date|null} The previous time of this sequence, or null if the sequence is currently at its start
         * time.
         * Use [next]{@link PeriodicTimeSequence#next} to step forwards through this sequence.
         */
        PeriodicTimeSequence.prototype.previous = function () {
            if (!this.currentTime) {
                this.currentTime = this.endTime;
            } else if (this.currentTime.getTime() === this.startTime.getTime()) {
                this.currentTime = null;
            } else {
                this.currentTime = this.getTimeForScale(0.9999 * this.scaleForCurrentTime);
            }

            return this.currentTime;
        };

        /**
         * Resets this sequence's current time to its start time.
         * Use [next]{@link PeriodicTimeSequence#next} to step forwards through this sequence.
         * Use [previous]{@link PeriodicTimeSequence#previous} to step backwards through this sequence.
         */
        PeriodicTimeSequence.prototype.reset = function () {
            this.currentTime = null;
        };

        /**
         * Returns the time associated with a specified value in the range [0, 1]. A value of 0 returns this
         * sequence's start time. A value of 1 returns this sequence's end time. A value of 0.5 returs a time
         * mid-way between this sequence's start and end times.
         * @param scale The scale value. This value is clamped to the range [0, 1] before the time is determined.
         * @returns {Date}
         */
        PeriodicTimeSequence.prototype.getTimeForScale = function (scale) {
            if (scale <= 0) {
                return this.startTime;
            }

            if (scale >= 1) {
                return this.endTime;
            }

            var time = new Date(this.startTime.getTime()),
                previousTime = time,
                s = 0;

            for (s = 0; s < scale; s = (time.getTime() - this.startTime.getTime()) / this.intervalMilliseconds) {
                previousTime = time;
                time = PeriodicTimeSequence.incrementTime(time, this.period);
            }

            return previousTime;
        };

        // Intentionally not documented. Adds this sequence's period to a specified time.
        PeriodicTimeSequence.incrementTime = function (currentTime, period) {
            var newTime = new Date(currentTime.getTime());

            if (period[0] != 0) {
                newTime.setUTCFullYear(newTime.getUTCFullYear() + period[0]);
            }

            if (period[1] != 0) {
                PeriodicTimeSequence.addMonths(newTime, period[1]);
            }

            if (period[2] != 0) {
                newTime.setUTCDate(newTime.getUTCDate() + 7 * period[2]);
            }

            if (period[3] != 0) {
                newTime.setUTCDate(newTime.getUTCDate() + period[3]);
            }

            if (period[4] != 0) {
                newTime.setUTCHours(newTime.getUTCHours() + period[4]);
            }

            if (period[5] != 0) {
                newTime.setUTCMinutes(newTime.getUTCMinutes() + period[5]);
            }

            if (period[6] != 0) {
                newTime.setUTCSeconds(newTime.getUTCSeconds() + period[6]);
            }

            return newTime;
        };

        // Intentionally not documented.
        PeriodicTimeSequence.isLeapYear = function (year) {
            return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
        };

        // Intentionally not documented.
        PeriodicTimeSequence.getDaysInMonth = function (year, month) {
            return [31, (PeriodicTimeSequence.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
        };

        // Intentionally not documented.
        PeriodicTimeSequence.addMonths = function (date, numMonths) {
            var n = date.getUTCDate();
            date.setUTCDate(1);
            date.setUTCMonth(date.getUTCMonth() + numMonths);
            date.setUTCDate(Math.min(n, PeriodicTimeSequence.getDaysInMonth(date.getUTCFullYear(), date.getUTCMonth())));
            return date;
        };

        /*
         * Parses a ISO8601 period string.
         * @param {String} period iso8601 period string
         * @param {Boolean} distributeOverflow if 'true', the unit overflows are merge into the next higher units.
         */
        PeriodicTimeSequence.parsePeriodString = function (period, distributeOverflow) {
            // Taken from https://github.com/nezasa/iso8601-js-period/blob/master/iso8601.js

            // regex splits as follows
            // grp0 omitted as it is equal to the sample
            //
            // | sample            | grp1   | grp2 | grp3 | grp4 | grp5 | grp6       | grp7 | grp8 | grp9 |
            // --------------------------------------------------------------------------------------------
            // | P1Y2M3W           | 1Y2M3W | 1Y   | 2M   | 3W   | 4D   | T12H30M17S | 12H  | 30M  | 17S  |
            // | P3Y6M4DT12H30M17S | 3Y6M4D | 3Y   | 6M   |      | 4D   | T12H30M17S | 12H  | 30M  | 17S  |
            // | P1M               | 1M     |      | 1M   |      |      |            |      |      |      |
            // | PT1M              | 3Y6M4D |      |      |      |      | T1M        |      | 1M   |      |
            // --------------------------------------------------------------------------------------------

            var _distributeOverflow = (distributeOverflow) ? distributeOverflow : false;
            var valueIndexes = [2, 3, 4, 5, 7, 8, 9];
            var duration = [0, 0, 0, 0, 0, 0, 0];
            var overflowLimits = [0, 12, 4, 7, 24, 60, 60];
            var struct;

            // upcase the string just in case people don't follow the letter of the law
            period = period.toUpperCase();

            // input validation
            if (!period)
                return duration;
            else if (typeof period !== "string") {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "PeriodicTimeSequence", "parsePeriodString",
                        "Invalid ISO8601 period string '" + period + "'"));
            }

            // parse the string
            if (struct = /^P((\d+Y)?(\d+M)?(\d+W)?(\d+D)?)?(T(\d+H)?(\d+M)?(\d+S)?)?$/.exec(period)) {

                // remove letters, replace by 0 if not defined
                for (var i = 0; i < valueIndexes.length; i++) {
                    var structIndex = valueIndexes[i];
                    duration[i] = struct[structIndex] ? +struct[structIndex].replace(/[A-Za-z]+/g, '') : 0;
                }
            }
            else {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "PeriodicTimeSequence", "parsePeriodString",
                        "String '" + period + "' is not a valid ISO8601 period."));
            }

            if (_distributeOverflow) {
                // note: stop at 1 to ignore overflow of years
                for (i = duration.length - 1; i > 0; i--) {
                    if (duration[i] >= overflowLimits[i]) {
                        duration[i - 1] = duration[i - 1] + Math.floor(duration[i] / overflowLimits[i]);
                        duration[i] = duration[i] % overflowLimits[i];
                    }
                }
            }

            return duration;
        };

        return PeriodicTimeSequence;
    })
;