/**
 * @class DateFormatter
 * The DateFormatter supports decoding from and encoding to
 * ISO8601 formatted strings. Accepts formats with and without
 * hyphen/colon separators and correctly parses zoning info.
 */
var DateFormatter = function (opts) {
  this.opts = {}
  this.setOpts(opts)
}

/**
 * Default options for DateFormatter
 * @static
 * @see DateFormatter#setOpts
 */
DateFormatter.DEFAULT_OPTIONS = {
  colons: true
, hyphens: false
, local: true
, ms: false
, offset: false
}

/**
 * Regular Expression that disects ISO 8601 formatted strings into
 * an array of parts.
 * @static
 */
DateFormatter.ISO8601 = new RegExp(
  '([0-9]{4})([-]?([0-9]{2}))([-]?([0-9]{2}))'
+ '(T([0-9]{2})(((:?([0-9]{2}))?((:?([0-9]{2}))?(\.([0-9]+))?))?)'
+ '(Z|([+-]([0-9]{2}(:?([0-9]{2}))?)))?)?'
)

/**
 * Sets options for encoding Date objects to ISO8601 strings.
 *    Omitting the 'opts' argument will reset all options to the default.
 *
 * @param {Object} opts - Options (optional)
 * @param {Boolean} opts.colons  - Enable/disable formatting the time portion
 *                                 with a colon as separator (default: true)
 * @param {Boolean} opts.hyphens - Enable/disable formatting the date portion
 *                                 with a hyphen as separator (default: false)
 * @param {Boolean} opts.local   - Encode as local time instead of UTC
 *                                 (default: true)
 * @param {Boolean} opts.ms      - Enable/Disable output of milliseconds
 *                                 (default: false)
 * @param {Boolean} opts.offset  - Enable/Disable output of UTC offset
 *                                 (default: false)
 */
DateFormatter.prototype.setOpts = function (opts) {
  if (!opts) opts = DateFormatter.DEFAULT_OPTIONS

  var ctx = this
  Object.keys(DateFormatter.DEFAULT_OPTIONS).forEach(function (k) {
    ctx.opts[k] = opts.hasOwnProperty(k) ?
      opts[k] : DateFormatter.DEFAULT_OPTIONS[k]
  })
}

/**
 * Converts a date time stamp following the ISO8601 format to a JavaScript Date
 * object.
 *
 * @param {String} time - String representation of timestamp.
 * @return {Date}       - Date object from timestamp.
 */
DateFormatter.prototype.decodeIso8601 = function(time) {
  var dateParts = time.toString().match(DateFormatter.ISO8601)
  if (!dateParts) {
    throw new Error('Expected a ISO8601 datetime but got \'' + time + '\'')
  }

  var date = [
    [dateParts[1], dateParts[3] || '01', dateParts[5] || '01'].join('-')
    , 'T'
    , [
        dateParts[7] || '00'
      , dateParts[11] || '00'
      , dateParts[14] || '00'
      ].join(':')
    , '.'
    , dateParts[16] || '000'
  ].join('')

  date += (dateParts[17] !== undefined) ?
    dateParts[17] +
      ((dateParts[19] && dateParts[20] === undefined) ? '00' : '') :
    DateFormatter.formatCurrentOffset(new Date(date))

  return new Date(date)
}

/**
 * Converts a JavaScript Date object to an ISO8601 timestamp.
 *
 * @param {Date} date - Date object.
 * @return {String}   - String representation of timestamp.
 */
DateFormatter.prototype.encodeIso8601 = function(date) {
  var parts = this.opts.local ?
    DateFormatter.getLocalDateParts(date) :
    DateFormatter.getUTCDateParts(date)

  return [
    [parts[0],parts[1],parts[2]].join(this.opts.hyphens ? '-' : '')
  , 'T'
  , [parts[3],parts[4],parts[5]].join(this.opts.colons ? ':' : '')
  , (this.opts.ms) ? '.' + parts[6] : ''
  , (this.opts.local) ? ((this.opts.offset) ?
      DateFormatter.formatCurrentOffset(date) : '') : 'Z'
  ].join('')
}

/**
 * Helper function to get an array of zero-padded date parts,
 * in UTC
 *
 * @param {Date} date - Date Object
 * @return {String[]}
 */
DateFormatter.getUTCDateParts = function (date) {
  return [
    date.getUTCFullYear()
  , DateFormatter.zeroPad(date.getUTCMonth()+1,2)
  , DateFormatter.zeroPad(date.getUTCDate(),2)
  , DateFormatter.zeroPad(date.getUTCHours(), 2)
  , DateFormatter.zeroPad(date.getUTCMinutes(), 2)
  , DateFormatter.zeroPad(date.getUTCSeconds(), 2)
  , DateFormatter.zeroPad(date.getUTCMilliseconds(), 3)]
}


/**
 * Helper function to get an array of zero-padded date parts,
 * in the local time zone
 *
 * @param {Date} date - Date Object
 * @return {String[]}
 */
DateFormatter.getLocalDateParts = function (date) {
  return [
    date.getFullYear()
  , DateFormatter.zeroPad(date.getMonth()+1,2)
  , DateFormatter.zeroPad(date.getDate(),2)
  , DateFormatter.zeroPad(date.getHours(), 2)
  , DateFormatter.zeroPad(date.getMinutes(), 2)
  , DateFormatter.zeroPad(date.getSeconds(), 2)
  , DateFormatter.zeroPad(date.getMilliseconds(), 3)]
}

/**
 * Helper function to pad the digits with 0s to meet date formatting
 * requirements.
 *
 * @param {Number} digit  - The number to pad.
 * @param {Number} length - Length of digit string, prefix with 0s if not
 *                          already length.
 * @return {String}       - String with the padded digit
 */
DateFormatter.zeroPad = function (digit, length) {
  var padded = '' + digit
  while (padded.length < length) {
    padded = '0' + padded
  }

  return padded
}

/**
 * Helper function to get the current timezone to default decoding to
 * rather than UTC. (for backward compatibility)
 *
 * @return {String} - in the format /Z|[+-]\d{2}:\d{2}/
 */
DateFormatter.formatCurrentOffset = function (d) {
  var offset = (d || new Date()).getTimezoneOffset()
  return (offset === 0) ? 'Z' : [
      (offset < 0) ? '+' : '-'
    , DateFormatter.zeroPad(Math.abs(Math.floor(offset/60)),2)
    , ':'
    , DateFormatter.zeroPad(Math.abs(offset%60),2)
  ].join('')
}

// export an instance of DateFormatter only.
module.exports = new DateFormatter()
