text: 'use strict';
  
  var emitter = require('contra.emitter');
  var raf = require('raf');
  var dom = require('./dom');
  var text = require('./text');
  var parse = require('./parse');
  var clone = require('./clone');
  var defaults = require('./defaults');
  var momentum = require('./momentum');
  var classes = require('./classes');
  var events = require('./events');
  var noop = require('./noop');
  var no;
  
  function calendar (calendarOptions) {
    var o;
    var api = emitter({});
    var ref;
    var refCal;
    var container;
    var rendered = false;
  
    // date variables
    var monthOffsetAttribute = 'data-rome-offset';
    var weekdays = momentum.moment.weekdaysMin();
    var weekdayCount = weekdays.length;
    var calendarMonths = [];
    var lastYear;
    var lastMonth;
    var lastDay;
    var lastDayElement;
    var datewrapper;
    var back;
    var next;
  
    // time variables
    var secondsInDay = 60 * 60 * 24;
    var time;
    var timelist;
  
    destroy(true);
  
    raf(function () {
      init();
    });
  
    function napi () { return api; }
  
    function init (initOptions) {
      o = defaults(initOptions || calendarOptions, api);
      if (!container) { container = dom({ className: o.styles.container }); }
      lastMonth = no;
      lastYear = no;
      lastDay = no;
      lastDayElement = no;
      o.appendTo.appendChild(container);
  
      removeChildren(container);
      rendered = false;
      ref = o.initialValue ? o.initialValue : momentum.moment();
      refCal = ref.clone();
  
      api.container = container;
      api.destroyed = false;
      api.destroy = destroy.bind(api, false);
      api.emitValues = emitValues;
      api.getDate = getDate;
      api.getDateString = getDateString;
      api.getMoment = getMoment;
      api.hide = hide;
      api.options = changeOptions;
      api.options.reset = resetOptions;
      api.refresh = refresh;
      api.restore = napi;
      api.setValue = setValue;
      api.show = show;
  
      hideCalendar();
      eventListening();
  
      api.emit('ready', clone(o));
  
      return api;
    }
  
    function destroy (silent) {
      if (container) {
        container.parentNode.removeChild(container);
      }
  
      if (o) {
        eventListening(true);
      }
  
      api.destroyed = true;
      api.destroy = napi;
      api.emitValues = napi;
      api.getDate = noop;
      api.getDateString = noop;
      api.getMoment = noop;
      api.hide = napi;
      api.options = napi;
      api.options.reset = napi;
      api.refresh = napi;
      api.restore = init;
      api.setValue = napi;
      api.show = napi;
  
      if (silent !== true) {
        api.emit('destroyed');
      }
      api.off();
  
      return api;
    }
  
    function eventListening (remove) {
      var op = remove ? 'remove' : 'add';
      if (o.autoHideOnBlur) { events[op](document, 'focusin', hideOnBlur); }
      if (o.autoHideOnClick) { events[op](document, 'click', hideOnClick); }
    }
  
    function changeOptions (options) {
      if (arguments.length === 0) {
        return clone(o);
      }
      destroy();
      init(options);
      return api;
    }
  
    function resetOptions () {
      return changeOptions({});
    }
  
    function render () {
      if (rendered) {
        return;
      }
      rendered = true;
      renderDates();
      renderTime();
      api.emit('render');
    }
  
    function renderDates () {
      if (!o.date) {
        return;
      }
      var i;
      calendarMonths = [];
  
      datewrapper = dom({ className: o.styles.date, parent: container });
  
      for (i = 0; i < o.monthsInCalendar; i++) {
        renderMonth(i);
      }
  
      events.add(back, 'click', subtractMonth);
      events.add(next, 'click', addMonth);
      events.add(datewrapper, 'click', pickDay);
  
      function renderMonth (i) {
        var month = dom({ className: o.styles.month, parent: datewrapper });
        if (i === 0) {
          back = dom({ type: 'button', className: o.styles.back, attributes: { type: 'button' }, parent: month });
        }
        if (i === o.monthsInCalendar -1) {
          next = dom({ type: 'button', className: o.styles.next, attributes: { type: 'button' }, parent: month });
        }
        var label = dom({ className: o.styles.monthLabel, parent: month });
        var date = dom({ type: 'table', className: o.styles.dayTable, parent: month });
        var datehead = dom({ type: 'thead', className: o.styles.dayHead, parent: date });
        var dateheadrow = dom({ type: 'tr', className: o.styles.dayRow, parent: datehead });
        var datebody = dom({ type: 'tbody', className: o.styles.dayBody, parent: date });
        var j;
  
        for (j = 0; j < weekdayCount; j++) {
          dom({ type: 'th', className: o.styles.dayHeadElem, parent: dateheadrow, text: weekdays[weekday(j)] });
        }
  
        datebody.setAttribute(monthOffsetAttribute, i);
        calendarMonths.push({
          label: label,
          body: datebody
        });
      }
    }
  
    function renderTime () {
      if (!o.time || !o.timeInterval) {
        return;
      }
      var timewrapper = dom({ className: o.styles.time, parent: container });
      time = dom({ className: o.styles.selectedTime, parent: timewrapper, text: ref.format(o.timeFormat) });
      events.add(time, 'click', toggleTimeList);
      timelist = dom({ className: o.styles.timeList, parent: timewrapper });
      events.add(timelist, 'click', pickTime);
      var next = momentum.moment('00:00:00', 'HH:mm:ss');
      var latest = next.clone().add(1, 'days');
      while (next.isBefore(latest)) {
        dom({ className: o.styles.timeOption, parent: timelist, text: next.format(o.timeFormat) });
        next.add(o.timeInterval, 'seconds');
      }
    }
  
    function weekday (index, backwards) {
      var factor = backwards ? -1 : 1;
      var offset = index + o.weekStart * factor;
      if (offset >= weekdayCount || offset < 0) {
        offset += weekdayCount * -factor;
      }
      return offset;
    }
  
    function displayValidTimesOnly () {
      if (!o.time || !rendered) {
        return;
      }
      var times = timelist.children;
      var length = times.length;
      var date;
      var time;
      var item;
      var i;
      for (i = 0; i < length; i++) {
        item = times[i];
        time = momentum.moment(text(item), o.timeFormat);
        date = setTime(ref.clone(), time);
        item.style.display = isInRange(date, false, o.timeValidator) ? 'block' : 'none';
      }
    }
  
    function toggleTimeList (show) {
      var display = typeof show === 'boolean' ? show : timelist.style.display === 'none';
      if (display) {
        showTimeList();
      } else {
        hideTimeList();
      }
    }
  
    function showTimeList () { if (timelist) { timelist.style.display = 'block'; } }
    function hideTimeList () { if (timelist) { timelist.style.display = 'none'; } }
    function showCalendar () { container.style.display = 'inline-block'; api.emit('show'); }
    function hideCalendar () { container.style.display = 'none'; api.emit('hide'); }
  
    function show () {
      render();
      refresh();
      toggleTimeList(!o.date);
      showCalendar();
      return api;
    }
  
    function hide () {
      hideTimeList();
      raf(hideCalendar);
      return api;
    }
  
    function hideConditionally () {
      hideTimeList();
  
      var pos = classes.contains(container, o.styles.positioned);
      if (pos) {
        raf(hideCalendar);
      }
      return api;
    }
  
    function calendarEventTarget (e) {
      var target = e.target;
      if (target === api.associated) {
        return true;
      }
      while (target) {
        if (target === container) {
          return true;
        }
        target = target.parentNode;
      }
    }
  
    function hideOnBlur (e) {
      if (calendarEventTarget(e)) {
        return;
      }
      hideConditionally();
    }
  
    function hideOnClick (e) {
      if (calendarEventTarget(e)) {
        return;
      }
      hideConditionally();
    }
  
    function subtractMonth () { changeMonth('subtract'); }
    function addMonth () { changeMonth('add'); }
    function changeMonth (op) {
      var bound;
      var direction = op === 'add' ? -1 : 1;
      var offset = o.monthsInCalendar + direction * getMonthOffset(lastDayElement);
      refCal[op]('months', offset);
      bound = inRange(refCal.clone());
      ref = bound || ref;
      if (bound) { refCal = bound.clone(); }
      update();
    }
  
    function update (silent) {
      updateCalendar();
      updateTime();
      if (silent !== true) { emitValues(); }
      displayValidTimesOnly();
    }
  
    function updateCalendar () {
      if (!o.date || !rendered) {
        return;
      }
      var y = refCal.year();
      var m = refCal.month();
      var d = refCal.date();
      if (d === lastDay && m === lastMonth && y === lastYear) {
        return;
      }
      var canStay = isDisplayed();
      lastDay = refCal.date();
      lastMonth = refCal.month();
      lastYear = refCal.year();
      if (canStay) { updateCalendarSelection(); return; }
      calendarMonths.forEach(updateMonth);
      renderAllDays();
  
      function updateMonth (month, i) {
        var offsetCal = refCal.clone().add(i, 'months');
        text(month.label, offsetCal.format(o.monthFormat));
        removeChildren(month.body);
      }
    }
  
    function updateCalendarSelection () {
      var day = refCal.date() - 1;
      selectDayElement(false);
      calendarMonths.forEach(function (cal) {
        var days;
        if (sameCalendarMonth(cal.date, refCal)) {
          days = cast(cal.body.children).map(aggregate);
          days = Array.prototype.concat.apply([], days).filter(inside);
          selectDayElement(days[day]);
        }
      });
  
      function cast (like) {
        var dest = [];
        var i;
        for (i = 0; i < like.length; i++) {
          dest.push(like[i]);
        }
        return dest;
      }
  
      function aggregate (child) {
        return cast(child.children);
      }
  
      function inside (child) {
        return !classes.contains(child, o.styles.dayPrevMonth) &&
               !classes.contains(child, o.styles.dayNextMonth);
      }
    }
  
    function isDisplayed () {
      return calendarMonths.some(matches);
  
      function matches (cal) {
        if (!lastYear) { return false; }
        return sameCalendarMonth(cal.date, refCal);
      }
    }
  
    function sameCalendarMonth (left, right) {
      return left && right && left.year() === right.year() && left.month() === right.month();
    }
  
    function updateTime () {
      if (!o.time || !rendered) {
        return;
      }
      text(time, ref.format(o.timeFormat));
    }
  
    function emitValues () {
      api.emit('data', getDateString());
      api.emit('year', ref.year());
      api.emit('month', ref.month());
      api.emit('day', ref.day());
      api.emit('time', ref.format(o.timeFormat));
      return api;
    }
  
    function refresh () {
      lastYear = false;
      lastMonth = false;
      lastDay = false;
      update(true);
      return api;
    }
  
    function setValue (value) {
      var date = parse(value, o.inputFormat);
      if (date === null) {
        return;
      }
      ref = inRange(date) || ref;
      refCal = ref.clone();
      update(true);
  
      return api;
    }
  
    function removeChildren (elem, self) {
      while (elem && elem.firstChild) {
        elem.removeChild(elem.firstChild);
      }
      if (self === true) {
        elem.parentNode.removeChild(elem);
      }
    }
  
    function renderAllDays () {
      var i;
      for (i = 0; i < o.monthsInCalendar; i++) {
        renderDays(i);
      }
    }
  
    function renderDays (offset) {
      var month = calendarMonths[offset];
      var offsetCal = refCal.clone().add(offset, 'months');
      var total = offsetCal.daysInMonth();
      var current = offsetCal.month() !== ref.month() ? -1 : ref.date(); // -1 : 1..31
      var first = offsetCal.clone().date(1);
      var firstDay = weekday(first.day(), true); // 0..6
      var tr = dom({ type: 'tr', className: o.styles.dayRow, parent: month.body });
      var prevMonth = hiddenWhen(offset !== 0, [o.styles.dayBodyElem, o.styles.dayPrevMonth]);
      var nextMonth = hiddenWhen(offset !== o.monthsInCalendar - 1, [o.styles.dayBodyElem, o.styles.dayNextMonth]);
      var disabled = o.styles.dayDisabled;
      var lastDay;
  
      part({
        base: first.clone().subtract(firstDay, 'days'),
        length: firstDay,
        cell: prevMonth
      });
  
      part({
        base: first.clone(),
        length: total,
        cell: [o.styles.dayBodyElem],
        selectable: true
      });
  
      lastDay = first.clone().add(total, 'days');
  
      part({
        base: lastDay,
        length: weekdayCount - tr.children.length,
        cell: nextMonth
      });
  
      back.disabled = !isInRange(first, true);
      next.disabled = !isInRange(lastDay, true);
      month.date = offsetCal.clone();
  
      function part (data) {
        var i, day, node;
        for (i = 0; i < data.length; i++) {
          if (tr.children.length === weekdayCount) {
            tr = dom({ type: 'tr', className: o.styles.dayRow, parent: month.body });
          }
          day = data.base.clone().add(i, 'days');
          node = dom({
            type: 'td',
            parent: tr,
            text: day.format(o.dayFormat),
            className: validationTest(day, data.cell.join(' ').split(' ')).join(' ')
          });
          if (data.selectable && day.date() === current) {
            selectDayElement(node);
          }
        }
      }
  
      function validationTest (day, cell) {
        if (!isInRange(day, true, o.dateValidator)) { cell.push(disabled); }
        return cell;
      }
  
      function hiddenWhen (value, cell) {
        if (value) { cell.push(o.styles.dayConcealed); }
        return cell;
      }
    }
  
    function isInRange (date, allday, validator) {
      var min = !o.min ? false : (allday ? o.min.clone().startOf('day') : o.min);
      var max = !o.max ? false : (allday ? o.max.clone().endOf('day') : o.max);
      if (min && date.isBefore(min)) {
        return false;
      }
      if (max && date.isAfter(max)) {
        return false;
      }
      var valid = (validator || Function.prototype).call(api, date.toDate());
      return valid !== false;
    }
  
    function inRange (date) {
      if (o.min && date.isBefore(o.min)) {
        return inRange(o.min.clone());
      } else if (o.max && date.isAfter(o.max)) {
        return inRange(o.max.clone());
      }
      var value = date.clone().subtract(1, 'days');
      if (validateTowards(value, date, 'add')) {
        return inTimeRange(value);
      }
      value = date.clone();
      if (validateTowards(value, date, 'subtract')) {
        return inTimeRange(value);
      }
    }
  
    function inTimeRange (value) {
      var copy = value.clone().subtract(o.timeInterval, 'seconds');
      var times = Math.ceil(secondsInDay / o.timeInterval);
      var i;
      for (i = 0; i < times; i++) {
        copy.add(o.timeInterval, 'seconds');
        if (copy.date() > value.date()) {
          copy.subtract(1, 'days');
        }
        if (o.timeValidator.call(api, copy.toDate()) !== false) {
          return copy;
        }
      }
    }
  
    function validateTowards (value, date, op) {
      var valid = false;
      while (valid === false) {
        value[op](1, 'days');
        if (value.month() !== date.month()) {
          break;
        }
        valid = o.dateValidator.call(api, value.toDate());
      }
      return valid !== false;
    }
  
    function pickDay (e) {
      var target = e.target;
      if (classes.contains(target, o.styles.dayDisabled) || !classes.contains(target, o.styles.dayBodyElem)) {
        return;
      }
      var day = parseInt(text(target), 10);
      var prev = classes.contains(target, o.styles.dayPrevMonth);
      var next = classes.contains(target, o.styles.dayNextMonth);
      var offset = getMonthOffset(target) - getMonthOffset(lastDayElement);
      ref.add(offset, 'months');
      if (prev || next) {
        ref.add(prev ? -1 : 1, 'months');
      }
      selectDayElement(target);
      ref.date(day); // must run after setting the month
      setTime(ref, inRange(ref) || ref);
      refCal = ref.clone();
      if (o.autoClose) { hideConditionally(); }
      update();
    }
  
    function selectDayElement (node) {
      if (lastDayElement) {
        classes.remove(lastDayElement, o.styles.selectedDay);
      }
      if (node) {
        classes.add(node, o.styles.selectedDay);
      }
      lastDayElement = node;
    }
  
    function getMonthOffset (elem) {
      var offset;
      while (elem && elem.getAttribute) {
        offset = elem.getAttribute(monthOffsetAttribute);
        if (typeof offset === 'string') {
          return parseInt(offset, 10);
        }
        elem = elem.parentNode;
      }
      return 0;
    }
  
    function setTime (to, from) {
      to.hour(from.hour()).minute(from.minute()).second(from.second());
      return to;
    }
  
    function pickTime (e) {
      var target = e.target;
      if (!classes.contains(target, o.styles.timeOption)) {
        return;
      }
      var value = momentum.moment(text(target), o.timeFormat);
      setTime(ref, value);
      refCal = ref.clone();
      emitValues();
      updateTime();
      if (!o.date && o.autoClose) {
        hideConditionally();
      } else {
        hideTimeList();
      }
    }
  
    function getDate () {
      return ref.toDate();
    }
  
    function getDateString (format) {
      return ref.format(format || o.inputFormat);
    }
  
    function getMoment () {
      return ref.clone();
    }
  
    return api;
  }
  
  module.exports = calendar;
  