(function() {

var Y = YAHOO;
var Dom = Y.util.Dom;
var Lang = Y.lang;
var DomEvent = Y.util.Event;
var DateMath = Y.widget.DateMath;
var JSON = Y.lang.JSON;

////////////////////////////////////////////////////////////////////////////////
// Define some utility routines
////////////////////////////////////////////////////////////////////////////////

function json_decode(s) {
  return JSON.parse(s);
}

function json_encode(o) {
  return JSON.stringify(o);
}

function post_encode(o) {
  var data = [];
  for (var k in o) if (o.hasOwnProperty(k)) {
   data.push(k + '=' + encodeURIComponent(o[k]));
  }
  data = data.length ? 
              data.join('&') : 
              null;
  return data;
}

function copyDate(dt) {
  return new Date(
    dt.getFullYear(), dt.getMonth(), dt.getDate(), 
    dt.getHours(), dt.getMinutes(), dt.getSeconds(),
    dt.getMilliseconds());
}

function getMonthName(dt) {
  var a = [
    'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
    'September', 'November', 'December'
  ];
  if (dt instanceof Date) {
    return a[dt.getMonth()];
  } 
  return null;
}

function tsToDate(s) {
  if (Lang.isString(s)) {
    return new Date(s.replace(/-/g, '/'));
  } else {
    return s;
  }
}

function dtToString(dt) {
  if (dt instanceof Date) {
    return new String(
      getMonthName(dt) + ' ' +
      dt.getDate() + ', ' + 
      dt.getFullYear()    
    );
  } else {
    throw new Error('dt must be an instance of Date');
  } 
}

/**
 * Take a JavaScript Date and turn it into a date string in YYYY-MM-DD format.
 */
function dtToDate(dt) {
  var m = dt.getMonth() + 1;
  var d = dt.getDate();
  return dt.getFullYear() + '-' + 
    (m < 10 ? '0' + m : m) + '-' + 
    (d < 10 ? '0' + d : d);
}

/**
 * Take a JavaScript Date and turn it into a time string in HH:MMaa format.
 */
function dtToTime(dt, tw, sec) {
  sec = sec || false;
  tw = tw == undefined ? true : tw;
  var time;
  var H = dt.getHours();
  var i = dt.getMinutes();
  var s = dt.getSeconds();
  var A;

  if (tw) {
    A = (H >= 12 && H <= 23) ? 'pm' : 'am';
    if (H > 12) {
      H = H % 12;
    } else if(H == 0) {
      H = 12;
    }
  }

  H = H < 10 ? '0' + H : H;
  i = i < 10 ? '0' + i : i;
  s = s < 10 ? '0' + s : s;

  if (sec) {
    time = H + ':' + i + ':' + s;
  } else {
    time = H + ':' + i;
  }

  if (tw) {
    time += A;
  }

  return time;
}

function dtToTs(dt, tw, sec) {
  return dtToDate(dt) + ' ' + dtToTime(dt, tw, sec);
}

function clearTime(dt) {
  if (dt instanceof Date) {
    return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); 
  } else {
    return dt;
  }
}

/**
 * Run a regular expression against the given parameter to verify that it is
 * a valid 12-hour time representation.
 *
 * @todo: expand this so it handles 24-hour time strings as well.
 */
function matchTime(t) {
  var r = 
    /^([0]*[1-9]|[1][0-2]):([0-5][0-9])\s*([aA]\.*[mM]\.*|[pP]\.*[mM]\.*)$/;
  return t.match(r);
}

/**
 * Run a regular expression agains the given parameter to verify that it is at
 * least a *sane* date representation.
 *
 * Note: Accepts dates of the year, month, day form only; however, the 
 * separator characters can be any non-word character, so 2008-01-01 and 
 * 2008/01/01 are both fine, as is 20080101.
 *
 * Important: This does not do any checking to ensure that it is a valid date, 
 * it only makes a first-line of defense against completely incorrect values.
 */
function matchDate(d) {
  var r = /^(\d{4})\W*(\d{1,2})\W*(\d{1,2})$/;
  return d.match(r);
}

/**
 * Given a start and end date, generate a calendar of all dates in-between.
 *
 * Note: The resultant calendar is inclusive (includes both start and end).
 * Note: Returns an array of JS Dates.
 */
function dateSequence(start, end) {
  throw new Exception('not implemented');
}

function configure(config, defaults) {
  var k;

  if (defaults) {
    for (k in defaults) if (defaults.hasOwnProperty(k)) {
      this[k] = defaults[k];
    }
  }

  if (config) {
    for (k in config) if (config.hasOwnProperty(k)) {
      this[k] = config[k];
    } 
  }
}

function insist(these) {
  for (var i=0; i<these.length; i += 1) {
    if (!this[these[i]]) {
      throw new Error(this + ' insists on having ' + these);
    }
  }
}

function request(o) {
  if (!o) {
    throw new Error('Object expected');
  }

  if (!o.uri) {
    throw new Error('URI Parameter expected');
  } else if (o.uri && !Lang.isString(o.uri)) {
    throw new Error('URI Parameter must be a string');
  }

  o.method = o.method || 'get';
  o.callback = o.callback || {};
  o.postData = o.data ? post_encode(o.data) : null;

  if (o.method.toLowerCase() == 'get') {
    o.uri += '?rnd=' + Number(new Date());
  }

  return Y.util.Connect.asyncRequest(
    o.method, o.uri, o.callback, o.postData
  );
}

////////////////////////////////////////////////////////////////////////////////
// The calendar module
////////////////////////////////////////////////////////////////////////////////

function Calendar(config) {
  this.init.call(this, config);
}

Calendar.calendars = [];

Calendar.prototype = {

  id: null,

  name: null,

  events: [],

  init: function (config) {
    configure.call(this, config);
    insist.call(this, ['name']);

    if (this.events instanceof Array) {
      var i, 
          events = [], 
          len = this.events.length;

      for (i=0; i<len; i += 1) {
        events.push(new Event(this.events[i]));
      } 

      this.events = events;
    }
    else {
      throw new Error('events attribute must be Array');
    }

    Calendar.calendars.push(this);
  },

  addEvent: function (event) {
    this.events.push(event);
  },

  removeEvent: function (event) {
    var events = [];
    for (var i=0; i<this.events.length; i += 1) {
      if (this.events[i] != event) {
        events.push(this.events[i]);
      }
    }
    this.events = events;
  },

  calculate: function(begin, end) {
    var i, 
        retval = [],
        events = this.events,
        len = this.events.length;
   
    if (end && begin) {
      begin = new Date(begin);
      end = new Date(end);
    } else if (begin && !end) {
      begin = new Date(begin);
      begin.setDate(1);
      end = YAHOO.widget.DateMath.findMonthEnd(begin);
    } else if (!begin && !end) {
      begin = new Date();
      begin.setDate(1);
      end = YAHOO.widget.DateMath.findMonthEnd(begin);
    } else {
      throw new Error('Invalid call signature.');
    }

    for (i=0; i<len; i += 1) {
      retval.push(events[i].instances(begin, end));
    }

    return retval;
  }
};

////////////////////////////////////////////////////////////////////////////////
// The event module
////////////////////////////////////////////////////////////////////////////////

function Event(config) {
  configure.call(this, config);
  insist.call(this, ['start', 'end', 'summary']);

  if (!this.id) {
    throw new Error(this + ' does not have an id');
  }

  if (typeof this.start == 'string') {
    this.start = new Date(this.start);
  }

  if (typeof this.end == 'string') {
    this.end = new Date(this.end);
  }

  if (this.recur && !(this.recur instanceof Recur)) {
    this.recur = new Recur(this.recur);
  }
}

Event.prototype = {
  id: null,
  start: null,
  end: null,
  summary: null,
  description: null,

  recur: null,

//  calendar: null,

  created: null,

  instances: function (begin, end) {
    var retval;

    if (this.recur) {
      retval = this.recur.calculate(this, begin, end);
    } else {
      // the event is a single occurrence so we only need check to see if the
      // event overlaps the time interval [begin, end] being requested. Namely,
      // if the start time is between begin and end inclusive or if the end is
      // strictly after begin.  If either of these are the case then we simply
      // return an array containing the single instance.
      if (this.start >= begin && this.start <= end) {
        retval = [this.toInstance()];
      } else if (this.end > begin) {
        retval = [this.toInstance()];
      } else {
        retval = [];
      }
    }

    return retval;
  },

  toInstance: function (start, end) {
    return new EventInstance({
      id: this.id,
      start: start || this.start,
      end: end || this.end,
      summary: this.summary,
      description: this.description,
      recur: this.recur//,
 //     calendar: this.calendar
    });
  },

  toString: function () {
    return 'Event';
  }
};

function EventInstance(config) {
  configure.call(this, config);
}

EventInstance.prototype = {
  toString: function () {
    return 'EventInstance';
  }
};

function Recur(config) {
  configure.call(this, config);

  insist.call(this, ['frequency']);

  this.interval = config.interval || 1; 

  if (this.until && this.count) {
    throw new Error('Cannot have both "until" and "count" set.');
  }

  if (this.until) {
    this.until = new Date(this.until);
  }
}

Recur.SU = 'SU';
Recur.MO = 'MO';
Recur.TU = 'TU';
Recur.WE = 'WE';
Recur.TH = 'TH';
Recur.FR = 'FR';
Recur.SA = 'SA';

Recur.prototype = {
  frequency: null,
  interval: null,

  count: null,
  until: null,

  byDay: null,
  byMonth: null,
  byMonthDay: null,
  byYearDay: null,
  wkst: null,

  toString: function () {
    return 'Recurrence';
  },

  calculate: function (event, range_begin, range_end) {
    if (!this.frequency || !(range_begin && range_end)) {
      return [event.toInstance()];
    }

    if (range_end < range_begin) {
      throw new Error('range_end cannot be before range_begin');
    }

    var retval = [];
    var start = new Date(event.start);
    var end = new Date(event.end);
    var diff = end - start;
    var i, len;
    var instance;

    function copy_time(a, b) {
      b.setHours(a.getHours());
      b.setMinutes(a.getMinutes());
      b.setSeconds(a.getSeconds());
      b.setMilliseconds(a.getMilliseconds());
    };

    /*function  day_of_week(dt) {
      return ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][dt.getDay()];
    };*/

    copy_time(start, range_begin);
    copy_time(start, range_end);

    while (start <= range_end) {

      if (this.until) {
        if (this.until < start) {
          break;
        }
      }

      if (this.count) {
        if (this.count <= retval.length) {
          break;
        }
      }

      if (start >= range_begin || end >= range_begin) {

        instance = event.toInstance(start, end); 
        retval.push(instance);

        /*if (this.byDay || this.byMonth) {

          if (this.byDay) {
            len = this.byDay.length;
            for (i=0; i<len; i += 1) {
              console.log(this.byDay[i]);
            }
          }

          if (this.byMonth) {
            len = this.byMonth.length;
            for (i=0; i<len; i += 1) {
              console.log(this.byMonth[i]);
            }
          }

        }
        else {
          retval.push(instance);
        }*/

      }

      start = this._add(start, this.frequency, this.interval);
      end = this._shift(start, diff);
    }

    return retval;
  },

  _add: function (d, f, i) {
    var DateMath = YAHOO.widget.DateMath;
    var tr = {
      DAILY: 'DAY',
      WEEKLY: 'WEEK',
      MONTHLY: 'MONTH',
      YEARLY: 'YEAR'     
    };
    return DateMath.add(d, DateMath[tr[f]], i);
  },

  _shift: function (dt, ms) {
    return new Date(Number(dt) + Number(ms)); 
  }
};

////////////////////////////////////////////////////////////////////////////////
// The EventCalendar module
////////////////////////////////////////////////////////////////////////////////

function EventCalendar(config) {
  this.init(config);
};

EventCalendar.Calendar = Calendar;
EventCalendar.Event = Event;

EventCalendar.prototype = {

  id: null,
  container: null,

  _dt_by_c: {}, // mapping cell ids to dates

  yCal: null,

  eventownerid: 'ec-eventowner',
  eventowner: null,

  quicknav: null,
  quicknavContainer: null,

  upcoming: null,
  upcomingContainer: null,

  calendars: null,
  calendarsContainer: null,

  previousMonthBtn: null,
  nextMonthBtn: null,

  viewPanel: null,
  addEventDlg: null,

  requests: {
    events: null,
    calendars: null,
    upcoming: null
  },

  data: {
    calendars: null,
    upcoming: null,
    events: null
  },

  eventsReady: null,

  init: function (config) {
    var yCal; // needed so embedded functions can access the 
              // this.yCal object without needing to modify
              // their scope. See the override of the OOM
              // renderer below.

    configure.call(this, config);

    // Locate various element ids and ensure they exist in the DOM
    this._locateDomIDs();

    // create the YUI Calendar widget for the main calendar
    this.id = this.id || Dom.generateId();
    this.yCal = new Y.widget.Calendar(this.id, this.container, {
      hide_blank_weeks: true
    });
    yCal = this.yCal;

    this._setMonthLabel(this.yCal.buildMonthLabel());

    // override the default behavior for OOM days
    this.yCal.renderCellNotThisMonth = this.customizeNotThisMonthRenderer();

    // override the default renderer as well
    this.yCal.renderCellDefault = this.customizeDefaultCellRenderer();

    // Here we are going to "hijack" the Calendar widget's
    // render method so as to ensure our event handlers are
    // always properly atached each time the calendar gets
    // rendered (e.g. on page navigation).
    this.yCal.render = function (that, old_render) {
      return function () {
        // blank out mapping of cell ids to dates
        that._clearCellDates();

        // call the original function in the proper scope and pass along any 
        // arguments
        old_render.apply(this, arguments);

        // add listeners *after* rendering so they don't get clobbered
        that._attachListeners();
        that._attachAdminListeners();
      };
    }(this, this.yCal.render);

    this._setupEvents();
    this._attachNavListeners();
  },

  _locateDomIDs: function () {
    this.previousMonthBtn = Dom.get(this.previousMonthBtn);
    if (!this.previousMonthBtn) {
      throw new Error('Could not locate previous month navigation button.');
    }

    this.nextMonthBtn = Dom.get(this.nextMonthBtn);
    if (!this.nextMonthBtn) {
      throw new Error('Could not locate next month navigation button.');
    }
  },

  _setupEvents: function () {
    this.eventsReady = 
        new YAHOO.util.CustomEvent('eventsReady', this);
    this.requestingEventData = 
        new YAHOO.util.CustomEvent('requestingEventData', this);

    this.calendarsReady = 
        new YAHOO.util.CustomEvent('calendarsReady', this);
    this.requestingCalendars = 
        new YAHOO.util.CustomEvent('requestingCalendars', this);

    this.upcomingReady =
        new YAHOO.util.CustomEvent('upcomingReady', this);
    this.requestingUpcoming =
        new YAHOO.util.CustomEvent('requestingUpcoming', this);
  },

  gotoPreviousMonth: function () {
    this.yCal.previousMonth();
    this._setMonthLabel(this.yCal.buildMonthLabel());
    this._fetchEventData();
  }, 

  gotoNextMonth: function () {
    this.yCal.nextMonth();
    this._setMonthLabel(this.yCal.buildMonthLabel());
    this._fetchEventData();
  },

  _attachNavListeners: function () {
    // add listener for navigation to the previous month
    DomEvent.addListener(this.previousMonthBtn, 'click', function () {
      this.gotoPreviousMonth();
    }, null, this);

    // add listener for navigation to the next month
    DomEvent.addListener(this.nextMonthBtn, 'click', function () {
      this.gotoNextMonth();
    }, null, this);
  },

  _attachListeners: function () {
    // Attach handler to all the more elements
    DomEvent.on(this._getMoreButton(), 'click', this._handleMoreButtonClick, this);
  },

  _setMonthLabel: function (str) {
    var oMonthLabel = Dom.get('ec-page-title');
    oMonthLabel.innerHTML = str;
  },

  getPageDate: function () {
    if (this.yCal) {
      return this.yCal.cfg.getProperty(
        YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key);
    }
  },

  _fetchUpcomingEvents: function () {
    var uri = '/admin/events/upcoming';
    var data;
    var callback = {
      success: function (o) {
        this.requests.upcoming = null;
        this.data.upcoming = json_decode(o.responseText);
        this.upcomingReady.fire();
      },
      failure: function (o) {
        console.log(['failure', o]);
      },
      timeout: 5000,
      scope: this
    };

    this.requests.upcoming = 
        Y.util.Connect.asyncRequest('post', uri, callback, data);
    this.requestingUpcoming.fire();
  },

  _fetchCalendarList: function () {
    var uri = '/admin/calendars/list';
    var data;
    var callback = {
      success: function (o) {
        this.requests.calendars = null;
        this.data.calendars = json_decode(o.responseText);
        this.calendarsReady.fire();
      },
      timeout: 5000,
      scope: this
    };

    this.requests.calendars = 
        Y.util.Connect.asyncRequest('post', uri, callback, data);
    this.requestingCalendars.fire();
  },

  _fetchEventData: function () {
    var uri;

    if (this.security.mode == 'admin') {
      uri = '/admin/events/list/';
    } else {
      uri = '/events/list/';
    }

    uri += 'm/' + String(this.getPageDate().getMonth() + 1) + '/';
    uri += 'y/' + this.getPageDate().getFullYear();

    this.requestingEventData.fire();
    this.requests.events = request({
      method: 'get',
      uri: uri,//'/admin/events/list',
      callback: {
        success: function (o) {
          this.requests.events = null;
          var json = json_decode(o.responseText);
          if (json.list && json.list instanceof Array) {
            this.data.events = json.list;
          } else {
            throw new Error('No data was returned');
          }
          this.eventsReady.fire();
        },
        timeout: 5000,
        scope: this
      },
      data: {
        m: this.getPageDate().getMonth() + 1,
        y: this.getPageDate().getFullYear()
      }
    });
  },

  load: function () {
    this.loadEvents();

    if (this.quicknav) {
      this.loadQuickNav();
    }

    if (this.calendars) {
      this.loadCalendarList();
    }

    if (this.upcoming) {
      this.loadUpcomingEventsList();
    }
  },

  render: function () {
    this.yCal.render();

    if (this.calendarsContainer) {
      this.renderCalendarList();
    }

    if (this.quicknavContainer) {
      this.renderQuickNav();
    }

    if (this.upcomingContainer) {
      this.renderUpcomingEventsList();
    }

    if (this.security.mode == 'admin') {
      this.renderAdminModule();
    }

    this.renderViewPanel();
    this.renderMorePanel();
  },

  loadEvents: function () {
    this.eventsReady.subscribe(function () { 
      this.drawEvents(); 
    }, null, this);
    this._fetchEventData();
  },

  /*_renderEventOwner: function() {
    var eventowner = document.createElement('div');
    eventowner.id = this.eventownerid;
    Dom.insertBefore(eventowner, this.id);
    this.eventowner = eventowner;
  },*/

  renderEvent: function (e) {
    var time, mer, h;
    var span;
    var start = tsToDate(e.start);

    span = document.createElement('span');
    Dom.addClass(span, 'ec-event');

    if (e.all_day == 1) {
      span.innerHTML = e.summary;
    } else {
      h = start.getHours();
      if (h > 12) {
        h = h % 12;
      } else {
        h = h == 0 ? 12 : h;
      }
      mer = start.getHours() >= 12 ? 'p' : 'a';
      if (start.getMinutes()) {
        time = h + ':' + start.getMinutes() + mer;
      } else {
        time = h + mer;
      }
      span.innerHTML = time + ' ' + e.summary;
    }

    DomEvent.on(span, 'mouseover', function () {
      Dom.addClass(this, 'hover');
    });

    DomEvent.on(span, 'mouseout', function () {
      Dom.removeClass(this, 'hover');
    });

    DomEvent.on(span, 'click', function (ev) {
      // need to preserve closure for the summary text
      return function (event) {

        if (this.security.mode == 'admin') {
          this.editEvent(ev);
        } else {
          this.viewEvent(ev);
        }

        // prevent the add event form from displaying
        DomEvent.stopEvent(event);
      };
    }(e), null, this);

    return span;
  },

  _makeCellId: function (dt) {
    return 'ec-' + dtToDate(dt);
  },

  /**
   * Construct cell element to reside within the Calendar cell.
   */
  _renderCell: function (dt) {
    var w = document.createElement('div');
    var dl = document.createElement('a');
    var oMoreAnchor = document.createElement('a');

    Dom.addClass(oMoreAnchor, 'more');
    Dom.setStyle(oMoreAnchor, 'display', 'none');
    oMoreAnchor.href = 'javascript:void(0);';
    oMoreAnchor.innerHTML = '+0';

    w.id = this._makeCellId(dt);
    Dom.addClass(w, 'ec-cal-cell');
    Dom.addClass(dl, 'daylabel');

    dl.innerHTML = dt.getDate();
    w.appendChild(dl);
    w.appendChild(oMoreAnchor);
    this._setCellDate(w, dt);

    return w;
  },

  getCellByDate: function (dt) {
    var idx = this.yCal.getCellIndex(dt);
    if (idx != -1) {
      return Dom.get(this._makeCellId(dt));
    } else {
      return false;
    }
  },

  getDateByCell: function (c) {
    return this._dt_by_c[c.id];
  },

  _setCellDate: function (c, dt) {
    // Make sure to copy the date. It would appear that the YUI Calendar
    // widget is using the same date object over and over and so the 
    // values will get clobbered if they are not explicitly copied.
    this._dt_by_c[c.id] = copyDate(dt);
  },

  _clearCellDates: function () {
    this._dt_by_c = {};
  },

  getCellEvents: function (c) {
    return this.data._ev_by_c[c.id];
  },

  getEventsForDate: function (dt) {
    var cell = this.getCellByDate(dt);
    return this.getCellEvents(cell);
  },

  getCellPosition: function (c) {
    return Dom.getXY(c);
  },

  putEvent: function (e, c) {
    var eid = Dom.generateId(e);
    var ep = this.eventowner;
    var pos = this.getCellPosition(c);

    c.appendChild(e);
    
    return this;
  },

  isCurrentDate: function (dt) {
    return this.getCellByDate(dt) ? true : false;
  },

  _fetchEventSpan: function (e) {
    var start = e.start;
    var end = e.end;

    if (dtToDate(start) == dtToDate(end)) {
      // span is just within one day
      return [dtToDate(start)];
    } else {
      // otherwise construct a sequence of dates from beginning to finish
      return dateSequence(start, end);
    }
  },

  clearEvents: function () {
    var events;
    var i, len;
    events = Dom.getElementsByClassName('ec-event', 'span', this.yCal.id);

    // remove events synchronously so as to avoid race condition with script 
    // that adds the new events.
    for (i=0, len=events.length; i<len; i+=1) {
      DomEvent.removeListener(events[i], 'click');
      DomEvent.removeListener(events[i], 'mouseover');
      DomEvent.removeListener(events[i], 'mouseout');
      events[i].parentNode.removeChild(events[i]);
    }
  },

  _handleMoreButtonClick: function (e, self) {
    self.showMore(self.getDateByCell(this.parentNode));
    // prevent any further actions
    DomEvent.stopEvent(e);
  },

  /**
   * Fetch all of the "more" buttons for the current calendar or the more 
   * button of one of the cells.
   */
  _getMoreButton: function (c) {
    var MOREBTN_CLASS = 'more';
    if (c) {
      return Dom.getElementsByClassName(MOREBTN_CLASS, 'a', c)[0];
    } else {
      return Dom.getElementsByClassName(MOREBTN_CLASS, 'a', this.container);
    }
  },

  drawEvents: function (events) {
    var self = this;
    var i, len;
    var cell;
    var re, e;
    var start;
    var ebc = {}; // events by cell

    var h = 13; // height of an event item
    var h_bar = 16; // height of a day header
    var h_more = 16; // height of the more link container

    var H_max = function (c) {
      return c.clientHeight;
    }

    var N = function (c) {
      return ebc[c.id].list.length;
    };

    var H = function (c) {
      return (H_max(c) - h_bar) - N(c) * h;
    };

    var _update_more = function (c) {
      var oMoreAnchor;
      var n = ebc[c.id].overflow.length;
      oMoreAnchor = self._getMoreButton(c);

      if (n > 0) {
        Dom.setStyle(oMoreAnchor, 'display', 'block');
        oMoreAnchor.innerHTML = '+' + n;
      } else {
        Dom.setStyle(oMoreAnchor, 'display', 'none');
      }
    };

    /**
     * For the given cell test whether or not we should allow the addition of 
     * a single event to the cell.
     */
    var _test = function (c) {
      // the total remaining height for the cell H(c) must be sufficiently 
      // large to hold one more event in addition to the height of the 
      // more link.
      return (H(c) - h > h_more);
    };

    events = events || this.data.events;

    if (Lang.isArray(events)) {

      this.clearEvents();

      for (i=0, len=events.length; i<len; i+=1) {
        e = events[i];
        start = tsToDate(e.start);
        cell = this.getCellByDate(start);
        if (cell) {
          // Make sure things are setup
          if (ebc[cell.id] === undefined) {
            ebc[cell.id] = {};
          }
          if (ebc[cell.id].list === undefined) {
            ebc[cell.id].list = [];
          }
          if (ebc[cell.id].overflow === undefined) {
            ebc[cell.id].overflow = [];
          }

          // Decide whether or not we should place the event in the cell.
          if (_test(cell)) {
            ebc[cell.id].list.push(e);
            re = this.renderEvent(e);
            this.putEvent(re, cell);
          } else {
            // There are already too many events in the cell to be displayed
            // so put it in the overflow.
            ebc[cell.id].overflow.push(e);
          }

          // Update the "more" link for this cell.
          _update_more(cell);
        }
      }
    }
    this.data._ev_by_c = ebc;
  },

  renderCalendarList: function () {
    var oList, oForm;
    this.calendarsContainer = Dom.get(this.calendarsContainer);
    if (this.calendarsContainer) {
      oList = document.createElement('ul');
      oForm = document.createElement('form');

      this.calendars = oList;
      this.calendarsContainer.innerHTML = '';
      this.calendarsContainer.appendChild(oForm);
      oForm.appendChild(oList);

    } else {
      throw new Error('Calendars container element not found.');
    }
  },

  loadCalendarList: function () {

    // subscribe to the event that the calendar data is ready
    this.calendarsReady.subscribe(function () {
      var calendars = this.data.calendars;

      if (calendars instanceof Array) {
        var len = calendars.length;
        var i;

        for (i=0; i<len; i+=1) {
          new Calendar(calendars[i]);
        }
      }
      else {
        throw new Error('calendars must be of type Array');
      }

      this.drawCalendarList();
    });

    this._fetchCalendarList();
  },

  drawCalendarList: function () {
    var i;

    if (this.calendars) {
      if (this.data.calendars) {
        this.calendars.innerHTML = '';

        var cal, li, input, inputId;
        var html = '';

        for (i=0; i<this.data.calendars.length; i+=1) {
          cal = this.data.calendars[i];
          inputId = 'cal_' + cal.id;         

          li = document.createElement('li');

          html = '<label for="' + inputId + '">';
          html += '<input type="checkbox" id="' + inputId + '" ' +
                        'checked="checked" ' + 
                        'value="' + cal.id + '"/>';
          html += '&nbsp;' + cal.description;
          html += '</label>';

          li.innerHTML = html;

          this.calendars.appendChild(li);

          DomEvent.on(inputId, 'click', function (that) {
            console.info(['calendar checkbox', arguments, this]);
          }, this);
        }

      } else {
        throw new Error('No calendar data.');
      }
    } else {
      throw new Error('No calendar list to draw to.');
    }
  },

  renderUpcomingEventsList: function () {
    var oList, oForm;
    this.upcomingContainer = Dom.get(this.upcomingContainer);
    if (this.upcomingContainer) {
      oList = document.createElement('ul');
      oForm = document.createElement('form');

      this.upcoming = oList;
      this.upcomingContainer.innerHTML = '';
      this.upcomingContainer.appendChild(oForm);
      oForm.appendChild(oList);

    } else {
      throw new Error('Upcoming container element not found.');
    }

    // attach event listeners
    this.upcomingReady.subscribe(this.drawUpcomingEventsList, null, this);
  },

  loadUpcomingEventsList: function () {
    this._fetchUpcomingEvents();
  },

  drawUpcomingEventsList: function () {
    var i, len, e, li, html, start, end, span;

    if (this.data.upcoming) {
      this.data.upcoming.innerHTML = '';

      for (i=0, len=this.data.upcoming.length; i<len; i+=1) {
        html = '';
        e = this.data.upcoming[i];
        li = document.createElement('li');

        start = tsToDate(e.start);

        span = document.createElement('span');
        span.className = 'event-start';
        span.innerHTML = dtToString(start);
        li.appendChild(span);

        span = document.createElement('span');
        span.className = 'event-description';
        span.innerHTML = e.summary;
        li.appendChild(span);

        this.upcoming.appendChild(li);
      }
    }
  },

  renderQuickNav: function () {
    var wrap;
    var c;

    // we're going to first construct a wrapper div which will be appended to
    // this.quicknavContainer and the calendar widget will be appended to the
    // wrapper div.
    wrap = document.createElement('div');
    Dom.addClass(wrap, 'quick-nav-wrap');

    c = Dom.get(this.quicknavContainer);
    if (!c) {
      throw new Error('Unknown element "' + this.quicknavContainer + '"');
    }
    
    c.innerHTML = '';
    c.appendChild(wrap);

    this.quicknav = new Y.widget.Calendar(Dom.generateId(), wrap, {
      hide_blank_weeks: true
    });
    this.quicknav.render = function (that, old_render) {
      return function () {
        old_render.apply(this, arguments);
        that._attachQuickNavEvents(wrap);
      };
    }(this, this.quicknav.render);

    this.quicknav.render();
  },

  _setupQuickNavEvents: function () {
    // todo: setup custom quicknav events here
  },

  _attachQuickNavEvents: function (wrap) {
    // the wrapper will listen for clicks on the calendar days and handle them
    // accordingly
    DomEvent.addListener(wrap, 'click', function () {
      console.info(['Selected dates = ', this.quicknav.getSelectedDates()]);
    }, null, this);
  },

  loadQuickNav: function () {
    // modify quicknav calendar to reflect the data    
  },

  renderAdminModule: function () {
    var dlg;

    if (this.security.mode != 'admin') {
      throw new Error('Cannot render administration module.');
    }

    this.addEventDlg = dlg = new YAHOO.widget.Dialog('ec-admin-addeventdlg', {
      visible: false,
      constraintoviewport: true,
      underlay: 'shadow',
      zindex: 1000,
      modal: true,
      buttons: [
        {text: "Submit", isDefault: true, handler: function () {
          this.submit();
        }},
        {text: "Cancel", handler: function () {
          this.cancel(); 
        }}
      ]
    });

    dlg.callback.success = function (that) {
      return function (o) {
        //debugger;
        var response;
        if (o && o.responseText) {
          response = json_decode(o.responseText);
          that._fetchEventData();
        }
      };
    }(this);

    dlg.render();

    // Draw a delete button
    dlg.deleteBtn = document.createElement('a');
    dlg.deleteBtn.innerHTML = 'Delete this event';
    Dom.get(dlg.id).appendChild(dlg.deleteBtn);
    Dom.addClass(dlg.deleteBtn, 'deletebtn lower-left');
    

    ////////////////////////////////////////////////////////////////////////////
    // Apply event listeners
    ////////////////////////////////////////////////////////////////////////////

    DomEvent.on([dlg.form.start_d, dlg.form.start_t, dlg.form.end_t, dlg.form.end_d], 'change', function (e) {
      var start = dlg.getStart();
      var end = dlg.getEnd();
      var delta = Number(dlg._db.end) - Number(dlg._db.start);

      // if we're changing the start time we must modify the end time to keep
      // the delta value
      if (this == dlg.form.start_t || this == dlg.form.start_d) {
        // take the new start value, add to it the value of delta; hence the
        // new value for end.
        end = new Date(Number(start) + delta);
      }

      // update the values for good measure (will turn things like user input
      // like 2008-1-1 into nice things like 2008-01-01)
      dlg.setEnd(end);
      dlg.setStart(start);

      // last but not least update the data store
      dlg._db.start = start;
      dlg._db.end = end;
    });

    DomEvent.on(dlg.form.all_day[1], 'change', function (e) {
      var start = dlg.form.start_t;
      var end = dlg.form.end_t;
      if (this.checked) {
        Dom.setStyle(start, 'display', 'none');
        Dom.setStyle(end, 'display', 'none');
      } else {
        Dom.setStyle(start, 'display', 'inline');
        Dom.setStyle(end, 'display', 'inline');
      }
    });

    ////////////////////////////////////////////////////////////////////////////
    // Customize the dialog with some functions attributes
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Data store.
     *
     * Note: Holds some of the form data we need to keep around separate from 
     * user changes so we can preserve thigns like the delta between start 
     * and end when the user changes the start time.
     */
    dlg._db = {
      start: null,
      end: null
    };

    dlg.validate = function () {
      var retval = true;
      var data = this.getData();
      var msg = [];

      if (!data.summary) {
        retval = false;
        msg.push("I'm sorry but it'd be nice if you'd " + 
            "provide at least a subject of the event.");
      }

      if (!data.start_d) {
        retval = false;
        msg.push("I've got to at least know the date of the event.");
      }

      if (msg.length) {
        msg.push("Thanks!");
        alert(msg.join('\n\n'));
      }

      return retval;
    };

    dlg.clearFields = function () {
      var f = this.form;
      f.id.value = '-1';
      f.summary.value = '';
      f.description.value = '';
      f.where.value = '';
      f.start_d.value = '';
      f.start_t.value = '';
      this._db.start = null;
      f.end_d.value = '';
      f.end_t.value = '';
      this._db.end = null;
      f.all_day[1].checked = false;
      return this;
    }

    dlg.setId = function (id) {
      this.form.id.value = id;
      return this;
    };

    dlg.setStart = function (dt) {
      this.form.start_d.value = dtToDate(dt);
      this.form.start_t.value = dtToTime(dt);
      this._db.start = this.getStart();
      return this;
    };

    dlg.getStart = function () {
      var retval;
      var d = this.form.start_d.value;
      var t = this.form.start_t.value;
      var md = matchDate(d);
      var mt = matchTime(t);

      if (md && mt) {
        mt[3] = mt[3].replace(/\.*/g, ''); // get rid of periods
        md[0] = md[0].replace(/-/g, '/'); // js date doesn't like dashes
        // fixme: validate the date
        retval = md[0] + ' ' + mt[1] + ':' + mt[2] + ' ' + mt[3];
        return new Date(retval);
      } else {
        return false;
      }
    };

    dlg.setEnd = function (dt) {
      this.form.end_d.value = dtToDate(dt);
      this.form.end_t.value = dtToTime(dt);
      this._db.end = this.getEnd();
      return this;
    };

    dlg.getEnd = function () {
      var retval;
      var d = this.form.end_d.value;
      var t = this.form.end_t.value;
      var md = matchDate(d);
      var mt = matchTime(t);

      if (md && mt) {
        mt[3] = mt[3].replace(/\.*/g, ''); // get rid of periods
        md[0] = md[0].replace(/-/g, '/'); // js date doesn't like dashes
        // fixme: validate the date
        retval = md[0] + ' ' + mt[1] + ':' + mt[2] + ' ' + mt[3];
        return new Date(retval);
      } else {
        return false;
      }
    };

    dlg.setSummary = function (s) {
      this.form.summary.value = s;
      return this;
    };

    dlg.setDescription = function (d) {
      this.form.description.value = d;
      return this;
    };

    dlg.setWhere = function (w) {
      this.form.where.value = w;
      return this;
    }

    dlg.setCalendar = function (c) {
      this.form.cal_id = c;
      return this;
    };

    dlg.setAllDay = function (b) {
      var start = dlg.form.start_t;
      var end = dlg.form.end_t;

      this.form.all_day[1].checked = (b == true ? true : false);

      if (this.form.all_day[1].checked) {
        Dom.setStyle(start, 'display', 'none');
        Dom.setStyle(end, 'display', 'none');
      } else {
        Dom.setStyle(start, 'display', 'inline');
        Dom.setStyle(end, 'display', 'inline');
      }
    };

    /*var s = new Buttress.widget.Select({
      input: dlg.form.start_t,
      options: [
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"}
      ]
    });
    s.render();

    var e = new Buttress.widget.Select({
      input: dlg.form.end_t,
      options: [
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"},
        {text: "Item 1"}, {text: "Item 2"}
      ]
    });
    e.render();*/
  },

  _attachAdminListeners: function () {
    if (this.security.mode == 'admin') {
      this.yCal.selectEvent.subscribe(function () {
        this.addEvent(this.yCal.getSelectedDates()[0]);
      }, null, this);
    }
  },

  addEvent: function (dt) {
    var tenAm = new Date(0, 0, 0, 10);
    dt = dt || tenAm;

    this.addEventDlg.form.action = '/admin/events/add';
    this.addEventDlg.clearFields();
    Dom.setStyle(this.addEventDlg.deleteBtn, 'display', 'none');

    dt.setHours(tenAm.getHours());
    dt.setMinutes(tenAm.getMinutes());

    // fixme: need to be a little less naieve about this ...
    this.addEventDlg.setStart(dt);
    this.addEventDlg.setEnd(new Date(new Number(dt) + 1000 * 60 * 60));
    this.addEventDlg.setAllDay(false);

    this.addEventDlg.show();
    this.addEventDlg.center();
  },

  editEvent: function (e) {
    var dlg = this.addEventDlg;
    var fmt = '%Y-%m-%d %H:%M:%S';

    dlg.form.action = '/admin/events/edit';
    dlg.clearFields();

    // show the delete button and attach the apropriate event handler.
    Dom.setStyle(dlg.deleteBtn, 'display', 'inline');
    DomEvent.purgeElement(dlg.deleteBtn); // remove previous delete listeners
    DomEvent.on(dlg.deleteBtn, 'click', function () {
      if (this.deleteEvent(e)) {
        dlg.cancel();
      }
    }, null, this);

    dlg.
      setId(e['id']).
      setSummary(e['summary']).
      setDescription(e['description']).
      setWhere(e['location']).
      setStart(Date.parseDate(e['start'], fmt)).
      setEnd(Date.parseDate(e['end'], fmt)).
      setAllDay(e['all_day']);

    dlg.show();
    dlg.center();
  },

  renderViewPanel: function () {
    var panel;

    panel = new YAHOO.widget.Panel('ec-viewpanel', {
      visible: false,
      underlay: 'matte',
      zindex: 1000,
      modal: true
    });
    this.viewPanel = panel;

    panel.setHeader('Viewing Event');
    panel.setBody('<div id="ec-viewpanel-event"></div>');
    panel.render(document.body);
  },

  viewEvent: function (ev) {
    //console.log(['Viewing Event', ev]);
    var html = '';
    var oEventTarget = Dom.get('ec-viewpanel-event');

    html += '<table style="width:100%;background: white;">';

    if (ev.all_day == 1) {
      html += '<tr>';
      html += '<th>When:</th>';
      html += '<td><em>All day</em></td>';
      html += '</tr>';
    } else {
      html += '<tr>';
      html += '<th>When:</th>';
      html += '<td>' + ev.start + '&mdash;' + ev.end + '</td>';
      html += '</tr>';
    }

    if (ev.location) {
      html += '<tr>';
      html += '<th>Where:</th>';
      html += '<td>' + ev.location + '</td>';
      html += '</tr>';
    }

    if (ev.description) {
      html += '<tr>';
      html += '<th>Description:</th>';
      html += '<td><div style="height:100px;text-align:left;overflow:auto">' + 
        ev.description + 
        '</div></td>';
      html += '</tr>';
    }

    html += '</table>';

    html += '<p><a href="/events/' + ev.id + '.html">Direct link</a></p>';

    oEventTarget.innerHTML = html;

    this.viewPanel.setHeader(ev.summary);
    this.viewPanel.show();
    this.viewPanel.center();
  },

  renderMorePanel: function () {
    var panel;

    // more cowbell...er...more panel
    panel = new YAHOO.widget.Panel('ec-morepanel', {
      visible: false,
      underlay: 'matte',
      zindex: 1000,
      modal: true,
      width: '400px'
    });
    this.morePanel = panel;

    panel.setHeader('More events ...');
    panel.setBody('<div id="ec-morepanel-list"></div>');
    panel.render(document.body);  
  },

  /**
   * Show a panel with a listing of all the events for the given day.
   *
   * @param Date
   */
  showMore: function (dt) {
    var self = this;
    var oListTarget = Dom.get('ec-morepanel-list');
    var oEvents = this.getEventsForDate(dt);

    //console.log(['Show More', oListTarget, oEvents]);

    this.morePanel.setHeader(dt.toDateString());

    function put_list(a) {
      var oE;
      var a, i, len;
      for (i=0, len=a.length; i<len; i+=1) {
        oE = self.renderEvent(a[i]);
        oListTarget.appendChild(oE);
      }
    }

    // FIXME: purge element children as well to release resources in IE6
    oListTarget.innerHTML = '';

    put_list(oEvents.list);
    put_list(oEvents.overflow);

    this.morePanel.show();
    this.morePanel.center();
  },

  deleteEvent: function (e) {
    var msg = 'This will permanently delete event "' + e.summary + '"\n\n' + 
      'Do you want to proceed?';
    if (!confirm(msg)) {
      return false;
    }

    request({
      uri: '/admin/events/destroy',
      data: {
        id: e.id
      },
      callback: {
        success: function (o) {
          this._fetchEventData();
        },
        timeout: 5000,
        scope: this
      },
      method: 'post'
    });

    return true;
  },

  customizeNotThisMonthRenderer: function () {
    var that = this;
    return function (dt, c) {
      var w = document.createElement('div');
      var dl = document.createElement('a');

      Dom.addClass(c, this.Style.CSS_CELL_OOM);
      Dom.addClass(w, 'ec-cal-cell');
      Dom.addClass(w, 'oom');
      Dom.addClass(dl, 'daylabel');
      
      w.id = that._makeCellId(dt);

      dl.innerHTML = dt.getDate();
      c.innerHTML = '';
      w.appendChild(dl);
      c.appendChild(w);

      return Y.widget.Calendar.STOP_RENDER;
    };
  },

  customizeDefaultCellRenderer: function () {
    var that = this;
    return function (dt, c) {

      /*var w = document.createElement('div');
      var dl = document.createElement('a');
      var oMoreAnchor = document.createElement('a');

      Dom.addClass(oMoreAnchor, 'more');
      Dom.setStyle(oMoreAnchor, 'display', 'none');
      oMoreAnchor.href = 'javascript:void(0);';
      oMoreAnchor.innerHTML = '+0';

      Dom.addClass(w, 'ec-cal-cell');
      Dom.addClass(dl, 'daylabel');

      w.id = that._makeCellId(dt);

      dl.innerHTML = dt.getDate();
      c.innerHTML = '';
      w.appendChild(dl);
      w.appendChild(oMoreAnchor);
      c.appendChild(w);

      that._dt_by_c[w.id] = dt; // mapping of cell ids to dates*/

      c.innerHTML = '';
      c.appendChild(that._renderCell(dt));

      return Y.widget.Calendar.STOP_RENDER;
    };
  },

  getCalendars: function () {
    return Calendar.calendars;
  }
};

Buttress.register('widget.EventCalendar', EventCalendar);

})();
