'use strict';

/**
 * The maximum number of allowed hours in a day
 * NOTE: This can be greater than 24 due to trips starting late in the day on one 
 * day and continuing into the next calendar day, but the hours continue from the 
 * first day (ie, 25:30 = 1:30 AM)
 * It is also artificially increase in TripsTable.js by 24 hours when looking for 
 * a trip that started on the previous day (but has a 24+ hr time)
 * @private
 */
const MAX_HOURS = 48;

/**
 * DateTime Class
 * @see {@link DateTime}
 * @module utils/DateTime
 */

/**
 * Right Track Date Time
 * ---------------------
 * This class handles the various representations of dates and
 * times used by the GTFS Spec and the Right Track Library and
 * the various transformations between them.
 *
 * Time Formats
 * - _string_ hh:mm aa (1:30 PM) | **Human Readable Time**
 * - _string_ HH:mm:ss (13:30:00) | **GTFS Time**
 * - _string_ HH:mm (13:30)
 * - _string_ HHmm (1330)
 * - _int_ seconds since midnight (7:00 am = 25200) | **Time Seconds**
 *
 * Date Formats
 * - _int_ yyyymmdd (20170930) | **Date Integer**
 *
 * **Module:** {@link module:utils/DateTime|utils/DateTime}
 *
 * @class
 * @alias DateTime
 */
class DateTime {

  /**
   * Right Track Date Time Constructor
   * @constructor
   * @param {(string|int)} time String or Integer representation of the time.  Can be in one
   * of the following formats: 1) HH:mm:ss 2) HH:mm 3) HHmm 4) h:mm aa or 5) an integer of the
   * number of seconds since midnight (ex 7:00 am = 25200)
   * @param {int} date The date of the DateTime in the format yyyymmdd
   */
  constructor(time, date) {

    /**
     * INTERNAL REPRESENTATION
     * time = {int} number of seconds since midnight
     * date = {int} date in the format of yyyymmdd
     */


    // ==== PARSE THE TIME ==== //

    // Time Format: integer with seconds in day
    if ( Number.isInteger(time) ) {

      // Check to make sure time int is within reasonable range
      if ( time >= 0 && time <= (MAX_HOURS*3600) ) {
        this.time = time;
      }

      // Time int is not within reasonable range of 0 --> MAX_HOURS hours
      else {
        throw new Error('DATETIME ERROR: Time Integer is out of bounds. time=' + time + ' seconds');
      }

    }

    // Time Format: string
    else {

      // Time format: hh:mm aa
      let matches = time.match('((1[0-2]|0?[1-9]):([0-5][0-9])\\s?([AaPp][Mm]))');
      if ( null !== matches && matches.length > 0 ) {
        let h = parseInt(time.split(':')[0]);
        let m = parseInt(time.split(':')[1].substr(0, 2));
        let aa = time.slice(-2).toLowerCase();

        // Reset h to 0 for 12 AM
        if ( aa === 'am' && h === 12 ) {
          h = 0;
        }

        // Add 12 to h for pm
        else if ( aa === 'pm' && h !== 12 ) {
          h = h + 12;
        }

        this.time = h*3600 + m*60;
      }

      // Time Format: HH:mm:ss OR HH:mm
      else if ( time.indexOf(':') > -1 ) {
        let parts = time.split(':');

        // When there is either 1 or 2 colons
        if ( parts.length === 3 || parts.length === 2 ) {
          let h = parseInt(parts[0]);
          let m = parseInt(parts[1]);
          let s = parts.length === 3 ? parseInt(parts[2]) : 0;

          // Check to make sure time parts are within a reasonable range
          if (Number.isInteger(h) && h >= 0 && h <= MAX_HOURS &&
            Number.isInteger(m) && m >= 0 && m <= 59 &&
            Number.isInteger(s) && s >= 0 && s <= 59) {
            this.time = h*3600 + m*60 + s;
          }
          else {
            throw new Error('DATETIME ERROR: Could not parse the time. h=' + h + ', m=' + m + ', s=' + s);
          }

        }

        // Time format: incorrect with colon
        else {
          throw new Error('DATETIME ERROR: Could not parse the time: ' + time);
        }

      }

      // Time Format: HHmm
      else if ( time.length === 4 ) {

        // Parse hours and minutes from time
        let h = parseInt(time.substring(0, 2));
        let m = parseInt(time.substring(2, 4));

        // Check to make sure time parse are numbers within a reasonable range
        if ( Number.isInteger(h) && h >= 0 && h <= MAX_HOURS &&
           Number.isInteger(m) && m >= 0 && m <= 59 ) {
          this.time = h*3600 + m*60;
        }
        else {
          throw new Error('DATETIME ERROR: Could not parse the time. h=' + h + ', m=' + m);
        }

      }

      // Time Format: incorrect
      else {
        throw new Error('DATETIME ERROR: Could not parse the time: ' + time);
      }

    }



    // ==== PARSE THE DATE ==== //


    // Make sure the date is within a reasonable range
    if ( date >= 19700101 && date <= 21001231 ) {
      this.date = date;
    }
    else {
      throw new Error('DATETIME ERROR: Date is not within the expected range. date=' + date);
    }

  }


  // ==== INTERNAL HELPER FUNCTIONS ==== //

  /**
   * Get a JavaScript Date representation of the DateTime
   * @returns {Date}
   * @private
   */
  _getJSDate() {
    let hours = this._getHours();
    let deltaDays = 0;
    if ( hours >= 24 ) {
      hours = hours - 24;
      deltaDays = 1;
    }

    let date = new Date(
      this._getYear(),
      this._getMonth()-1,
      this._getDate(),
      hours,
      this._getMins(),
      this._getSecs()
    );
    date.setDate(date.getDate()+deltaDays);

    return date;
  }

  /**
   * Get the DateTime's Hours
   * @returns {number}
   * @private
   */
  _getHours() {
    return Math.floor(this.time/3600);
  }

  /**
   * Get the DateTime's Minutes
   * @returns {number}
   * @private
   */
  _getMins() {
    return Math.floor((this.time%3600)/60);
  }

  /**
   * Get the DateTime's Seconds
   * @returns {number}
   * @private
   */
  _getSecs() {
    return Math.floor((this.time%3600)%60);
  }

  /**
   * Get the DateTime's Year
   * @returns {string}
   * @private
   */
  _getYear() {
    return this.date.toString().substr(0, 4);
  }

  /**
   * Get the DateTime's Month (1 based)
   * @returns {string}
   * @private
   */
  _getMonth() {
    return this.date.toString().substring(4, 6);
  }

  /**
   * Get the DateTime's Date
   * @returns {string}
   * @private
   */
  _getDate() {
    return this.date.toString().substring(6, 8);
  }


  // ==== MUTATORS ==== //

  /**
   * Add or Subtract the specified number of days to the DateTime's date
   * @param delta +/- number of days to add
   * @returns {DateTime} return the DateTime
   */
  deltaDays(delta) {
    let date = this._getJSDate();
    date.setDate(date.getDate() + delta);

    let y = date.getFullYear();
    let m = date.getMonth()+1;
    let d = date.getDate();

    if ( m < 10 ) {
      m = '0' + m;
    }
    if ( d < 10 ) {
      d = '0' + d;
    }

    this.date = parseInt('' + y + m + d);

    return this;
  }


  /**
   * Add or Subtract the specified number of minutes to the DateTime's time
   * @param delta +/- number of minutes to add
   * @returns {DateTime} return the DateTime
   */
  deltaMins(delta) {

    // Create new JS Date with changed time
    let date = this._getJSDate();
    let time = date.getTime() + (delta * 60000);
    date.setTime(time);
    let js = DateTime.createFromJSDate(date);

    // Set time and date properties
    this.time = js.time;
    this.date = js.date;

    // return a reference to this date
    return this;
  }




  // ==== TIME GETTERS ==== //

  /**
   * Get the time in seconds since midnight
   * @returns {int} time integer in seconds
   */
  getTimeSeconds() {
    return this.time;
  }

  /**
   * Get the Time in HHmm format
   * @return {string} HHmm
   */
  getTimeInt() {
    let h = this._getHours();
    let m = this._getMins();

    // Pad with leading 0s
    if ( h < 10 ) {
      h = '0' + h;
    }
    if ( m < 10 ) {
      m = '0' + m;
    }

    return h + '' + m;
  }

  /**
   * Get the GTFS Spec time representation (HH:mm:ss)
   * @returns {string} GTFS Time (HH:mm:ss)
   */
  getTimeGTFS() {
    let h = this._getHours();
    let m = this._getMins();
    let s = this._getSecs();

    // Pad with leading 0s
    if ( h < 10 ) {
      h = '0' + h;
    }
    if ( m < 10 ) {
      m = '0' + m;
    }
    if ( s < 10 ) {
      s = '0' + s;
    }

    return h + ':' + m + ':' + s;
  }

  /**
   * Get the human readable time (12 hr with AM/PM)
   * @returns {string} human readable time
   */
  getTimeReadable() {
    let h = this._getHours();
    let m = this._getMins();

    // Pad Minutes with 0s
    if ( m < 10 ) {
      m = '0' + m;
    }

    // 12 AM
    if ( h === 0 ) {
      return '12:' + m + ' AM';
    }

    // AM
    else if ( h < 12 ) {
      return h + ':' + m + ' AM';
    }

    // 12 PM
    else if ( h === 12 ) {
      return h + ':' + m + ' PM';
    }

    // PM
    else if ( h < 24 ) {
      h = h - 12;
      return h + ':' + m + ' PM';
    }

    // Next day
    else if ( h === 24 ) {
      h = 12;
      return h + ':' + m + ' AM';
    }

    // Next Day
    else if ( h > 24 ) {
      h = h - 24;
      return h + ':' + m + ' AM';
    }

    // Shouldn't reach here
    else {
      return this.getTimeGTFS();
    }

  }


  // ==== DATE GETTERS ==== //

  /**
   * Check if the date is set
   * @returns {boolean} true if the date is set
   */
  isDateSet() {
    return this.date !== 19700101;
  }

  /**
   * Get the integer representation of the date (yyyymmdd)
   * @returns {int} date integer (yyyymmdd)
   */
  getDateInt() {
    return this.date;
  }

  /**
   * Get the full name of the weekday of the date (monday, tuesday, etc)
   * @returns {string} day of week
   */
  getDateDOW() {
    let dow = new Array(7);
    dow[0] = 'sunday';
    dow[1] = 'monday';
    dow[2] = 'tuesday';
    dow[3] = 'wednesday';
    dow[4] = 'thursday';
    dow[5] = 'friday';
    dow[6] = 'saturday';

    return dow[this._getJSDate().getDay()];
  }

  /**
   * Get the human readable date ([Thu, ]Apr 18, 2019)
   * @param  {boolean} dow When true, include the day of the week
   * @return {string} human readable date
   */
  getDateReadable(dow) {
    let days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

    let rtn = "";
    if ( dow ) {
      rtn += days[this._getJSDate().getDay()] + ", ";
    }
    rtn += months[this._getJSDate().getMonth()] + " " + this._getJSDate().getDate() + ", " + this._getJSDate().getFullYear();

    return rtn;
  }




  // ==== DATE/TIME FUNCTIONS ==== //

  /**
   * Get a String representation of the DateTime to be
   * used as a MySQL DateTime
   * @returns {string} MySQL DateTime String
   */
  toMySQLString() {
    if ( this.date === 19700101 ) {
      console.warn('DATE NOT SET');
    }

    // Set date
    let str = this._getYear() + '-' + this._getMonth() + '-' + this._getDate();

    // Set time
    str = str + ' ' + this.getTimeGTFS();

    return str;
  }

  /**
   * Get a String representation of the DateTime to be used
   * in HTTP Headers
   * @returns {string} HTTP Header String
   */
  toHTTPString() {
    return this._getJSDate().toUTCString();
  }


  /**
   * Get a timestamp (in ms) of the DateTime
   * @returns {number} timestamp (ms) of DateTime
   */
  toTimestamp() {
    return this._getJSDate().getTime();
  }

  /**
   * Get a String representation of the DateTIme
   * @returns {string} string of DateTime
   */
  toString() {
    let str = '';
    if ( this.date !== 19700101 ) {
      let y = this._getYear();
      let m = this._getMonth();
      let d = this._getDate();
      str = str + y + '-' + m + '-' + d + ' ';
    }
    str = str + '@ ' + this.getTimeGTFS();
    return str;
  }

  /**
   * Create a new DateTime Object with the properties of this one
   * @returns {DateTime} DateTime with same date and time
   */
  clone() {
    return new DateTime(this.time, this.date);
  }

}





// ==== DATETIME FACTORIES ==== //


/**
 * DateTime Factory: with time and date
 * @param {string|int} time Time
 * @param {int} date Date
 * @returns {DateTime} DateTime
 */
DateTime.create = function(time, date) {
  return new DateTime(time, date);
};

/**
 * DateTime Factory: date and time of now
 * @returns {DateTime} DateTime
 */
DateTime.now = function() {
  return DateTime.createFromJSDate(new Date());
};


/**
 * DateTime Factory: with JavaScript Date
 * @param {Date} jd JavaScript Date
 */
DateTime.createFromJSDate = function(jd) {
  // Construct Date
  let y = jd.getFullYear();
  let m = jd.getMonth() + 1;
  let d = jd.getDate();
  if ( m < 10 ) {
    m = '0' + m;
  }
  if ( d < 10 ) {
    d = '0' + d;
  }
  let date = parseInt('' + y + m + d);

  // Construct Time
  let h = jd.getHours();
  let min = jd.getMinutes();
  let sec = jd.getSeconds();
  if ( h < 10 ) {
    h = '0' + h;
  }
  if ( min < 10 ) {
    min = '0' + min;
  }
  if ( sec < 10 ) {
    sec = '0' + sec;
  }
  let time = h + ':' + min + ':' + sec;

  return new DateTime(time, date);
};

/**
 * DateTime Factory: with time
 * @param {string} time Time
 * @param {boolean} [guessDate=false] Set to true to guess the Date relative to today
 * @returns {DateTime} DateTime
 */
DateTime.createFromTime = function(time, guessDate) {
  // Default Date = epoch
  let date = 19700101;
  let delta = 0;

  // Try to guess the date based on the requested and current times
  if ( guessDate ) {
    date = DateTime.now().getDateInt();
    let ts = DateTime.create(time, 19700101).getTimeSeconds();
    let ns = DateTime.now().getTimeSeconds();

    // AM: before 4 AM
    if ( ns <= 14400 ) {
      // Assume late night times (after 8 PM) are yesterday
      if ( ts >= 72000 ) {
        delta = -1;
      }
    }

    // PM: after 4 PM
    else if ( ns >= 57600 ) {
      // Assume early morning times (before 8 AM) are next day
      if ( ts <= 28800 ) {
        delta = +1;
      }
    }
  }

  // Create the DateTime
  return new DateTime(time, date).deltaDays(delta);
};

/**
 * DateTime Factory: with date
 * @param {int} date Date
 * @returns {DateTime} DateTime
 */
DateTime.createFromDate = function(date) {
  return new DateTime('00:00:00', date);
};


module.exports = DateTime;