// AMD support (Thanks to @FagnerMartinsBrack)
;(function(factory) {
  'use strict';

  if (typeof define === 'function' && define.amd) {
    define(['jquery'], factory);
  } else {
    factory(jQuery);
  }
})(function($){
  'use strict';

  var instances = [],
      matchers  = [],
      defaultOptions  = {
        precision: 100, // 0.1 seconds, used to update the DOM
        elapse: false,
        defer: false
      };
  // Miliseconds
  matchers.push(/^[0-9]*$/.source);
  // Month/Day/Year [hours:minutes:seconds]
  matchers.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/
    .source);
  // Year/Day/Month [hours:minutes:seconds] and
  // Year-Day-Month [hours:minutes:seconds]
  matchers.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/
    .source);
  // Cast the matchers to a regular expression object
  matchers = new RegExp(matchers.join('|'));
  // Parse a Date formatted has String to a native object
  function parseDateString(dateString) {
    // Pass through when a native object is sent
    if(dateString instanceof Date) {
      return dateString;
    }
    // Caste string to date object
    if(String(dateString).match(matchers)) {
      // If looks like a milisecond value cast to number before
      // final casting (Thanks to @msigley)
      if(String(dateString).match(/^[0-9]*$/)) {
        dateString = Number(dateString);
      }
      // Replace dashes to slashes
      if(String(dateString).match(/\-/)) {
        dateString = String(dateString).replace(/\-/g, '/');
      }
      return new Date(dateString);
    } else {
      throw new Error('Couldn\'t cast `' + dateString +
        '` to a date object.');
    }
  }
  // Map to convert from a directive to offset object property
  var DIRECTIVE_KEY_MAP = {
    'Y': 'years',
    'm': 'months',
    'n': 'daysToMonth',
    'd': 'daysToWeek',
    'w': 'weeks',
    'W': 'weeksToMonth',
    'H': 'hours',
    'M': 'minutes',
    'S': 'seconds',
    'D': 'totalDays',
    'I': 'totalHours',
    'N': 'totalMinutes',
    'T': 'totalSeconds'
  };
  // Returns an escaped regexp from the string
  function escapedRegExp(str) {
    var sanitize = str.toString().replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
    return new RegExp(sanitize);
  }
  // Time string formatter
  function strftime(offsetObject) {
    return function(format) {
      var directives = format.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi);
      if(directives) {
        for(var i = 0, len = directives.length; i < len; ++i) {
          var directive   = directives[i]
              .match(/%(-|!)?([a-zA-Z]{1})(:[^;]+;)?/),
            regexp    = escapedRegExp(directive[0]),
            modifier  = directive[1] || '',
            plural    = directive[3] || '',
            value     = null;
            // Get the key
            directive = directive[2];
          // Swap shot-versions directives
          if(DIRECTIVE_KEY_MAP.hasOwnProperty(directive)) {
            value = DIRECTIVE_KEY_MAP[directive];
            value = Number(offsetObject[value]);
          }
          if(value !== null) {
            // Pluralize
            if(modifier === '!') {
              value = pluralize(plural, value);
            }
            // Add zero-padding
            if(modifier === '') {
              if(value < 10) {
                value = '0' + value.toString();
              }
            }
            // Replace the directive
            format = format.replace(regexp, value.toString());
          }
        }
      }
      format = format.replace(/%%/, '%');
      return format;
    };
  }
  // Pluralize
  function pluralize(format, count) {
    var plural = 's', singular = '';
    if(format) {
      format = format.replace(/(:|;|\s)/gi, '').split(/\,/);
      if(format.length === 1) {
        plural = format[0];
      } else {
        singular = format[0];
        plural = format[1];
      }
    }
    // Fix #187
    if(Math.abs(count) > 1) {
      return plural;
    } else {
      return singular;
    }
  }
  // The Final Countdown
  var Countdown = function(el, finalDate, options) {
    this.el       = el;
    this.$el      = $(el);
    this.interval = null;
    this.offset   = {};
    this.options  = $.extend({}, defaultOptions);
    // console.log(this.options);
    // Register this instance
    this.instanceNumber = instances.length;
    instances.push(this);
    // Save the reference
    this.$el.data('countdown-instance', this.instanceNumber);
    // Handle options or callback
    if (options) {
      // Register the callbacks when supplied
      if(typeof options === 'function') {
        this.$el.on('update.countdown', options);
        this.$el.on('stoped.countdown', options);
        this.$el.on('finish.countdown', options);
      } else {
        this.options = $.extend({}, defaultOptions, options);
      }
    }
    // Set the final date and start
    this.setFinalDate(finalDate);
    // Starts the countdown automatically unless it's defered,
    // Issue #198
    if (this.options.defer === false) {
      this.start();
    }
  };
  $.extend(Countdown.prototype, {
    start: function() {
      if(this.interval !== null) {
        clearInterval(this.interval);
      }
      var self = this;
      this.update();
      this.interval = setInterval(function() {
        self.update.call(self);
      }, this.options.precision);
    },
    stop: function() {
      clearInterval(this.interval);
      this.interval = null;
      this.dispatchEvent('stoped');
    },
    toggle: function() {
      if (this.interval) {
        this.stop();
      } else {
        this.start();
      }
    },
    pause: function() {
      this.stop();
    },
    resume: function() {
      this.start();
    },
    remove: function() {
      this.stop.call(this);
      instances[this.instanceNumber] = null;
      // Reset the countdown instance under data attr (Thanks to @assiotis)
      delete this.$el.data().countdownInstance;
    },
    setFinalDate: function(value) {
      this.finalDate = parseDateString(value); // Cast the given date
    },
    update: function() {
      // Stop if dom is not in the html (Thanks to @dleavitt)
      if(this.$el.closest('html').length === 0) {
        this.remove();
        return;
      }
      var hasEventsAttached = $._data(this.el, 'events') !== undefined,
          now               = new Date(),
          newTotalSecsLeft;
      // Create an offset date object
      newTotalSecsLeft = this.finalDate.getTime() - now.getTime(); // Millisecs
      // Calculate the remaining time
      newTotalSecsLeft = Math.ceil(newTotalSecsLeft / 1000); // Secs
      // If is not have to elapse set the finish
      newTotalSecsLeft = !this.options.elapse && newTotalSecsLeft < 0 ? 0 :
        Math.abs(newTotalSecsLeft);
      // Do not proceed to calculation if the seconds have not changed or
      // does not any event attached
      if (this.totalSecsLeft === newTotalSecsLeft || !hasEventsAttached) {
        return;
      } else {
        this.totalSecsLeft = newTotalSecsLeft;
      }
      // Check if the countdown has elapsed
      this.elapsed = (now >= this.finalDate);
      // Calculate the offsets
      this.offset = {
        seconds     : this.totalSecsLeft % 60,
        minutes     : Math.floor(this.totalSecsLeft / 60) % 60,
        hours       : Math.floor(this.totalSecsLeft / 60 / 60) % 24,
        days        : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7,
        daysToWeek  : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7,
        daysToMonth : Math.floor(this.totalSecsLeft / 60 / 60 / 24 % 30.4368),
        weeks       : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7),
        weeksToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7) % 4,
        months      : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 30.4368),
        years       : Math.abs(this.finalDate.getFullYear()-now.getFullYear()),
        totalDays   : Math.floor(this.totalSecsLeft / 60 / 60 / 24),
        totalHours  : Math.floor(this.totalSecsLeft / 60 / 60),
        totalMinutes: Math.floor(this.totalSecsLeft / 60),
        totalSeconds: this.totalSecsLeft
      };
      // Dispatch an event
      if(!this.options.elapse && this.totalSecsLeft === 0) {
        this.stop();
        this.dispatchEvent('finish');
      } else {
        this.dispatchEvent('update');
      }
    },
    dispatchEvent: function(eventName) {
      var event = $.Event(eventName + '.countdown');
      event.finalDate     = this.finalDate;
      event.elapsed       = this.elapsed;
      event.offset        = $.extend({}, this.offset);
      event.strftime      = strftime(this.offset);
      this.$el.trigger(event);
    }
  });
  // Register the jQuery selector actions
  $.fn.countdown = function() {
    var argumentsArray = Array.prototype.slice.call(arguments, 0);
    return this.each(function() {
      // If no data was set, jQuery.data returns undefined
      var instanceNumber = $(this).data('countdown-instance');
      // Verify if we already have a countdown for this node ...
      // Fix issue #22 (Thanks to @romanbsd)
      if (instanceNumber !== undefined) {
        var instance = instances[instanceNumber],
          method = argumentsArray[0];
        // If method exists in the prototype execute
        if(Countdown.prototype.hasOwnProperty(method)) {
          instance[method].apply(instance, argumentsArray.slice(1));
        // If method look like a date try to set a new final date
        } else if(String(method).match(/^[$A-Z_][0-9A-Z_$]*$/i) === null) {
          instance.setFinalDate.call(instance, method);
          // Allow plugin to restart after finished
          // Fix issue #38 (thanks to @yaoazhen)
          instance.start();
        } else {
          $.error('Method %s does not exist on jQuery.countdown'
            .replace(/\%s/gi, method));
        }
      } else {
        // ... if not we create an instance
        new Countdown(this, argumentsArray[0], argumentsArray[1]);
      }
    });
  };
});
