var LONG_MONTHS = ["January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"];
var SHORT_MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug",
    "sep", "oct", "nov", "dec"];

var latestCalendar = null;
function initCalendar(id, apiUrl, startDate, singleLetterDayHeader, monthBelowGrid) {
  /* Start here to understand how to build a new calendar API. For example,
   * initCalendar("show_calendar", "/api/monthly_calendar/${show.slug}/"); */
  $(document).ready(function() {
    latestCalendar = new Calendar();
    latestCalendar.singleLetterDayHeader = singleLetterDayHeader;
    latestCalendar.monthBelowGrid = monthBelowGrid;
    latestCalendar.init(id, apiUrl, startDate);
  });
}

var calendarDataCache = {};
/* We try to load one month extra at all times so if the user isn't going
 * too fast, it looks like the information is ready instantly. Now let's say
 * the user clicks back and forth really fast. We don't want to continue
 * requesting the same data just because the first request hasn't finished
 * yet. Hence, when we're about to request, set the cache to an empty
 * value. Now let's say a user clicks around quickly and a request that
 * was pre-emptive finds itself the request we need now. We need to
 * call the callback. Hence, the callback is in charge of figuring out
 * if the data it gets back is what it requested most recently. */
function calendarData(apiUrl, date, callback) {
  var requestedYearMonth = [date.getFullYear(), date.getMonth() + 1]
  var requestedKey = requestedYearMonth.join("/");
  var previousYearMonth = correctYearMonth(requestedYearMonth[0],
      requestedYearMonth[1] - 1);
  var nextYearMonth = correctYearMonth(requestedYearMonth[0],
      requestedYearMonth[1] + 1);
  var keys = [previousYearMonth.join("/"), requestedKey,
      nextYearMonth.join("/")];

  var keyI = 0;
  var gotMonths = 0;
  if (calendarDataCache[requestedKey]) {
    callback(date, calendarDataCache[requestedKey]);
  }
  while (keyI < keys.length && !gotMonths) {
    if (!calendarDataCache[keys[keyI]]) {
      while (keyI + gotMonths < keys.length &&
          !calendarDataCache[keys[keyI + gotMonths]]) {
        calendarDataCache[keys[keyI + gotMonths]] = {};
        gotMonths++;
      }
      var completeUrl = apiUrl + keys[keyI] + "/" + gotMonths + "/";
      $.getJSON(completeUrl, function (json, status) {
        if ("success" == status) {
          for (var monthString in json) {
            var monthDate = dateFromString(monthString);
            var monthKey = monthDate.getFullYear() + "/" +
                (monthDate.getMonth() + 1);
            calendarDataCache[monthKey] = json[monthString];
            callback(monthDate, json[monthString]);
          }
        }
      });
    }
    keyI++;
  }
}

/* Month can be any wacky number. This function fixes it to be 1 <= month <= 12
 * and adjusts the year accordingly. */
function correctYearMonth(year, month) {
    var under = month < 1 ? Math.ceil((-1 * (month - 1)) / 12) : 0;
    var over = month > 12 ? Math.floor((month - 1) / 12) : 0;
    return [year - under + over, month + 12 * under - 12 * over];
}

function Calendar() {
  this.init = function(id, apiUrl, startDate) {
    this.dateElementLookup = {}; // Keys: days of month. Values: <td/>.
    this.calendarDataLookup = {}; // Keys: days of month. Values: details.
    this.apiUrl = apiUrl;
    this.calendarWrapper = $("#" + id);
    this.calendarWrapper.addClass("calendar");
    this.calendarTable = $("<table/>").appendTo(this.calendarWrapper);
    this.detailWrapper = $("<div/>").addClass("calendar_detail");
    this.shouldHideDetails = false;
    var me = this;
    this.detailWrapper.hover(function() {
      me.shouldHideDetails = false;
    }, function() {
      hideCalendarData(me);
    });
    this.detailWrapper.appendTo($("body"));
    this.calendarDetails = $("<div/>").addClass("calendar_detail_inner");
    this.calendarDetails.appendTo(this.detailWrapper);
    this.loadedMonth = monthFromUrl() || startDate || new Date();
    this.loadMonth(this.loadedMonth);
  };

  this.loadMonth = function(date) {
    this.loadedMonth = date;
    var year = date.getFullYear();
    var month = date.getMonth();
    var nextMonth = month + 1;
    var nextYear = year;
    var previousMonth = month - 1;
    var previousYear = year;
    if (nextMonth > 11) {
      nextMonth -= 12;
      nextYear++;
    } else if (previousMonth < 0) {
      previousMonth += 12;
      previousYear--;
    }
    var startDate = new Date(year, month, 1, 0, 0, 0, 0);
    var nextDate = new Date(nextYear, nextMonth, 1, 0, 0, 0, 0);
    var previousDate = new Date(previousYear, previousMonth, 1, 0, 0, 0, 0);
    var endDate = new Date();
    endDate.setTime(nextDate.getTime() - 24 * 60 * 60 * 1000);
    this.calendarTable.empty();

    var tHead = this.createTHead();
    this.createDayRow(tHead);
    if (!this.monthBelowGrid) {
      this.createMonthRow(tHead, previousDate, date, month, nextDate);
    }
    var tBody = this.createTBody();
    this.createDateRows(tBody, startDate, endDate);
    if (this.monthBelowGrid) {
      this.createMonthRow(tBody, previousDate, date, month, nextDate);
    }
    this.loadCalendarData(date);
  };

  this.setMonthLink = function(url) {
    this.monthLink.attr("href", url);
  };

  this.createMonthRow = function(parentElt, previousDate, date, month, nextDate) {
    var row = $("<tr/>").addClass("monthRow").appendTo(parentElt);
    var me = this;
    $("<td/>").html("&laquo;").appendTo(row).click(function() {
      me.loadMonth(previousDate);
    }).addClass("move").addClass("left");
    var year = date.getFullYear();
    var now = new Date();
    var future = now.getFullYear() < year ||
      (now.getFullYear() == year && now.getMonth() < month);
    var dateString = LONG_MONTHS[month] + " " + year;
    // Set the href when the data loads.
    this.monthLink = $("<a/>").html(dateString);
    // .attr("colspan", 5) doesn't work in IE6
    var td = $("<td colspan=\"5\"/>").addClass("month").appendTo(row);
    if (future) {
      td.html(dateString);
    } else {
      td.append(this.monthLink);
    }
    $("<td/>").html("&raquo;").appendTo(row).click(function() {
      me.loadMonth(nextDate);
    }).addClass("move").addClass("right");
  };

  this.createTHead = function() {
    return $("<thead/>").appendTo(this.calendarTable);
  };

  this.createDayRow = function(parentElt) {
    var dayRow = $("<tr/>").addClass("dayRow").appendTo(parentElt);
    var days = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
    if (this.singleLetterDayHeader) {
      var days = ["S", "M", "T", "W", "T", "F", "S"];
    }
    for (var dayI = 0; dayI < days.length; dayI++) {
      dayRow.append($("<td/>").html(days[dayI]));
    }
  };

  this.createTBody = function() {
    return $("<tbody/>").appendTo(this.calendarTable);
  };

  this.createDateRows = function(parentElt, startDate, endDate) {
    var daysPrintedSoFar = 0;
    var nextRow = $("<tr/>").addClass("dateRow").appendTo(parentElt);
    var keepGoing = true;
    this.dateElementLookup = {};
    var today = new Date();
    var pastMonth = endDate < today;
    var futureMonth = today < startDate;
    var thisMonth = !pastMonth && !futureMonth;
    while (keepGoing) {
      for (var i = 0; i < 7; i++) {
        if ((daysPrintedSoFar == 0 && i < startDate.getDay()) ||
            (daysPrintedSoFar >= endDate.getDate())) {
          nextRow.append($("<td/>").addClass("emptyDate").html("&nbsp;"));
        } else {
          daysPrintedSoFar++;
          var day = today.getDate();
          var cls = "future";
          if (pastMonth || (thisMonth && day > daysPrintedSoFar)) {
            cls = "past";
          } else if (thisMonth && day == daysPrintedSoFar) {
            cls = "today";
          }
          var td = $("<td/>").html(daysPrintedSoFar).appendTo(nextRow);
          this.dateElementLookup[daysPrintedSoFar] = td.addClass(cls);
        }
      }
      if (daysPrintedSoFar < endDate.getDate()) {
        var nextRow = $("<tr/>").addClass("dateRow").appendTo(parentElt);
      } else {
        keepGoing = false;
      }
    }
  }

  this.loadCalendarData = function(loadDate) {
    var me = this;
    calendarData(this.apiUrl, loadDate, function(dataDate, data) {
      /* These could come back in all kinds of crazy orders depending on how
       * fast the user clicks, what's loaded already, network issues, etc...
       * Hence, only load this data into the UI if it's for the month we're
       * on. */
      if (me.loadedMonth.getMonth() == dataDate.getMonth() &&
          me.loadedMonth.getYear() == dataDate.getYear()) {
        me.setMonthLink(data.url);
        for (var dateString in data.items) {
          var dataItem = data.items[dateString];
          dataItem.date = dateFromString(dateString);
          var day = dataItem.date.getDate();
          var element = me.dateElementLookup[day];
          me.calendarDataLookup[day] = dataItem;
          element.addClass("active");
          var div = $("<div/>").html(element.html())
          me.setupDayHover(element, day);
          element.html("");
          $("<a/>").attr("href", dataItem.url).append(div).appendTo(element);
        }
      }
    });
  };

  /* Here we juggle "this" references for hover, and the fact that day is
   * "by reference" and referenced within a closure created in a for loop. */
  this.setupDayHover = function(element, day) {
    var me = this;
    element.hover(function() {
      showCalendarData(me, day);
    }, function() {
      hideCalendarData(me);
    });
  };

  this.setCalendarDetails = function(detail) {
    var header = $("<h4/>").appendTo(this.calendarDetails);
    header.html(LONG_MONTHS[detail.date.getMonth()] + " " +
        detail.date.getDate() + " " + detail.date.getFullYear());
    this.appendCalendarDetails(detail, 1);
  };

  this.hasCalendarDetails = function(day) {
    var detail = this.calendarDataLookup[day];
    return detail.title ||
           (typeof detail.description != "undefined" && detail.description) ||
           typeof detail.items != "undefined";
  };

  this.appendCalendarDetails = function(detail, level) {
    if (detail.title) {
      var header = $("<h" + level + "/>").appendTo(this.calendarDetails);
      if (detail.url) {
        header.append($("<a/>").html(detail.title).attr("href", detail.url));
      } else {
        header.html(detail.title);
      }
    }
    if (typeof detail.description != "undefined" && detail.description) {
      var div = $("<div/>").addClass("level_" + level);
      div.strippedHtml(detail.description, 60).appendTo(this.calendarDetails);
    }
    if (typeof detail.items != "undefined") {
      for (var itemI = 0; itemI < detail.items.length; itemI++) {
        this.appendCalendarDetails(detail.items[itemI], level + 1);
      }
    }
  };
}

function showCalendarData(calendar, day) {
  calendar.shouldHideDetails = false;
  calendar.calendarDetails.html("");
  if (!calendar.hasCalendarDetails(day)) {
    return;
  }
  calendar.setCalendarDetails(calendar.calendarDataLookup[day]);
  var cell = calendar.dateElementLookup[day];
  var cellOffset = cell.offset();
  var cellTop = cellOffset.top;
  var cellLeft = cellOffset.left;
  var cellHeight = cell.outerHeight();
  var cellWidth = cell.outerWidth();
  var detailHeight = calendar.detailWrapper.outerHeight();
  var detailWidth = calendar.detailWrapper.outerWidth();
  var totalHeight = $(document).height();
  var totalWidth = $(document).width();
  var left = 0;
  var top = 0;
  if (cellLeft > totalWidth - cellLeft - cellWidth) {
    left = cellLeft - detailWidth;
  } else {
    left = cellLeft + cellWidth;
  }
  var topOffset = 20;
  if (totalHeight - (cellTop + topOffset) >= detailHeight) {
    top = cellTop + topOffset;
  } else if (cellTop + cellHeight >= detailHeight) {
    top = cellTop - (detailHeight - cellHeight);
  }
  calendar.detailWrapper.css({left: left, top: top});
}

function hideCalendarData(calendar) {
  calendar.shouldHideDetails = true;
  window.setTimeout(function() {
    if (calendar.shouldHideDetails) {
      calendar.detailWrapper.css({left: -999});
    }
  }, 250);
}

function monthFromUrl() {
  var path = window.location.href;
  var months = new RegExp(SHORT_MONTHS.join("|"));
  var match = path.match(months);
  if (match) {
    var shortMonth = match[0];
    var monthIndex = 0;
    while (SHORT_MONTHS[monthIndex] != shortMonth) {
      monthIndex++;
    }
    var yearSearch = /(19|20)\d\d/;
    match = path.match(yearSearch);
    if (match) {
      var year = match[0];
      return new Date(year, monthIndex, 1);
    }
  }
}

