/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */

(function() {
  'use strict';

  /**
   * @name Attendees
   * @constructor
   * @param {object} component - a Component object instance
   */
  function Attendees(component) {
    this.component = component;
    if (this.component.attendees) {
      _.forEach(this.component.attendees, function(attendee) {
        attendee.image = Attendees.$gravatar(attendee.email, 32);
      });
    }
    this.workDaysOnly = true;
    this.slotStartTimeLimit = new Date();
    this.slotStartTimeLimit.setMinutes(0);
    this.slotStartTimeLimit.setHours(Attendees.dayStartHour);
    this.slotEndTimeLimit = new Date();
    this.slotEndTimeLimit.setMinutes(0);
    this.slotEndTimeLimit.setHours(Attendees.dayEndHour);
    this.$days = [];
    this.$futureFreebusyData = {};
    this.updateFreeBusyCoverage();
    this.updateFreeBusy();
    if (this.$days.length == 0) {
      this.getDays();
    }
  }

  /**
   * @memberof Attendees
   * @desc The factory we'll use to register with Angular
   * @returns the Attendees constructor
   */
  Attendees.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Attendees_ROLES', 'Preferences', 'User', 'Card', 'Gravatar', 'Resource', function($q, $timeout, $log, Settings, ROLES, Preferences, User, Card, Gravatar, Resource) {
    angular.extend(Attendees, {
      $q: $q,
      $timeout: $timeout,
      $log: $log,
      $settings: Settings,
      $User: User,
      $Preferences: Preferences,
      $Card: Card,
      $gravatar: Gravatar,
      $$resource: new Resource(Settings.activeUser('folderURL') + 'Calendar', Settings.activeUser()),
      ROLES: ROLES
    });

    Attendees.dayStartHour = parseInt(Preferences.defaults.SOGoDayStartTime.split(':')[0]);
    Attendees.dayEndHour = parseInt(Preferences.defaults.SOGoDayEndTime.split(':')[0]);

    return Attendees; // return constructor
  }];

  /**
   * @module SOGo.SchedulerUI
   * @desc Factory registration of Attendees in Angular module.
   */
  try {
    angular.module('SOGo.SchedulerUI');
  }
  catch(e) {
    angular.module('SOGo.SchedulerUI', ['SOGo.Common']);
  }
  angular.module('SOGo.SchedulerUI')
    .constant('Attendees_ROLES', {
      REQ_PARTICIPANT: 'req-participant',
      OPT_PARTICIPANT: 'opt-participant',
      NON_PARTICIPANT: 'non-participant',
      CHAIR: 'chair'
    })
    .factory('Attendees', Attendees.$factory);

  /**
   * @function timeToQuarters
   * @memberof Attendees
   * @param {date} dateTime - a Date object instance
   * @desc Return the number of quarters matching the time
   * @returns the number of quarters
   */
  Attendees.timeToQuarters = function(dateTime) {
    return dateTime.getHours() * 4 + Math.ceil(dateTime.getMinutes()/15);
  };

  /**
   * @function getLength
   * @memberof Attendees.prototype
   * @returns the number of attendees
   */
  Attendees.prototype.getLength = function() {
    return this.component.attendees ? this.component.attendees.length : 0;
  };

  /**
   * @function initOrganizer
   * @memberof Attendees.prototype
   * @desc Extend instance with organizer including her freebusy information.
   * @param {object} calendar - Calendar instance associated to current component
   */
  Attendees.prototype.initOrganizer = function(calendar) {
    var _this = this, promise;
    if (calendar && calendar.isSubscription) {
      promise = Attendees.$User.$filter(calendar.owner).then(function(results) {
        var owner = results[0];
        _this.component.organizer = {
          uid: owner.uid,
          name: owner.cn,
          email: owner.c_email
        };
      });
    }
    else {
      if (!this.component.organizer) {
        this.component.organizer = {
          uid: Attendees.$settings.activeUser('login'),
          name: Attendees.$settings.activeUser('identification'),
          email: Attendees.$settings.activeUser('email')
        };
      }
      promise = Attendees.$q.when();
    }
    // Fetch organizer's freebusy
    promise.then(function() {
      _this.updateFreeBusyAttendee(_this.component.organizer);
    });
  };

  /**
   * @function add
   * @memberof Attendees.prototype
   * @desc Add an attendee and fetch his freebusy info.
   * @param {Object} card - an Card object instance to be added to the attendees list
   */
  Attendees.prototype.add = function(card, options) {
    var _this = this, attendee, list, url, params, promise = Attendees.$q.when();
    if (card) {
      if (!this.component.attendees || (options && options.organizerCalendar)) {
        // No attendee yet; initialize the organizer
        this.initOrganizer(options? options.organizerCalendar : null);
      }
      if (card.$isList({expandable: true})) {
        // Decompose list members
        list = Attendees.$Card.$find(card.container, card.c_name);
        promise = list.$id().then(function(listId) {
          _.forEach(list.refs, function(ref) {
            attendee = {
              name: ref.c_cn,
              email: ref.$preferredEmail(options? options.partial : null),
              role: Attendees.ROLES.REQ_PARTICIPANT,
              partstat: 'needs-action',
              uid: ref.c_uid,
              $avatarIcon: 'person',
            };
            if (!_.find(_this.component.attendees, function(o) {
              return o.email == attendee.email;
            })) {
              // Contact is not already an attendee, add it
              attendee.image = Attendees.$gravatar(attendee.email, 32);
              if (_this.component.attendees)
                _this.component.attendees.push(attendee);
              else
                _this.component.attendees = [attendee];
              _this.updateFreeBusyAttendee(attendee);
            }
          });
        });
      }
      else {
        // Single contact
        attendee = {
          uid: card.c_uid,
          domain: card.c_domain,
          isMSExchange: card.ismsexchange,
          isGroup: card.$isList(),
          isExpandableGroup: false,
          isResource: card.isresource,
          name: card.c_cn,
          email: card.$$email,
          role: Attendees.ROLES.REQ_PARTICIPANT,
          partstat: 'needs-action',
          $avatarIcon: card.$avatarIcon
        };
        if (!_.find(this.attendees, function(o) {
          return o.email == attendee.email;
        })) {
          if (card.$isList() && Attendees.$Preferences.defaults.SOGoLDAPGroupExpansionEnabled) {
            // LDAP list -- preload members
            promise = card.$members().then(function(members) {
              attendee.members = members;
              attendee.isExpandableGroup = true;
            });
          }
          attendee.image = Attendees.$gravatar(attendee.email, 32);
          if (this.component.attendees) {
            if (_.findIndex(this.component.attendees, { email: attendee.email }) < 0)
              this.component.attendees.push(attendee);
          }
          else
            this.component.attendees = [attendee];
          this.updateFreeBusyAttendee(attendee);
        }
      }
    }

    return promise;
  };

  /**
   * @function nextRole
   * @memberof Attendees.prototype
   * @desc Switch the attendee to the next participation role.
   * @param {Object} attendee - the attendee definition
   */
  Attendees.prototype.nextRole = function(attendee) {
    var roles = _.values(Attendees.ROLES);
    var index = _.findIndex(roles, function(role) {
      return attendee.role === role;
    });
    attendee.role = roles[++index % 4];
  };

  /**
   * @function hasAttendee
   * @memberof Attendees.prototype
   * @desc Verify if one of the email addresses of a Card instance matches an attendee.
   * @param {Object} card - an Card object instance
   * @returns true if the Card matches an attendee
   */
  Attendees.prototype.hasAttendee = function(card) {
    var attendee = _.find(this.component.attendees, function(attendee) {
      return _.find(card.emails, function(email) {
        return email.value == attendee.email;
      });
    });
    return angular.isDefined(attendee);
  };

  /**
   * @function remove
   * @memberof Attendees.prototype
   * @desc Remove an attendee from the component.
   * @param {Object} attendee - an object literal defining an attendee
   */
  Attendees.prototype.remove = function(attendee) {
    var index = _.findIndex(this.component.attendees, function(currentAttendee) {
      return currentAttendee.email == attendee.email;
    });
    if (index > -1)
      this.component.attendees.splice(index, 1);
    delete this.$futureFreebusyData[attendee.uid];
  };

  /**
   * @function updateFreeBusyCoverage
   * @memberof Attendees.prototype
   * @desc Build a 15-minute-based representation of the component's period.
   * @returns an object literal hashed by days and hours and arrays of four 1's and 0's
   */
  Attendees.prototype.updateFreeBusyCoverage = function() {
    var _this = this, freebusy = {};
    var roundedStart, roundedEnd, startQuarter, endQuarter;

    if (this.component.start && this.component.end) {
      roundedStart = new Date(this.component.start.getTime());
      roundedEnd = new Date(this.component.end.getTime());
      if (this.component.isAllDay) {
        roundedStart.setHours(Attendees.dayStartHour);
        roundedStart.setMinutes(0);
        roundedEnd.setHours(Attendees.dayEndHour);
        roundedEnd.setMinutes(0);
        startQuarter = endQuarter = 0;
      }
      else {
        startQuarter = parseInt(roundedStart.getMinutes()/15 + 0.5);
        endQuarter = parseInt(roundedEnd.getMinutes()/15 + 0.5);
      }
      roundedStart.setMinutes(15*startQuarter);
      roundedEnd.setMinutes(15*endQuarter);

      _.forEach(roundedStart.beginOfDay().daysUpTo(roundedEnd.beginOfDay()), function(date, index) {
        if (date < roundedStart)
          date = new Date(roundedStart.getTime());
        var currentDay = date.getDate(),
            dayKey = date.getDayString(),
            hourKey;
        if (dayKey === roundedStart.getDayString()) {
          hourKey = date.getHours().toString();
          freebusy[dayKey] = {};
          freebusy[dayKey][hourKey] = [];
          while (startQuarter > 0) {
            freebusy[dayKey][hourKey].push(0);
            startQuarter--;
          }
        }
        else {
          date = date.beginOfDay();
          freebusy[dayKey] = {};
        }
        while (date.getTime() < roundedEnd.getTime() &&
               date.getDate() == currentDay) {
          hourKey = date.getHours().toString();
          if (angular.isUndefined(freebusy[dayKey][hourKey]))
            freebusy[dayKey][hourKey] = [];
          freebusy[dayKey][hourKey].push(1);
          date.addMinutes(15);
        }
      });
      this.freebusy = freebusy;
    }
  };

  /**
   * @function coversFreeBusy
   * @memberof Attendees.prototype
   * @desc Check if a specific quarter matches the component's period.
   * @returns true if the quarter covers the component's period
   */
  Attendees.prototype.coversFreeBusy = function(day, hour, quarter) {
    var b = (this.freebusy &&
             angular.isDefined(this.freebusy[day]) &&
             angular.isDefined(this.freebusy[day][hour]) &&
             this.freebusy[day][hour][quarter] == 1);
    return b;
  };

  /**
   * @function getDays
   * @memberof Attendees.prototype
   * @desc Define a period of one week before and one week after the component's period or a reference date.
   * @param refDate - a Date object
   * @returns an array of objects representing the days
   */
  Attendees.prototype.getDays = function(refDate) {
    var _this = this, sd, ed, formatFcn;

    if (refDate) {
      sd = refDate;
      ed = new Date(refDate.getTime());
      ed.addMinutes(this.component.delta);
    }
    else {
      sd = this.component.start;
      ed = this.component.end;
    }

    if (this.$days.length === 0 ||
        _.findIndex(this.$days, ['getDayString', sd.getDayString()]) < 0 ||
        _.findIndex(this.$days, ['getDayString', ed.getDayString()]) < 0) {
      sd = sd.beginOfDay().addDays(-7);
      ed = ed.beginOfDay().addDays(7);
      formatFcn = Attendees.$Preferences.$mdDateLocaleProvider.formatDate;
      this.$days.splice(0, this.$days.length);
      _.forEach(sd.daysUpTo(ed), function(date) {
        date.$dateFormat = Attendees.$Preferences.defaults.SOGoLongDateFormat;
        _this.$days.push({
          stringWithSeparator: formatFcn(date),
          getDayString: date.getDayString()
        });
      });
    }

    return this.$days;
  };

  /**
   * @function updateFreeBusy
   * @memberof Attendees.prototype
   * @desc Fetch the freebusy information of the organizer and all attendees.
   * @returns a promise of the all HTTP operations
   */
  Attendees.prototype.updateFreeBusy = function(refDate) {
    var _this = this, promises = [];

    if (this.getLength() > 0) {
      if (this.component.organizer) {
        promises.push(this.updateFreeBusyAttendee(this.component.organizer, refDate));
      }
      _.forEach(_.filter(this.component.attendees, 'uid'), function(attendee) {
        promises.push(_this.updateFreeBusyAttendee(attendee, refDate));
      });
    }

    return Attendees.$q.all(promises);
  };

  /**
   * @function updateFreeBusyAttendee
   * @memberof Attendees.prototype
   * @desc Update the freebusy information for the component's period for a specific attendee.
   * @param {Object} card - an Card object instance of the attendee
   * @returns a promise of the HTTP operation if the information was not cached
   */
  Attendees.prototype.updateFreeBusyAttendee = function(attendee, refDate) {
    var promise, resource, uid, sd, ed, params, days;

    if (attendee.uid) {
      uid = attendee.uid;
      if (attendee.domain)
        uid += '@' + attendee.domain;
      days = _.map(this.getDays(refDate), 'getDayString');
      params =
        {
          sday: days[0],
          eday: days[days.length - 1]
        };

      if (attendee.isMSExchange) {
        // Attendee is not a local user, but her freebusy data is available from an external MS Exchange server;
        // we query /SOGo/so/<login_user>/freebusy.ifb/ajaxRead?uid=<uid>
        resource = Attendees.$$resource.userResource();
        params.uid = uid;
      }
      else {
        // Attendee is a user;
        // web query /SOGo/so/<uid>/freebusy.ifb/ajaxRead
        resource = Attendees.$$resource.userResource(uid);
      }

      if (angular.isUndefined(attendee.freebusy))
        attendee.freebusy = {};

      if (_.intersection(_.keys(attendee.freebusy), days).length !== days.length) {
        // Fetch FreeBusy information
        promise = resource.fetch('freebusy.ifb', 'ajaxRead', params).then(function(data) {
          _.forEach(days, function(day) {
            var hour;

            if (angular.isUndefined(attendee.freebusy[day]))
              attendee.freebusy[day] = {};

            if (angular.isUndefined(data[day]))
              data[day] = {};

            for (var i = 0; i <= 23; i++) {
              hour = i.toString();
              if (data[day][hour])
                attendee.freebusy[day][hour] = [
                  data[day][hour]["0"],
                  data[day][hour]["15"],
                  data[day][hour]["30"],
                  data[day][hour]["45"]
                ];
              else
                attendee.freebusy[day][hour] = [0, 0, 0, 0];
            }
          });
        });
      }
      else {
        promise = Attendees.$q.when();
      }

      this.$futureFreebusyData[attendee.uid] = promise;

      return promise;
    }
  };


  /**
   * @function forwardFindDate
   * @memberof Attendees.prototype
   * @desc Find the next slot for which all attendees are available whitin the reference day
   * @param {date} currentStart - the reference day
   * @returns a date object or null if no slot were found
   */
  Attendees.prototype.forwardFindDate = function(currentStart) {
    var foundDate = null;
    var maxOffset = this.endLimit - this.duration;
    var offset = 0;

    if (this.firstStep) {
      offset = Math.floor(this.start.getHours() * 4 + this.start.getMinutes() / 15) + 1;
      this.firstStep = false;
    }
    else {
      offset = this.currentEntries.indexOf(0);
    }
    if (offset > -1 && offset < this.startLimit) {
      offset = this.startLimit;
    }

    while (!foundDate && offset > -1 && offset <= maxOffset) {
      var testDuration = 0;
      while (this.currentEntries[offset] === 0 && testDuration < this.duration) {
        testDuration++;
        offset++;
      }
      if (testDuration == this.duration) {
        foundDate = new Date();
        var foundTime = (currentStart.getTime() + (offset - testDuration) * 900000);
        foundDate.setTime(foundTime);
      }
      else {
        offset = this.currentEntries.indexOf(0, offset + 1);
      }
    }

    return foundDate;
  };

  /**
   * @function forwardAdjustCurrentStart
   * @memberof Attendees.prototype
   * @desc Adjust a date to the next non-weekend day
   * @param {date} currentStart - the reference day
   */
  Attendees.prototype.forwardAdjustCurrentStart = function (currentStart) {
    var day = currentStart.getDay();
    if (day === 0) {
      currentStart.addDays(1);
    }
    else if (day === 6) {
      currentStart.addDays(2);
    }
  };

  /**
   * @function backwardFindDate
   * @memberof Attendees.prototype
   * @desc Find the previous slot for which all attendees are available whitin the reference day
   * @param {date} currentStart - the reference day
   * @returns a date object or null if no slot were found
   */
  Attendees.prototype.backwardFindDate = function (currentStart) {
    var foundDate = null;
    var maxOffset = this.endLimit - this.duration;
    var offset;
    if (this.firstStep) {
      offset = Math.floor(this.start.getHours() * 4 + this.start.getMinutes() / 15) - 1;
      this.firstStep = false;
    }
    else {
      offset = this.currentEntries.lastIndexOf(0);
    }
    if (offset > maxOffset) {
      offset = maxOffset;
    }
    while (!foundDate && offset >= this.startLimit) {
      var testDuration = 0;
      var testOffset = offset;
      while (this.currentEntries[testOffset] === 0 && testDuration < this.duration) {
        testDuration++;
        testOffset++;
      }
      if (testDuration == this.duration) {
        foundDate = new Date();
        var foundTime = (currentStart.getTime() + offset * 900000);
        foundDate.setTime(foundTime);
      }
      else {
        offset = this.currentEntries.lastIndexOf(0, offset - 1);
      }
    }
    Attendees.$log.debug(['found = ' + foundDate, offset]);
    return foundDate;
  };

  /**
   * @function backwardAdjustCurrentStart
   * @memberof Attendees.prototype
   * @desc Adjust a date to the previous non-weekend day
   * @param {date} currentStart - the reference day
   */
  Attendees.prototype.backwardAdjustCurrentStart = function (currentStart) {
    var day = currentStart.getDay();
    if (day == 0) {
      currentStart.addDays(-2);
    }
    else if (day == 6) {
      currentStart.addDays(-1);
    }
  };

  /**
   * @function findSlot
   * @memberof Attendees.prototype
   * @desc Find the next or previous slot when all attendees are available.
   * @param {number} direction - the search direction (1 or -1)
   */
  Attendees.prototype.findSlot = function(direction) {
    var _this = this, currentStart;

    this.direction = direction;
    this.firstStep = true;

    if (direction > 0) {
      this.findDate = this.forwardFindDate;
      this.adjustCurrentStart = this.forwardAdjustCurrentStart;
    }
    else {
      this.findDate = this.backwardFindDate;
      this.adjustCurrentStart = this.backwardAdjustCurrentStart;
    }

    if (this.component.isAllDay) {
      // Event lasts all day within limits
      this.start = this.component.start.clone();
      this.start.setHours(Attendees.dayStartHour);
      this.start.setMinutes(0);
      this.start.setSeconds(0);

      this.end = this.component.end.clone();
      this.end.setHours(Attendees.dayEndHour);
      this.end.setMinutes(0);
      this.end.setSeconds(0);

      this.startLimit = Attendees.dayStartHour * 4; // from user's defaults
      this.endLimit = Attendees.dayEndHour * 4; // from user's defaults

      this.duration = (Attendees.dayEndHour - Attendees.dayStartHour) * 4;
    }
    else {
      // Event can be outside limits
      this.start = this.component.start;
      this.end = this.component.end;

      this.startLimit = Attendees.timeToQuarters(this.slotStartTimeLimit); // from time picker
      this.endLimit = Attendees.timeToQuarters(this.slotEndTimeLimit); // from time picker

      this.duration = Math.ceil((this.end.getTime() - this.start.getTime()) / 900000);
    }

    currentStart = this.component.start.clone();
    currentStart.setHours(0, 0, 0, 0);

    if (this.workDaysOnly) {
      this.adjustCurrentStart(currentStart);
    }

    // Start a recursive search
    return this.step(currentStart).then(function (foundDate) {
      _this.component.start = new Date(foundDate.getTime());
      _this.component.end = new Date(_this.component.start.getTime());
      _this.component.end.addMinutes(_this.component.delta);
      _this.updateFreeBusyCoverage();
      return foundDate;
    }).catch(function (err) {
      _this.updateFreeBusy();
      throw err;
    });
  };

  /**
   * @function mergeFreebusy
   * @memberof Attendees.prototype
   * @desc Merge freebusy information of organizer and all attendees for a referene date.
   * @param {date) start - the reference date
   */
  Attendees.prototype.mergeFreebusy = function(start) {
    var _this = this;
    var startDay = start.getDayString();

    return this.updateFreeBusy(start).then(function () {
      var i, j, attendee, attendeeEntries;
      _this.currentEntries = _.flatMap(_this.component.organizer.freebusy[startDay]);
      for (i = 0; i < _this.component.attendees.length; i++) {
        attendee = _this.component.attendees[i];
        if (attendee.freebusy && attendee.role !== Attendees.ROLES.NON_PARTICIPANT) {
          attendeeEntries = _.flatMap(attendee.freebusy[startDay]);
          for (j = 0; j < _this.currentEntries.length; j++) {
            _this.currentEntries[j] += attendeeEntries[j];
          }
        }
      }
    });
  };

  /**
   * @function step
   * @memberof Attendees.prototype
   * @desc Recursively search for the next available slot, one day a the time.
   * @param {date) currentStart - the starting day
   */
  Attendees.prototype.step = function(currentStart, count) {
    var _this = this;
    if (!parseInt(count)) {
      count = 0;
    } else if (count >= 30) {
      return Attendees.$q.reject(l('There\'s no free slot available for all attendees in the next 30 days. Please try a different date or length.'));
    }
    // var currentStartDay = currentStart.getDayString();
    return this.mergeFreebusy(currentStart).then(function () {
      var foundDate = _this.findDate(currentStart);
      if (foundDate) {
        return foundDate;
      }
      else {
        currentStart.addDays(_this.direction > 0 ? 1 : -1);
        currentStart.setHours(0, 0, 0, 0);
        if (_this.workDaysOnly) {
          _this.adjustCurrentStart(currentStart);
        }
        return _this.step(currentStart, count + 1);
      }
    });
  };

})();
