(function ($) {
  'use strict';

  var serialize = function (data, accuracy) {
    accuracy = accuracy > 0 ? accuracy : 1;
    var chunkSize = 24 * accuracy;
    var res = [];
    var i = 0;
    for (i = 0; i < chunkSize * 7; i++) {
      res[i] = 0;
    }
    for (i = 0; i < 7; i++) {
      var row = data[i + 1];
      if (!row) {continue;}
      for (var j = 0, rowLen = row.length; j < rowLen; j++) {
        res[i * chunkSize + row[j]] = 1;
      }
    }

    return res.join('');
  };

  var parse = function (strSequence, accuracy) {
    accuracy = accuracy > 0 ? accuracy : 1;
    var chunkSize = 24 * accuracy;
    var res = {};
    for (var i = 0, row = 1, len = strSequence.length; i < len; i++) {
      var col = i % chunkSize;
      if (strSequence[i] === '1') {
        !res[row] && (res[row] = []);
        res[row].push(col);
      }
      if ((i + 1) % chunkSize === 0) {
        row++;
      }
    }

    return res;
  };

  var toStr = function (currentSelectRange) {
    return Object.prototype.toString.call(currentSelectRange);
  };
  // it only does '%s', and return '' when arguments are undefined
  var sprintf = function (str) {
    var args = arguments;
    var flag = true;
    var i = 1;

    str = str.replace(/%s/g, function () {
      var arg = args[i++];

      if (typeof arg === 'undefined') {
        flag = false;
        return '';
      }
      return arg;
    });
    return flag ? str : '';
  };

  /**
   * Return an interger array of ascending range form 'form' to 'to'.
   * @param {Number} form
   * @param {Number} to
   * @return {Array}
   */
  var makeRange = function (from, to) {
    // 保证 from <= to
    if (from > to) {
      from = from + to;
      to = from - to;
      from = from - to;
    }

    var res = [];
    for (var i = from; i <= to; i++) {
      res.push(i);
    }
    return res;
  };

  var makeMatrix = function (startCoord, endCoord) {
    var matrix = {};
    var colArr = makeRange(startCoord[1], endCoord[1]);
    var fromRow = startCoord[0] < endCoord[0] ? startCoord[0] : endCoord[0];
    var steps = Math.abs(startCoord[0] - endCoord[0]) + 1;
    for (var i = 0; i < steps; i++) {
      matrix[fromRow + i] = colArr.slice(0);
    }
    return matrix;
  };

  /**
   * Merge to arrays, return an new array.
   * @param {Array} origin
   * @param {Array} addition
   */
  var mergeArray = function (origin, addition) {
    var hash = {};
    var res = [];

    origin.forEach(function (item, i) {
      hash[item] = 1;
      res[i] = item;
    });

    addition.forEach(function (item) {
      if (!hash[item]) {
        res.push(item);
      }
    });

    return res.sort(function (num1, num2) {
      return num1 - num2;
    });
  };

  /**
   * 去当前数组中去除指定集合，返回新数组
   * @param {Array} origin 原数组
   * @param {Array} reject 要去除的数组
   */
  var rejectArray = function (origin, reject) {
    var hash = {};
    var res = [];

    reject.forEach(function (item, i) {
      hash[item] = i;
    });

    origin.forEach(function (item) {
      if (!hash.hasOwnProperty(item)) {
        res.push(item);
      }
    });

    return res.sort(function (num1, num2) {
      return num1 - num2;
    });
  };

  // 选择模式
  var SelectMode = {
    JOIN: 1, // 合并模式，添加到选区
    MINUS: 2, // 减去模式，从之前的选区中减去
    REPLACE: 3, // 替换模式，弃用之前的选区，直接使用给定的选区作为最终值
    NONE: 0
  };

  var Scheduler = function (el, options) {
    this.$el = $(el);
    if (!this.$el.is('table')) {
      this.$el = $('<table></table>').appendTo(this.$el);
    }

    // 自定义项
    this.options = options;
    // 选择模式
    this.selectMode = SelectMode.NONE;
    this.startCoord = null;
    this.endCoord = null;
    // 控件的数据对象，所有操作不会更改 this.options.data
    this.data = $.extend(true, {}, this.options.data);
    this.init();
  };

  // 默认项
  Scheduler.DEFAULTS = {
    locale: 'en', // i18n
    accuracy: 1, // how many cells of an hour
    data: [], // selected cells
    footer: true,
    multiple: true,
    disabled: false,
    // event callbacks
    onDragStart: $.noop,
    onDragMove: $.noop,
    onDragEnd: $.noop,
    onSelect: $.noop,
    onRender: $.noop
  };

  // Language
  Scheduler.LOCALES = {};

  // Simplified Chinese
  Scheduler.LOCALES['zh-cn'] = Scheduler.LOCALES.zh = {
    AM: '上午',
    PM: '下午',
    TIME_TITLE: '时间',
    WEEK_TITLE: '星期',
    WEEK_DAYS: ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'],
    DRAG_TIP: '可拖动鼠标选择时间段',
    RESET: '清空选择'
  };

  // English
  Scheduler.LOCALES['en-US'] = Scheduler.LOCALES.en = {
    AM: 'AM',
    PM: 'PM',
    TIME_TITLE: 'TIME',
    WEEK_TITLE: 'DAY',
    WEEK_DAYS: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'],
    DRAG_TIP: 'Drag to select hours',
    RESET: 'Reset Selected'
  };

  // Template
  Scheduler.TEMPLATES = {
    HALF_DAY_ROW: '<tr>' +
      '<th rowspan="2" class="slash">' +
      '<div class="scheduler-time-title">%s</div>' +
      '<div class="scheduler-week-title">%s</div>' +
      '</th>' +
      '<th class="scheduler-half-toggle" data-half-toggle="1" colspan="%s">%s</th>' +
      '<th class="scheduler-half-toggle" data-half-toggle="2" colspan="%s">%s</th>' +
      '</tr>',
    HOUR_HEAD_CELL: '<th class="scheduler-hour-toggle" data-hour-toggle="%s" colspan="%s">%s</th>',
    DAY_ROW: '<tr data-index="%s"><td class="scheduler-day-toggle" data-day-toggle="%s">%s</td>%s</tr>',
    HOUR_CELL: '<td class="scheduler-hour%s" data-row="%s" data-col="%s"></td>',
    FOOT_ROW: '<tr><td colspan="%s"><div class="scheduler-control-wrap"><span class="scheduler-tips">%s</span><a class="scheduler-reset">%s</a></div></td></tr>'
  };

  // Util
  Scheduler.Util = {
    parse: parse,
    serialize: serialize
  };

  var proto = Scheduler.prototype;

  proto.init = function () {
    this.initLocale();
    this.initTable();
    this.options.onRender.call(this.$el);
  };

  proto.initLocale = function () {
    var me = this;
    if (me.options.locale) {
      var parts = me.options.locale.toLowerCase().split(/-|_/);
      if (parts[1]) {
        parts[1] = parts[1].toUpperCase();
      }
      if ($.fn.scheduler.locales[me.options.locale]) {
        // locale as requested
        $.extend(me.options, $.fn.scheduler.locales[me.options.locale]);
      } else if ($.fn.scheduler.locales[parts.join('-')]) {
        // locale with sep set to - (in case original was specified with _)
        $.extend(me.options, $.fn.scheduler.locales[parts.join('-')]);
      } else if ($.fn.scheduler.locales[parts[0]]) {
        // short locale language code (i.e. 'en')
        $.extend(me.options, $.fn.scheduler.locales[parts[0]]);
      }
    }
  };

  proto.initTable = function () {
    this.$el.addClass('scheduler');
    if (this.options.disabled) {
      this.$el.addClass('scheduler-disabled');
    }
    this.initHead();
    this.initBody();
    if (this.options.footer) {
      this.initFoot();
    }
  };

  proto.initHead = function () {
    var me = this;
    me.$head = me.$el.find('>thead');
    if (!me.$head.length) {
      me.$head = $('<thead></thead>').appendTo(me.$el);
    }
    me.$head.append(me.getHeadHtml());

    // toggle select half day
    me.$head.on('click', '.scheduler-half-toggle', me.onToggleHalfDay.bind(me));
    // toggle select an hour
    me.$head.on('click', '.scheduler-hour-toggle', me.onToggleHour.bind(me));
  };

  proto.initBody = function () {
    var me = this;

    me.$body = me.$el.find('>tbody');
    if (!me.$body.length) {
      me.$body = $('<tbody></tbody>').appendTo(me.$el);
    }
    me.$body.append(me.getBodyHtml(me.options.data));

    // toggle select day
    me.$body.on('click', '.scheduler-day-toggle', me.onToggleDay.bind(me));
    // range toggle select hour
    me.$body.on('mousedown', '.scheduler-hour', me.onMouseDown.bind(me))
      .on('mousemove', '.scheduler-hour', me.onMouseMove.bind(me))
      .on('mouseup', '.scheduler-hour', me.onMouseUp.bind(me));
  };

  proto.initFoot = function () {
    var me = this;
    me.$foot = me.$el.find('>tfoot');
    if (!me.$foot.length) {
      me.$foot = $('<tfoot></tfoot>').appendTo(me.$el);
    }
    me.$foot.append(me.getFootHtml());
    me.$foot.on('click', '.scheduler-reset', me.onReset.bind(me));
  };

  proto.getHeadHtml = function (data) {
    var me = this;
    var options = me.options;
    me.$head.append(sprintf($.fn.scheduler.templates.HALF_DAY_ROW,
                            options.TIME_TITLE, // title: time
                            options.WEEK_TITLE, // title: week
                            me.options.accuracy * 12, // row span
                            options.AM, // title: 上午
                            me.options.accuracy * 12, // row span
                            options.PM // title: 下午
                           ));

    var hours = '';
    for (var i = 0; i < 24; i++) {
      hours += sprintf($.fn.scheduler.templates.HOUR_HEAD_CELL,
                       i, // hour indexs
                       options.accuracy, // row span
                       i // hour text
                      );
    }
    return sprintf('<tr>%s</tr>', hours);
  };

  proto.getFootHtml = function () {
    var me = this;
    var options = me.options;
    return sprintf(
      $.fn.scheduler.templates.FOOT_ROW,
      options.accuracy * 24 + 1,
      options.DRAG_TIP,
      options.RESET
    );
  };

  proto.getBodyHtml = function (data) {
    var me = this;
    var options = me.options;
    var rows = '';
    var cellOfRow = options.accuracy * 24;

    for (var i = 1; i <= 7; i++) {
      var cells = '';
      var selectedHours = data[i];
      for (var j = 0; j < cellOfRow; j++) {
        cells += sprintf(
          $.fn.scheduler.templates.HOUR_CELL,
          selectedHours && ~selectedHours.indexOf(j) ? ' scheduler-active' : '',
          i,
          j
        );
      }
      rows += sprintf(
        $.fn.scheduler.templates.DAY_ROW,
        i,
        i,
        options.WEEK_DAYS[i - 1],
        cells
      );
    }
    
    return rows;
  };

  // toggle select one day
  proto.onToggleDay = function (e) {
    if (this.options.disabled) {
      return;
    }
    var index = $(e.target).parent().data('index');
    var startCoord = [index, 0]; // [row, col] row start form 1
    var endCoord = [index, 24 * this.options.accuracy - 1];
    var selectMode = this.getRangeSelectMode(startCoord, endCoord);
    this.updateToggle(startCoord, endCoord, selectMode);
  };

  // toggle select half day
  proto.onToggleHalfDay = function (e) {
    if (this.options.disabled) {
      return;
    }
    var index = $(e.target).data('halfToggle'); // index = 1 | 2
    var fromIndex = (index - 1) * 12 * this.options.accuracy;
    var toIndex = fromIndex + 12 * this.options.accuracy - 1;
    var startCoord = [1, fromIndex]; // [row, col] row start form 1
    var endCoord = [7, toIndex];
    var selectMode = this.getRangeSelectMode(startCoord, endCoord);
    this.updateToggle(startCoord, endCoord, selectMode);
  };

  // toggle select an hour
  proto.onToggleHour = function (e) {
    if (this.options.disabled) {
      return;
    }
    var index = $(e.target).data('hourToggle'); // index = 1 | 2
    var fromIndex = index * this.options.accuracy;
    var toIndex = fromIndex + this.options.accuracy - 1;
    var startCoord = [1, fromIndex]; // [row, col] row start form 1
    var endCoord = [7, toIndex];
    var selectMode = this.getRangeSelectMode(startCoord, endCoord);
    this.updateToggle(startCoord, endCoord, selectMode);
  };

  proto.onMouseDown = function (e) {
    if (this.options.disabled) {
      return;
    }
    this.moving = true;
    var $cell = $(e.target);
    this.startCoord = [$cell.data('row'), $cell.data('col')];
    this.endCoord = this.startCoord.slice(0);
    this.selectMode = this.getCellSelectMode(this.startCoord);
    this.updateRange(this.startCoord, this.endCoord, this.selectMode);
    this.options.onDragStart.call(this.$el, this.cache);
  };

  proto.onMouseMove = function (e) {
    if (!this.moving) {
      return false;
    }
    var $cell = $(e.target);
    var row = $cell.data('row');
    var col = $cell.data('col');
    if (!this.selectMode || !this.startCoord || (this.endCoord &&
                                                 this.endCoord[0] === row &&
                                                 this.endCoord[1] === col)
       ) {
      return false;
    }
    this.endCoord = [$cell.data('row'), $cell.data('col')];
    this.updateRange(this.startCoord, this.endCoord, this.selectMode);
    this.options.onDragMove.call(this.$el, this.cache);
  };

  proto.onMouseUp = function (e) {
    if (!this.moving) {
      return false;
    }
    // 起始点都在同一个位置
    if (this.startCoord[0] === this.endCoord[0] &&
        this.startCoord[1] === this.endCoord[1]) {
      this.updateRange(this.startCoord, this.endCoord, this.selectMode);
    }
    this.options.onDragEnd.call(this.$el, this.cache);
    this.end();
  };

  /**
   * 根据当前的选中坐标系更新视图，并更新选中数据
   * @param {Array} startCoord 起始坐标 [row, col]
   * @param {Array} endCoord 终结坐标 [row, col]
   * @param {SelectMode} selectMode 选择模式
   */
  proto.updateToggle = function (startCoord, endCoord, selectMode) {
    this.updateRange(startCoord, endCoord, selectMode);
    this.end();
  };

  /**
   * 根据当前的选中坐标系更新视图
   * @param {Array} startCoord 起始坐标 [row, col]
   * @param {Array} endCoord 终结坐标 [row, col]
   * @param {SelectMode} selectMode 选择模式
   */
  proto.updateRange = function (startCoord, endCoord, selectMode) {
    var currentSelectRange = makeMatrix(startCoord, endCoord);
    this.cache = this.merge(this.data, currentSelectRange, selectMode);
    this.update(this.cache);
  };

  /**
   * 更新视图
   * @param {Object} data 选中的时间集合
   */
  proto.update = function (data) {
    this.$body.html(this.getBodyHtml(data));
  };

  // 并更新选中数据
  proto.end = function () {
    this.data = this.cache;
    this.cache = null;
    this.moving = false;
    this.startCoord = null;
    this.endCoord = null;
    this.selectMode = SelectMode.NONE;
    this.options.onSelect.call(this.$el, this.val());
  };

  proto.onReset = function () {
    if (this.options.disabled) {
      return;
    }
    this.val({});
    this.options.onSelect.call(this.$el, this.val());
  };

  /**
   * 根据选择模式合并合个集合
   * @param {Object} origin 上一次选中的数据
   * @param {Object} current 当前选中的数据
   * @param {Number} selectMode 选择模式 {0: none, 1: 选择（合并）模式, 2: 排除模式（从选区中减去）}
   */
  proto.merge = function (origin, current, selectMode) {
    var res = {};
    // 替换模式下，弃用之前的选区，直接使用当前选区
    if (selectMode === SelectMode.REPLACE) {
      for (var i = 1; i <= 7; i++) {
        if (current[i] && current[i].length) {
          res[i] = current[i].slice(0);
        }
      }
      return res;
    }
    for (var i = 1; i <= 7; i++) {
      if (!current[i]) {
        if (origin[i] && origin[i].length) {
          res[i] = origin[i].slice(0);
        }
        continue;
      }
      if (origin[i] && origin[i].length) {
        var m = selectMode === SelectMode.JOIN ?
          mergeArray(origin[i], current[i]) :
          rejectArray(origin[i], current[i]);
        m.length && (res[i] = m);
      } else if (selectMode === SelectMode.JOIN) {
        res[i] = current[i].slice(0);
      }
    }
    return res;
  };

  /**
   * 根据当前选中的范围内时间格式的空闲情况，决定是全选还是全不选
   * 全空闲：总时间格目 === 空闲时间格数目
   * 部分空闲：总时间格目 !== 空闲时间格数目
   * 无空闲：空闲时间格数目 === 0
   * 状态切换：
   * 当前范围全空闲 > 采用合并模式，全选当前范围
   * 部分空闲 > 采用合并模式，全选当前范围
   * 无空闲 > 采用合并模式，全不选当前范围
   *
   * @param {Array} startCoord 起始坐标 [row, col]
   * @param {Array} endCoord 终结坐标 [row, col]
   * @return {SelectMode}
   */
  proto.getRangeSelectMode = function (startCoord, endCoord) {
    if (!this.options.multiple) {
      return SelectMode.REPLACE;
    }
    var rowRange = this.sortCoord(startCoord[0], endCoord[0]);
    var colRange = this.sortCoord(startCoord[1], endCoord[1]);
    var startRow = rowRange[0];
    var endRow = rowRange[1];
    var startCol = colRange[0];
    var endCol = colRange[1];
    var rows = endRow - startRow + 1;
    var cols = endCol - startCol + 1;
    var total = rows * cols;

    // 计算已使用的时间格子
    // TODO 未过滤 disabled 的格子
    var used = 0;
    for (var i = 0; i < rows; i++) {
      var day = startRow + i;
      var data = this.data[day];
      if (!data) {
        continue;
      }
      for (var j = 0; j < data.length; j++) {
        if (data[j] >= startCol && data[j] <= endCol) {
          used++;
        }
      }
    }

    return total === used ? SelectMode.MINUS : SelectMode.JOIN;
  };

  /**
   * 根据当前选中的时间格式的空闲情况，决定是全选还是全不选
   * 状态切换：
   * 当前为单选模式(multiple=false)，-> 替换模式
   * 当前选中时间段为空闲 -> 全选不
   * 当前选中时间段为无空闲 - > 全不选
   *
   * @param {Array} startCoord 起始坐标 [row, col]
   * @return {SelectMode}
   */
  proto.getCellSelectMode = function (coord) {
    if (!this.options.multiple) {
      return SelectMode.REPLACE;
    }
    // TODO 未过滤 disabled 的格子
    var day = this.data[coord[0]];
    return day && ~day.indexOf(coord[1]) ? SelectMode.MINUS : SelectMode.JOIN;
  };

  proto.sortCoord = function (num1, num2) {
    if (num1 > num2) {
      return [num2, num1];
    }
    return [num1, num2];
  };

  proto.disable = function () {
    this.$el.toggleClass('scheduler-disabled', true);
    this.options.disabled = true;
  };

  proto.enable = function () {
    this.$el.toggleClass('scheduler-disabled', false);
    this.options.disabled = false;
  };

  /**
   * 如果无传参，则作为 Getter, 否则为 Setter
   * @param {Array} data optional 选中的内容
   * @return {Array} 返回当前选中的内容
   */
  proto.val = function (data) {
    // setter
    if (toStr(data) === '[object Object]') {
      // TODO 数据结构校验
      this.data = data;
      this.update(data);

    } else { // getter
      return this.merge(this.data, {}, SelectMode.JOIN);
    }
  };

  proto.destroy = function () {
    this.$el.removeClass('scheduler').empty();
  };

  $.extend(Scheduler.DEFAULTS, Scheduler.LOCALES.zh);

  // SCHEDULER PLUGIN DEFINITION
  // ---------------------------

  var apiMethods = [
    'val',
    'destroy',
    'disable',
    'enable'
  ];

  // Set as a jQuery plugin
  $.fn.scheduler = function (option) {
    var value;
    var args = Array.prototype.slice.call(arguments, 1);

    this.each(function () {
      var $this = $(this);
      var data = $this.data('scheduler');
      var options = $.extend({}, Scheduler.DEFAULTS, $this.data(),
                         typeof option === 'object' && option);

      if (typeof option === 'string') {
        if ($.inArray(option, apiMethods) < 0) {
          throw new Error('Unknown method: ' + option);
        }

        if (!data) {
          return;
        }

        value = data[option].apply(data, args);

        if (option === 'destroy') {
          $this.removeData('scheduler');
        }
      }

      if (!data) {
        $this.data('scheduler', (data = new Scheduler(this, options)));
      }
    });

    return typeof value === 'undefined' ? this : value;
  };

  // Exports settings
  $.fn.scheduler.defaults = Scheduler.DEFAULTS;
  $.fn.scheduler.templates = Scheduler.TEMPLATES;
  $.fn.scheduler.locales = Scheduler.LOCALES;
  $.fn.scheduler.util = Scheduler.Util;
})(jQuery);
