<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

<!DOCTYPE dialog [
  <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
  <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
  <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %dtd3;
]>

<bindings xmlns="http://www.mozilla.org/xbl"
          xmlns:xbl="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <!--
  ########################################################################
  ## scroll-container
  ########################################################################
  -->
  <binding id="scroll-container" extends="xul:box">
    <content>
      <xul:box class="container"
               xbl:inherits="flex"
               anonid="container"
               style="overflow: hidden; clip: rect(0px 0px 0px 0px);">
        <xul:box class="content"
                 xbl:inherits="flex,orient"
                 anonid="content">
          <children/>
        </xul:box>
      </xul:box>
    </content>

    <implementation>
      <property name="x">
        <setter><![CDATA[
          var content =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "content");
          content.setAttribute("style",
                               "margin-inline-start: " + (-val) + "px;");
          return val;
        ]]></setter>
      </property>

      <property name="y">
        <setter><![CDATA[
          var content =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "content");
          content.setAttribute("style",
                               "margin-top: " + (-val) + "px;");
          return val;
        ]]></setter>
      </property>
    </implementation>
  </binding>

  <!--
  ########################################################################
  ## freebusy-day
  ########################################################################
  -->
  <binding id="freebusy-day" extends="xul:box">
    <content>
      <xul:box orient="vertical">
        <xul:text class="freebusy-timebar-title"
                  style="font-weight:bold;"
                  anonid="day"/>
        <xul:box equalsize="always" anonid="hours"/>
      </xul:box>
    </content>

    <implementation>
      <field name="mDateFormatter">null</field>
      <field name="mStartDate">null</field>
      <field name="mEndDate">null</field>
      <field name="mStartHour">0</field>
      <field name="mEndHour">24</field>
      <field name="mForce24Hours">false</field>
      <field name="mZoomFactor">100</field>

      <property name="zoomFactor">
        <getter><![CDATA[
          return this.mZoomFactor;
        ]]></getter>
        <setter><![CDATA[
          this.mZoomFactor = val;
          let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
          removeChildren(hours);
          return val;
        ]]></setter>
      </property>

      <property name="force24Hours">
        <getter><![CDATA[
            return this.mForce24Hours;
        ]]></getter>
        <setter><![CDATA[
          this.mForce24Hours = val;
          this.initTimeRange();

          let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
          removeChildren(hours);
          return val;
        ]]></setter>
      </property>

      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/Preferences.jsm");

        this.initTimeRange();
      ]]></constructor>

      <method name="initTimeRange">
        <body><![CDATA[
          if (this.force24Hours) {
              this.mStartHour = 0;
              this.mEndHour = 24;
          } else {
              this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
              this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
          }
        ]]></body>
      </method>

      <property name="startDate">
        <setter><![CDATA[
          this.mStartDate = val.clone();
          this.mStartDate.minute = 0;
          this.mStartDate.second = 0;
          this.mStartDate.makeImmutable();
          return val;
        ]]></setter>
      </property>

      <property name="endDate">
        <setter><![CDATA[
          this.mEndDate = val.clone();
          this.mEndDate.makeImmutable();
          return val;
        ]]></setter>
      </property>

      <property name="dayHeight">
        <getter><![CDATA[
          var day =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "day");
          return day.boxObject.height;
        ]]></getter>
      </property>

      <property name="date">
        <setter><![CDATA[
          var date = val.clone();
          date.hour = 0;
          date.minute = 0;
          date.isDate = false;

          if (!this.mDateFormatter) {
              this.mDateFormatter =
                  Components.classes[
                      "@mozilla.org/calendar/datetime-formatter;1"]
                          .getService(
                              Components.interfaces.calIDateTimeFormatter);
          }

          // First set the formatted date string as title
          var day =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "day");
          var value = (this.mZoomFactor > 100) ?
              this.mDateFormatter.formatDateShort(date) :
              this.mDateFormatter.formatDateLong(date);
          day.setAttribute("value", value);

          // Now create as many 'hour' elements as needed
          var step_in_minutes = Math.floor(60 * this.mZoomFactor / 100);
          var hours =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "hours");
          date.hour = this.mStartHour;
          if (hours.childNodes.length <= 0) {
              var template = createXULElement("text");
              template.className = "freebusy-timebar-hour";
              var count = Math.ceil(
                  (this.mEndHour - this.mStartHour) * 60 / step_in_minutes);
              var remain = count;
              let first = true;
              while (remain--) {
                  var newNode = template.cloneNode(false);
                  var value = this.mDateFormatter.formatTime(date);
                  if (first) {
                      newNode.className += " first-in-day";
                      first = false;
                  }
                  newNode.setAttribute("value", value);
                  hours.appendChild(newNode);
                  date.minute += step_in_minutes;

                  if (remain == 0) {
                      newNode.className += " last-in-day"
                  }
              }
          }

          return val;
        ]]></setter>
      </property>
    </implementation>
  </binding>

  <!--
  ########################################################################
  ## freebusy-timebar
  ########################################################################
  -->
  <binding id="freebusy-timebar" extends="xul:box">
    <content>
      <xul:listbox anonid="listbox"
                   class="listbox-noborder"
                   seltype="multiple"
                   rows="1"
                   flex="1"
                   disabled="true">
        <xul:listcols>
          <xul:listcol anonid="day-column" flex="1"/>
        </xul:listcols>
        <xul:listitem anonid="item"
                      class="freebusy-listitem"
                      allowevents="true">
          <xul:listcell >
            <xul:scroll-container anonid="container">
              <xul:freebusy-day anonid="template"/>
            </xul:scroll-container>
          </xul:listcell>
        </xul:listitem>
      </xul:listbox>
    </content>

    <implementation>
      <field name="mNumDays">0</field>
      <field name="mRange">0</field>
      <field name="mStartDate">null</field>
      <field name="mEndDate">null</field>
      <field name="mDayOffset">0</field>
      <field name="mScrollOffset">0</field>
      <field name="mStartHour">0</field>
      <field name="mEndHour">24</field>
      <field name="mForce24Hours">false</field>
      <field name="mZoomFactor">100</field>

      <property name="zoomFactor">
        <getter><![CDATA[
          return this.mZoomFactor;
        ]]></getter>
        <setter><![CDATA[
          this.mZoomFactor = val;

          var template =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "template");
          var parent = template.parentNode;
          while (parent.childNodes.length > 1) {
              parent.lastChild.remove();
          }

          template.force24Hours = this.mForce24Hours;
          template.zoomFactor = this.mZoomFactor;

          this.onLoad();

          return val;
        ]]></setter>
      </property>

      <property name="force24Hours">
        <getter><![CDATA[
          return this.mForce24Hours;
        ]]></getter>
        <setter><![CDATA[
          this.mForce24Hours = val;
          this.initTimeRange();

          var template =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "template");

          var parent = template.parentNode;
          while (parent.childNodes.length > 1) {
            parent.lastChild.remove();
          }

          template.force24Hours = this.mForce24Hours;
          template.zoomFactor = this.mZoomFactor;

          this.onLoad();

          return val;
        ]]></setter>
      </property>

      <property name="contentWidth">
        <getter><![CDATA[
          // Calculate the difference between the first to day-elements, since
          // the width of the head element does not specify the width we need
          // due to an arbitrary margin value.
          var template =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "template");
          return template.nextSibling.boxObject.x - template.boxObject.x;
        ]]></getter>
      </property>

      <property name="containerWidth">
        <getter><![CDATA[
          return this.parentNode.boxObject.width;
        ]]></getter>
      </property>

      <property name="startDate">
        <setter><![CDATA[
          this.mStartDate = val.clone();
          this.mStartDate.makeImmutable();
          return val;
        ]]></setter>
        <getter><![CDATA[
          return this.mStartDate;
        ]]></getter>
      </property>

      <property name="endDate">
        <setter><![CDATA[
          this.mEndDate = val.clone();
          this.mEndDate.makeImmutable();
          return val;
        ]]></setter>
        <getter><![CDATA[
          return this.mEndDate;
        ]]></getter>
      </property>

      <property name="dayOffset">
        <setter><![CDATA[
          this.mDayOffset = val;
          var container =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "container");
          var date = this.mStartDate.clone();
          date.day += val;
          var numChilds = container.childNodes.length;
          for (var i = 0; i < numChilds; i++) {
              var child = container.childNodes[i];
              child.date = date;
              date.day++;
          }
          return val;
        ]]></setter>
      </property>

      <property name="step">
        <getter><![CDATA[
          // How much pixels spans a single day
          var oneday = this.contentWidth;

          // The difference in pixels between the content and the container.
          var shift = (oneday * this.mRange) - (this.containerWidth);

          // What we want to know is the scale of the total shift
          // needed to step one block further. Since the content
          // is divided into 'numHours' equal parts, we can simply state:
          var numHours = this.mEndHour - this.mStartHour;
          return (this.contentWidth) / (numHours * shift);
        ]]></getter>
      </property>

      <property name="scroll">
        <setter><![CDATA[
          this.mScrollOffset = val;

          // How much pixels spans a single day
          var oneday = this.contentWidth;

          // The difference in pixels between the content and the container.
          var shift = (oneday * this.mRange) - (this.containerWidth);

          // Now calculate the (positive) offset in pixels which the content
          // needs to be shifted. This is a simple scaling in one dimension.
          var offset = Math.floor(val * shift);

          // Now find out how much days this offset effectively skips.
          // this is a simple division which always yields a positive integer value.
          this.dayOffset = (offset - (offset % oneday)) / oneday;

          // Set the pixel offset for the content which will always need
          // to be in the range [0 <= offset <= oneday].
          offset %= oneday;

          // Set the offset at the content node.
          var container =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "container");
          container.x = offset;
          return val;
        ]]></setter>
        <getter><![CDATA[
          return this.mScrollOffset;
        ]]></getter>
      </property>

      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/Preferences.jsm");

        var args = window.arguments[0];
        var startTime = args.startTime;
        var endTime = args.endTime;

        this.initTimeRange();

        // The basedate is the date/time from which the display
        // of the timebar starts. The range is the number of days
        // we should be able to show. The start- and enddate
        // is the time the event is scheduled for.
        var kDefaultTimezone = calendarDefaultTimezone();
        this.startDate = startTime.getInTimezone(kDefaultTimezone);
        this.endDate = endTime.getInTimezone(kDefaultTimezone);
        this.mRange = Number(this.getAttribute("range"));

        var self = this;
        var load = function loadHandler() {
            self.onLoad();
        };
        window.addEventListener("load", load, true);
      ]]></constructor>

      <method name="refresh">
        <body><![CDATA[
          var date = this.mStartDate.clone();
          var template =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "template");
          var parent = template.parentNode;
          var numChilds = parent.childNodes.length;
          for (var i = 0; i < numChilds; i++) {
              var child = parent.childNodes[i];
              child.startDate = this.mStartDate;
              child.endDate = this.mEndDate;
              child.date = date;
              date.day++;
          }
          var offset = this.mDayOffset;
          this.dayOffset = offset;
        ]]></body>
      </method>

      <method name="onLoad">
        <body><![CDATA[
            this.initialize();
            var template =
                document.getAnonymousElementByAttribute(
                    this, "anonid", "template");
            var event = document.createEvent('Events');
            event.initEvent('timebar', true, false);
            event.details = this.contentWidth;
            event.height = template.dayHeight;
            this.dispatchEvent(event);
        ]]></body>
      </method>

      <method name="initialize">
        <body><![CDATA[
          var args = window.arguments[0];
          var startTime = args.startTime;
          var endTime = args.endTime;
          var calendar = args.calendar;

          var kDefaultTimezone = calendarDefaultTimezone();
          this.startDate = startTime.getInTimezone(kDefaultTimezone);
          this.endDate = endTime.getInTimezone(kDefaultTimezone);

          // Set the number of 'freebusy-day'-elements
          // we need to fill up the content box.
          // TODO: hardcoded value
          this.mNumDays = 4 * this.mZoomFactor / 100;
          if (this.mNumDays < 2) {
              this.mNumDays = 2;
          }

          // Now create those elements and set their date property.
          var date = this.mStartDate.clone();
          var template =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "template");
          template.force24Hours = this.mForce24Hours;
          template.zoomFactor = this.mZoomFactor;
          template.startDate = this.mStartDate;
          template.endDate = this.mEndDate;
          template.date = date;
          var parent = template.parentNode;
          if (parent.childNodes.length <= 1) {
              var count = this.mNumDays - 1;
              if (count > 0) {
                  for (var i = 0; i < count; i++) {
                      date.day++;
                      var newNode = template.cloneNode(false);
                      newNode.force24Hours = this.mForce24Hours;
                      newNode.zoomFactor = this.mZoomFactor;
                      newNode.startDate = this.mStartDate;
                      newNode.endDate = this.mEndDate;
                      newNode.date = date;
                      parent.appendChild(newNode);
                  }
              }
          }
        ]]></body>
      </method>

      <method name="initTimeRange">
        <body><![CDATA[
          if (this.force24Hours) {
              this.mStartHour = 0;
              this.mEndHour = 24;
          } else {
              this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
              this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
          }
        ]]></body>
      </method>
    </implementation>
  </binding>

  <!--
  ########################################################################
  ## freebusy-row
  ########################################################################
  -->
  <binding id="freebusy-row" extends="xul:box">
    <content>
      <xul:scroll-container flex="1" anonid="container">
        <xul:box equalsize="always" anonid="hours"/>
      </xul:scroll-container>
    </content>

    <implementation>
      <field name="mState">null</field>
      <field name="mEntries">null</field>
      <field name="mOffset">0</field>
      <field name="mStartDate">null</field>
      <field name="mEndDate">null</field>
      <field name="mRange">0</field>
      <field name="mStartHour">0</field>
      <field name="mEndHour">24</field>
      <field name="mForce24Hours">false</field>
      <field name="mZoomFactor">100</field>

      <property name="zoomFactor">
        <getter><![CDATA[
          return this.mZoomFactor;
        ]]></getter>
        <setter><![CDATA[
          this.mZoomFactor = val;

          let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
          removeChildren(hours);
          this.onLoad();

          return val;
        ]]></setter>
      </property>

      <property name="force24Hours">
        <getter><![CDATA[
          return this.mForce24Hours;
        ]]></getter>
        <setter><![CDATA[
          this.mForce24Hours = val;
          this.initTimeRange();

          let hours = document.getAnonymousElementByAttribute(this, "anonid", "hours");
          removeChildren(hours);
          this.onLoad();

          return val;
        ]]></setter>
      </property>

      <property name="startDate">
        <setter><![CDATA[
          this.mStartDate = val.clone();
          this.mStartDate.isDate = false;
          this.mStartDate.makeImmutable();
          return val;
        ]]></setter>
      </property>

      <property name="endDate">
        <setter><![CDATA[
          this.mEndDate = val.clone();
          this.mEndDate.isDate = false;
          this.mEndDate.makeImmutable();
          return val;
        ]]></setter>
      </property>

      <property name="numHours">
        <getter><![CDATA[
          var numHours = this.mEndHour - this.mStartHour;
          return Math.ceil(numHours * 100 / this.mZoomFactor);
        ]]></getter>
      </property>

      <property name="contentWidth">
        <getter><![CDATA[
          var hours =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "hours");
          return (hours.childNodes[1].boxObject.x -
                  hours.childNodes[0].boxObject.x) *
                  this.numHours;
        ]]></getter>
      </property>

      <property name="containerWidth">
        <getter><![CDATA[
          // Step up the hierarchy until we reach the listbox
          return this.parentNode.
                      parentNode.
                      parentNode.
                      parentNode.boxObject.width;
        ]]></getter>
      </property>

      <property name="dayOffset">
        <setter><![CDATA[
          this.mOffset = val * this.numHours;
          this.showState();
          return val;
        ]]></setter>
      </property>

      <property name="documentSize">
        <getter><![CDATA[
          return  this.contentWidth * this.mRange;
        ]]></getter>
      </property>

      <property name="scroll">
        <setter><![CDATA[
          // How much pixels spans a single day
          var oneday = this.contentWidth;
          if (oneday <= 0) {
              return val;
          }

          // The difference in pixels between the content and the container.
          var shift = (oneday * this.mRange) - (this.containerWidth);

          // Now calculate the (positive) offset in pixels which the content
          // needs to be shifted. This is a simple scaling in one dimension.
          var offset = Math.floor(val * shift);

          // Now find out how much days this offset effectively skips.
          // this is a simple division which always yields a positive integer value.
          this.dayOffset = (offset - (offset % oneday)) / oneday;

          // Set the pixel offset for the content which will always need
          // to be in the range [0 <= offset <= oneday].
          offset %= oneday;

          // Set the offset at the content node.
          var container =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "container");
          container.x = offset;
          return val;
        ]]></setter>
      </property>

      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/Preferences.jsm");

        this.initTimeRange();
        this.mRange = Number(this.getAttribute("range"));
        this.onLoad();
      ]]></constructor>

      <method name="onLoad">
        <body><![CDATA[
          let numHours = this.mEndHour - this.mStartHour;
          this.mState = new Array(this.mRange * numHours);
          for (let i = 0; i < this.mState.length; i++) {
              this.mState[i] = Components.interfaces.calIFreeBusyInterval.UNKNOWN;
          }

          let step_in_minutes = Math.floor(60 * this.mZoomFactor / 100);
          let formatter = Components.classes[
              "@mozilla.org/calendar/datetime-formatter;1"]
                  .getService(
                      Components.interfaces.calIDateTimeFormatter);
          let date = cal.jsDateToDateTime(new Date());
          date.hour = this.mStartHour;
          date.minute = 0;
          let height = this.parentNode.parentNode.boxObject.height - 1;
          let hours =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "hours");
          if (hours.childNodes.length <= 0) {
              let template = createXULElement("text");
              template.className = "freebusy-grid";
              // TODO: hardcoded value
              let num_days = Math.max(2, 4 * this.mZoomFactor / 100);
              let count = Math.ceil(
                  (this.mEndHour - this.mStartHour) * 60 / step_in_minutes);
              let remain = count;
              for (let day = 1; day <= num_days; day++) {
                  let first = true;
                  while (remain--) {
                      let newNode = template.cloneNode(false);
                      let value = formatter.formatTime(date);
                      if (first) {
                          newNode.className += " first-in-day";
                          first = false;
                      }

                      newNode.setAttribute("value", value);
                      hours.appendChild(newNode);
                      date.minute += step_in_minutes;

                      if (remain == 0) {
                          newNode.className += " last-in-day"
                      }
                  }
                  date.hour = this.mStartHour;
                  date.day++;
                  remain = count;
              }
          }
        ]]></body>
      </method>

      <method name="onFreeBusy">
        <parameter name="aEntries"/>
        <body><![CDATA[
          // The argument denotes the requested freebusy intervals.
          // We need to set our state array according to this
          // result. After the state has been updated we call showState()
          // which will map the entries to attributes on the xul elements.
          if (aEntries) {
              // Remember the free/busy array which is used to find a
              // new time for an event. We store this array only if
              // the provider returned a valid array. In any other case
              // (temporarily clean the display) we keep the last know result.
              this.mEntries = aEntries;

              var kDefaultTimezone = calendarDefaultTimezone();

              var start = this.mStartDate.clone();
              start.hour = 0;
              start.minute = 0;
              start.second = 0;
              start.timezone = kDefaultTimezone;
              var end = start.clone();
              end.day += this.mRange;
              end.timezone = kDefaultTimezone;

              // First of all set all state slots to 'free'
              for (let i = 0; i < this.mState.length; i++) {
                  this.mState[i] = Components.interfaces.calIFreeBusyInterval.FREE;
              }

              let numHours = this.numHours;

              // Iterate all incoming freebusy entries
              for (let entry of aEntries) {
                  let rangeStart = entry.interval.start.getInTimezone(kDefaultTimezone);
                  let rangeEnd = entry.interval.end.getInTimezone(kDefaultTimezone);

                  if (rangeStart.compare(start) < 0) {
                      rangeStart = start.clone();
                  }
                  if (rangeEnd.compare(end) > 0) {
                      rangeEnd = end.clone();
                  }

                  let rangeDuration = rangeEnd.subtractDate(rangeStart);
                  let rangeStartHour = rangeStart.hour;
                  let rangeEndHour = rangeStartHour + (rangeDuration.inSeconds / 3600);

                  if ((rangeStartHour < this.mEndHour) &&
                      (rangeEndHour >= this.mStartHour)) {
                      let dayingrid = start.clone();
                      dayingrid.year = rangeStart.year;
                      dayingrid.month = rangeStart.month;
                      dayingrid.day = rangeStart.day;
                      dayingrid.getInTimezone(kDefaultTimezone);

                      // Ok, this is an entry we're interested in. Find out
                      // which hours are actually occupied.
                      let offset = rangeStart.subtractDate(dayingrid);

                      // Calculate how many days we're offset from the
                      // start of the grid. Eliminate hours in case
                      // we encounter the daylight-saving hop.
                      let dayoffset = dayingrid.subtractDate(start);
                      dayoffset.hours = 0;

                      // Add both offsets to find the total offset.
                      // dayoffset -> offset in days from start of grid
                      // offset -> offset in hours from start of current day
                      offset.addDuration(dayoffset);

                      let duration = rangeEnd.subtractDate(rangeStart);
                      let start_in_minutes = Math.floor(offset.inSeconds / 60);
                      let end_in_minutes = Math.ceil((duration.inSeconds / 60) +
                                                     (offset.inSeconds / 60));

                      let minute2offset = function (value, fNumHours, numHours, start_hour, zoomfactor) {
                          // 'value' is some integer in the interval [0, range * 24 * 60].
                          // we need to map this offset into our array which
                          // holds elements for 'range' days with [start, end] hours each.
                          let minutes_per_day = 24 * 60;
                          let day = (value - (value % minutes_per_day)) / minutes_per_day;
                          let minute = Math.floor(value % minutes_per_day) - (start_hour * 60);

                          minute = Math.max(0, minute);

                          if (minute >= (numHours * 60)) {
                              minute = (numHours * 60) - 1;
                          }
                          // How to get from minutes to offset?
                          // 60 = 100%, 30 = 50%, 15 = 25%, etc.
                          let minutes_per_block = 60 * zoomfactor / 100;

                          let block = Math.floor(minute / minutes_per_block);

                          return Math.ceil(fNumHours) * day + block;
                      };

                      // Number of hours (fractional representation)
                      let numHours = this.mEndHour - this.mStartHour;
                      let fNumHours = numHours * 100 / this.mZoomFactor;

                      let start_offset =
                          minute2offset(start_in_minutes,
                                        fNumHours,
                                        numHours,
                                        this.mStartHour,
                                        this.mZoomFactor);
                      let end_offset =
                          minute2offset(end_in_minutes - 1,
                                        fNumHours,
                                        numHours,
                                        this.mStartHour,
                                        this.mZoomFactor);

                      // Set all affected state slots
                      for (let i = start_offset; i <= end_offset; i++) {
                          this.mState[i] = entry.freeBusyType;
                      }
                  }
              }
          } else {
              // First of all set all state slots to 'unknown'
              for (let i = 0; i < this.mState.length; i++) {
                  this.mState[i] = Components.interfaces.calIFreeBusyInterval.UNKNOWN;
              }
          }

          this.showState();
        ]]></body>
      </method>

      <method name="showState">
        <body><![CDATA[
          let hours =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "hours");
          for (let i = 0; i < hours.childNodes.length; i++) {
              let hour = hours.childNodes[i];
              switch (this.mState[i + this.mOffset]) {
                  case Components.interfaces.calIFreeBusyInterval.FREE:
                      hour.setAttribute("state","free");
                      break;
                  case Components.interfaces.calIFreeBusyInterval.BUSY:
                      hour.setAttribute("state","busy");
                      break;
                  case Components.interfaces.calIFreeBusyInterval.BUSY_TENTATIVE:
                      hour.setAttribute("state","busy_tentative");
                      break;
                  case Components.interfaces.calIFreeBusyInterval.BUSY_UNAVAILABLE:
                      hour.setAttribute("state","busy_unavailable");
                      break;
                  default:
                      hour.removeAttribute("state");
              }
          }
        ]]></body>
      </method>

      <method name="nextSlot">
        <parameter name="aStartTime"/>
        <parameter name="aEndTime"/>
        <parameter name="allDay"/>
        <body><![CDATA[
          var newTime = aStartTime.clone();
          var duration = aEndTime.subtractDate(aStartTime);
          var newEndTime = newTime.clone();
          newEndTime.addDuration(duration);

          var kDefaultTimezone = calendarDefaultTimezone();

          if (this.mEntries) {
              for (var entry of this.mEntries) {
                var rangeStart =
                    entry.interval.start.getInTimezone(kDefaultTimezone);
                var rangeEnd =
                    entry.interval.end.getInTimezone(kDefaultTimezone);

                var isZeroLength = !newTime.compare(newEndTime);
                if ((isZeroLength &&
                     newTime.compare(rangeStart) >= 0 &&
                     newTime.compare(rangeEnd) < 0) ||
                    (!isZeroLength &&
                     newTime.compare(rangeEnd) < 0 &&
                     newEndTime.compare(rangeStart) > 0)) {
                    // Current range of event conflicts with another event.
                    // we need to find a new time for this event. A trivial approach
                    // is to set the new start-time of the event equal to the end-time
                    // of the conflict-range. All-day events need to be considered
                    // separately, in which case we skip to the next day.
                    newTime = rangeEnd.clone();
                    if (allDay) {
                        if (!((newTime.hour == 0) &&
                             (newTime.minute == 0) &&
                             (newTime.second == 0))) {
                            newTime.day++;
                            newTime.hour = 0;
                            newTime.minute = 0;
                            newTime.second = 0;
                        }
                    }
                    newEndTime = newTime.clone();
                    newEndTime.addDuration(duration);
                }
              }
          }

          return newTime;
        ]]></body>
      </method>

      <method name="initTimeRange">
        <body><![CDATA[
          if (this.force24Hours) {
              this.mStartHour = 0;
              this.mEndHour = 24;
          } else {
              this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
              this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
          }
        ]]></body>
      </method>
    </implementation>
  </binding>

  <!-- ############################################################################# -->
  <!-- 'freebusy-grid'-binding                                                       -->
  <!-- ############################################################################# -->

  <!-- id's are evil, use anonid -->
  <binding id="freebusy-grid">
    <content>
      <xul:listbox anonid="listbox"
                   class="listbox-noborder"
                   seltype="multiple"
                   rows="-1"
                   disabled="true"
                   flex="1"
                   style="min-width: 50em; min-height: 30em">
        <xul:listcols>
          <xul:listcol anonid="grid-column" flex="1"/>
        </xul:listcols>
        <xul:listitem anonid="item"
                      class="addressingWidgetItem"
                      allowevents="true">
          <xul:listcell class="addressingWidgetCell">
            <xul:freebusy-row id="attendeeCol4#1"
                              anonid="grid"
                              dirty="true"
                              xbl:inherits="range"/>
          </xul:listcell>
        </xul:listitem>
      </xul:listbox>
    </content>

    <implementation>
      <field name="mContentHeight">0</field>
      <field name="mRowHeight">0</field>
      <field name="mNumColumns">0</field>
      <field name="mMaxFreeBusy">0</field>
      <field name="mPendingRequests">null</field>
      <field name="mStartDate">null</field>
      <field name="mEndDate">null</field>
      <field name="mScrollOffset">0</field>
      <field name="mRange">0</field>
      <field name="mStartHour">0</field>
      <field name="mEndHour">24</field>
      <field name="mForce24Hours">false</field>
      <field name="mZoomFactor">100</field>

      <property name="zoomFactor">
        <getter><![CDATA[
          return this.mZoomFactor;
        ]]></getter>
        <setter><![CDATA[
          this.mZoomFactor = val;
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              var freebusy = this.getFreeBusyElement(i);
              freebusy.zoomFactor = this.mZoomFactor;
          }
          this.forceRefresh();
          return val;
        ]]></setter>
      </property>

      <property name="force24Hours">
        <getter><![CDATA[
          return this.mForce24Hours;
        ]]></getter>
        <setter><![CDATA[
          this.mForce24Hours = val;
          this.initTimeRange();
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              var freebusy = this.getFreeBusyElement(i);
              freebusy.force24Hours = this.mForce24Hours;
          }
          return val;
        ]]></setter>
      </property>

      <property name="firstVisibleRow">
        <getter><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          return listbox.getIndexOfFirstVisibleRow();
        ]]></getter>
        <setter><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          listbox.scrollToIndex(val);
          return val;
        ]]></setter>
      </property>

      <property name="ratio">
        <setter><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          var rowcount = listbox.getRowCount();
          listbox.scrollToIndex(Math.floor(rowcount * val));
          return val;
        ]]></setter>
      </property>

      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/Preferences.jsm");

        var args = window.arguments[0];

        this.initTimeRange();

        this.mRange = Number(this.getAttribute("range"));

        this.mMaxFreeBusy = 0;
        this.mPendingRequests = [];

        var self = this;
        var load = function freebusy_grid_loadHandler() {
            self.onLoad();
        };
        window.addEventListener("load", load, true);
        var unload = function freebusy_grid_unloadHandler() {
            self.onUnload();
        };
        window.addEventListener("unload", unload, true);
      ]]></constructor>

      <property name="startDate">
        <getter><![CDATA[
          return this.mStartDate;
        ]]></getter>
        <setter><![CDATA[
          this.mStartDate = val.clone();
          this.mStartDate.makeImmutable();
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              this.getFreeBusyElement(i).startDate = val;
          }
          return val;
        ]]></setter>
      </property>

      <property name="endDate">
        <getter><![CDATA[
          return this.mEndDate;
        ]]></getter>
        <setter><![CDATA[
          this.mEndDate = val.clone();
          this.mEndDate.makeImmutable();
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              this.getFreeBusyElement(i).endDate = val;
          }
          return val;
        ]]></setter>
      </property>

      <property name="documentSize">
        <getter><![CDATA[
          return this.getFreeBusyElement(1).documentSize;
        ]]></getter>
      </property>

      <method name="onLoad">
        <body><![CDATA[
          this.onInitialize();
        ]]></body>
      </method>

      <method name="onUnload">
        <body><![CDATA[
          // Cancel pending free/busy requests
          for (var request of this.mPendingRequests) {
              request.cancel(null);
          }

          this.mPendingRequests = [];
        ]]></body>
      </method>

      <method name="onInitialize">
        <body><![CDATA[
          var args = window.arguments[0];
          var startTime = args.startTime;
          var endTime = args.endTime;
          var calendar = args.calendar;

          var kDefaultTimezone = calendarDefaultTimezone();
          this.startDate = startTime.getInTimezone(kDefaultTimezone);
          this.endDate = endTime.getInTimezone(kDefaultTimezone);

          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          var template =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "item");
          this.appendNewRow(listbox, template, null);
          template.remove();

          this.updateFreeBusy();
        ]]></body>
      </method>

      <method name="onChangeCalendar">
        <parameter name="calendar"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <!-- appends a new empty row -->
      <method name="appendNewRow">
        <parameter name="aParentNode"/>
        <parameter name="aTemplateNode"/>
        <parameter name="aReplaceNode"/>
        <body><![CDATA[
          this.mMaxFreeBusy++;
          var newNode = aTemplateNode.cloneNode(true);
          if (aReplaceNode) {
              aParentNode.replaceChild(newNode, aReplaceNode);
          } else {
              aParentNode.appendChild(newNode);
          }

          var grid =
              document.getAnonymousElementByAttribute(
                  newNode, "anonid", "grid");
          var rowNumber = this.mMaxFreeBusy;
          if (rowNumber >= 0) {
              grid.setAttribute("id", "attendeeCol4#" + rowNumber);
          }

          // Propagate start/enddate to the new row.
          grid.startDate = this.mStartDate;
          grid.endDate = this.mEndDate;

          grid.force24Hours = this.mForce24Hours;
          grid.zoomFactor = this.mZoomFactor;

          // We always clone the first row.  The problem is that the first row
          // could be focused.  When we clone that row, we end up with a cloned
          // XUL textbox that has a focused attribute set.  Therefore we think
          // we're focused and don't properly refocus.  The best solution to this
          // would be to clone a template row that didn't really have any presentation,
          // rather than using the real visible first row of the listbox.
          // For now we'll just put in a hack that ensures the focused attribute
          // is never copied when the node is cloned.
          if (grid.getAttribute('focused') != '') {
              grid.removeAttribute('focused');
          }
        ]]></body>
      </method>

      <property name="scroll">
        <setter><![CDATA[
          this.mScrollOffset = val;
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              this.getFreeBusyElement(i).scroll = val;
          }
          return val;
        ]]></setter>
      </property>

      <method name="onModify">
        <parameter name="event"/>
        <body><![CDATA[
          // Add or remove rows depending on the number of items
          // contained in the list passed as argument.
          var list = event.details;
          if (this.mMaxFreeBusy != list.length) {
              var listbox =
                  document.getAnonymousElementByAttribute(
                      this, "anonid", "listbox");
              var template =
                  document.getAnonymousElementByAttribute(
                      this, "anonid", "item");
              while (this.mMaxFreeBusy < list.length) {
                  var nextDummy = this.getNextDummyRow();
                  this.appendNewRow(listbox, template, nextDummy);
                  template =
                      document.getAnonymousElementByAttribute(
                          this, "anonid", "item");
              }
              while (this.mMaxFreeBusy > list.length) {
                  this.deleteRow(this.mMaxFreeBusy);
              }
          }

          // Store the attributes in our grid rows.
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              var freebusy = this.getFreeBusyElement(i);
              freebusy.setAttribute("calid", list[i - 1].calid);
              freebusy.removeAttribute("dirty");
              if (list[i - 1].dirty) {
                  freebusy.setAttribute("dirty", "true");
              }
          }

          // Align all rows
          this.scroll = this.mScrollOffset;

          this.updateFreeBusy();
        ]]></body>
      </method>

      <!-- updateFreeBusy(), implementation of the core functionality of this binding -->
      <method name="updateFreeBusy">
        <body><![CDATA[
          var fbService = getFreeBusyService();
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              // Retrieve the string from the appropriate row
              var freebusy = this.getFreeBusyElement(i);
              if (freebusy.hasAttribute("dirty")) {
                  freebusy.removeAttribute("dirty");
                  var calid = freebusy.getAttribute("calid");
                  if (calid && calid.length > 0) {
                      // Define the datetime range we would like to ask for.
                      var start = this.mStartDate.clone();
                      start.hour = 0;
                      start.minute = 0;
                      start.second = 0;
                      var end = start.clone();
                      end.day += this.mRange;
                      // Update with 'no data available' until response will be received
                      freebusy.onFreeBusy(null);
                      try {
                          var listener = new calFreeBusyListener(freebusy, this);
                          var request = fbService.getFreeBusyIntervals(calid,
                                                                       start,
                                                                       end,
                                                                       Components.interfaces.calIFreeBusyInterval.BUSY_ALL,
                                                                       listener);
                          if (request && request.isPending) {
                              this.mPendingRequests.push(request);
                          }
                      } catch (ex) {
                          Components.utils.reportError(ex);
                      }
                  }
              }
          }
        ]]></body>
      </method>

      <method name="nextSlot">
        <body><![CDATA[
          var startTime = this.mStartDate.clone();
          var endTime = this.mEndDate.clone();

          startTime.isDate = false;
          endTime.isDate = false;

          var allDay = this.mStartDate.isDate;
          var step_in_minutes = Math.floor(60 * this.zoomFactor / 100);
          if (allDay) {
              step_in_minutes = 60 * 24;
              endTime.day++;
          }

          var duration = endTime.subtractDate(startTime);

          startTime.minute += step_in_minutes;

          if (startTime.hour < this.mStartHour) {
              startTime.hour = this.mStartHour;
              startTime.minute = 0;
          }

          endTime = startTime.clone();
          endTime.addDuration(duration);
          if (endTime.hour > this.mEndHour) {
              startTime.day++;
              startTime.hour = this.mStartHour;
              startTime.minute = 0;
              endTime = startTime.clone();
              endTime.addDuration(duration);
              if (endTime.hour > this.mEndHour) {
                  return this.mStartDate.clone();
              }
          }

          // Now iterate all freebusy-rows and ask each one
          // if it wants to modify the suggested time slot.
          // we keep iterating the rows until all of them
          // are happy with it.
          var recheck;
          do {
              recheck = false;

              for (var i = 1; i <= this.mMaxFreeBusy; i++) {
                  var row = this.getFreeBusyElement(i);
                  var newTime = row.nextSlot(startTime, endTime, allDay);
                  if (newTime) {
                    if (newTime.compare(startTime) != 0) {
                        startTime = newTime;

                        if (startTime.hour < this.mStartHour) {
                            startTime.hour = this.mStartHour;
                            startTime.minute = 0;
                        }

                        endTime = startTime.clone();
                        endTime.addDuration(duration);

                        if (endTime.hour > this.mEndHour) {
                            startTime.day++;
                            startTime.hour = this.mStartHour;
                            startTime.minute = 0;
                            endTime = startTime.clone();
                            endTime.addDuration(duration);
                        }

                        recheck = true;
                    }
                } else {
                    // A new slot could not be found
                    // and the given time was also invalid.
                    return this.mStartDate.clone();
                }
              }
          } while (recheck);

          // Return the unmodifed startdate of the item
          // in case no possible match was found.
          if (startTime.compare(this.mStartDate) == 0) {
              return this.mStartDate.clone();
          }

          // Special case for allday events - if the original
          // datetime was indeed a date we need to carry this
          // state over to the calculated datetime.
          if (this.mStartDate.isDate) {
              startTime.isDate = true;
          }

          // In case the new starttime happens to be scheduled
          // on a different day, we also need to update the
          // complete freebusy informations and appropriate
          // underlying arrays holding the informaion.
          if (this.mStartDate.day != startTime.day) {
              for (var i = 1; i <= this.mMaxFreeBusy; i++) {
                  var row = this.getFreeBusyElement(i);
                  row.setAttribute("dirty", "true");
              }
              this.updateFreeBusy();
          }

          // Return the new starttime of the item.
          return startTime;
        ]]></body>
      </method>

      <method name="forceRefresh">
        <body><![CDATA[
          for (var i = 1; i <= this.mMaxFreeBusy; i++) {
              var row = this.getFreeBusyElement(i);
              row.setAttribute("dirty", "true");
          }
          this.updateFreeBusy();
        ]]></body>
      </method>

      <!-- This method returns the <xul:listitem> at row numer 'aRow' -->
      <method name="getListItem">
        <parameter name="aRow"/>
        <body><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          if (listbox && aRow > 0) {
              var listitems = listbox.getElementsByTagNameNS('*', 'listitem');
              if (listitems && listitems.length >= aRow) {
                  return listitems[aRow - 1];
              }
          }
          return 0;
        ]]></body>
      </method>

      <method name="getFreeBusyElement">
        <parameter name="aRow"/>
        <body><![CDATA[
          return document.getAnonymousElementByAttribute(this, "id",
                                                         "attendeeCol4#" + aRow);
        ]]></body>
      </method>

      <method name="deleteRow">
        <parameter name="aRow"/>
        <body><![CDATA[
          // Reset id's in order to not break the sequence
          var max = this.mMaxFreeBusy;
          this.removeRow(aRow);
          var numberOfCols = this.numColumns;
          for (var row = aRow + 1; row <= max; row++) {
              for (var col = 1; col <= numberOfCols; col++) {
                  var colID = "attendeeCol" + col + "#" + row;
                  var elem = document.getAnonymousElementByAttribute(this, "id", colID);
                  if (elem) {
                      elem.setAttribute(
                          "id",
                          "attendeeCol" + (col) + "#" + (row - 1));
                  }
              }
          }
        ]]></body>
      </method>

      <method name="removeRow">
        <parameter name="aRow"/>
        <body><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          this.getListItem(aRow).remove();
          this.fitDummyRows();
          this.mMaxFreeBusy--;
        ]]></body>
      </method>

      <!-- gets the next row from the top down -->
      <method name="getNextDummyRow">
        <body><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          var kids = listbox.childNodes;
          for (var i = 0; i < kids.length; ++i) {
              if (kids[i].hasAttribute("_isDummyRow")) {
                  return kids[i];
              }
          }
          return null;
        ]]></body>
      </method>

      <method name="fitDummyRows">
        <body><![CDATA[
          var self = this;
          var func = function func() {
              self.calcContentHeight();
              self.createOrRemoveDummyRows();
          }
          setTimeout(func, 0);
        ]]></body>
      </method>

      <method name="calcContentHeight">
        <body><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          var items = listbox.getElementsByTagNameNS('*', 'listitem');
          this.mContentHeight = 0;
          if (items.length > 0) {
              var i = 0;
              do {
                  this.mRowHeight = items[i].boxObject.height;
                  ++i;
              } while (i < items.length && !this.mRowHeight);
              this.mContentHeight = this.mRowHeight * items.length;
          }
        ]]></body>
      </method>

      <method name="createOrRemoveDummyRows">
        <body><![CDATA[
          var listbox =
              document.getAnonymousElementByAttribute(
                  this, "anonid", "listbox");
          var listboxHeight = listbox.boxObject.height;

          // Remove rows to remove scrollbar
          var kids = listbox.childNodes;
          for (var i = kids.length - 1; this.mContentHeight > listboxHeight && i >= 0; --i) {
              if (kids[i].hasAttribute("_isDummyRow")) {
                  this.mContentHeight -= this.mRowHeight;
                  kids[i].remove();
              }
          }

          // Add rows to fill space
          if (this.mRowHeight) {
              while (this.mContentHeight + this.mRowHeight < listboxHeight) {
                  this.createDummyItem(listbox);
                  this.mContentHeight += this.mRowHeight;
              }
          }
        ]]></body>
      </method>

      <method name="createDummyCell">
        <parameter name="aParent"/>
        <body><![CDATA[
          var cell = document.createElement("listcell");
          cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
          if (aParent) {
              aParent.appendChild(cell);
          }
          return cell;
        ]]></body>
      </method>

      <method name="createDummyItem">
        <parameter name="aParent"/>
        <body><![CDATA[
          var titem = document.createElement("listitem");
          titem.setAttribute("_isDummyRow", "true");
          titem.setAttribute("class", "dummy-row");
          for (var i = this.numColumns; i > 0; i--) {
              this.createDummyCell(titem);
          }
          if (aParent) {
              aParent.appendChild(titem);
          }
          return titem;
        ]]></body>
      </method>

      <property name="numColumns">
        <getter><![CDATA[
          if (!this.mNumColumns) {
              var listbox =
                  document.getAnonymousElementByAttribute(
                      this, "anonid", "listbox");
              var listCols = listbox.getElementsByTagNameNS('*', 'listcol');
              this.mNumColumns = listCols.length || 1;
          }
          return this.mNumColumns;
        ]]></getter>
      </property>

      <method name="initTimeRange">
        <body><![CDATA[
          if (this.force24Hours) {
              this.mStartHour = 0;
              this.mEndHour = 24;
          } else {
              this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
              this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
          }
        ]]></body>
      </method>
    </implementation>
  </binding>
</bindings>
