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

(function() {
  'use strict';

  /**
   * @name AddressBook
   * @constructor
   * @param {object} futureAddressBookData - either an object literal or a promise
   */
  function AddressBook(futureAddressBookData) {
    // Data is immediately available
    if (typeof futureAddressBookData.then !== 'function') {
      this.init(futureAddressBookData);
      if (this.name && !this.id) {
        // Create a new addressbook on the server
        var newAddressBookData = AddressBook.$$resource.create('createFolder', this.name);
        this.$unwrap(newAddressBookData);
      }
      else if (this.id) {
        this.$acl = new AddressBook.$$Acl('Contacts/' + this.id);
      }
    }
    else {
      // The promise will be unwrapped first
      this.$unwrap(futureAddressBookData);
    }
  }

  /**
   * @memberof AddressBook
   * @desc The factory we'll use to register with Angular
   * @returns the AddressBook constructor
   */
  AddressBook.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Resource', 'Card', 'Acl', 'Preferences', function($q, $timeout, $log, Settings, Resource, Card, Acl, Preferences) {
    angular.extend(AddressBook, {
      $q: $q,
      $timeout: $timeout,
      $log: $log,
      $$resource: new Resource(Settings.activeUser('folderURL') + 'Contacts', Settings.activeUser()),
      $Card: Card,
      $$Acl: Acl,
      $Preferences: Preferences,
      activeUser: Settings.activeUser(),
      selectedFolder: null,
      $refreshTimeout: null
    });

    return AddressBook; // return constructor
  }];

  /**
   * @module SOGo.ContactsUI
   * @desc Factory registration of AddressBook in Angular module.
   */
  try {
    angular.module('SOGo.ContactsUI');
  }
  catch(e) {
    angular.module('SOGo.ContactsUI', ['SOGo.Common']);
  }
  angular.module('SOGo.ContactsUI')
    .factory('AddressBook', AddressBook.$factory);

  /**
   * @memberof AddressBook
   * @desc Search for cards among all addressbooks matching some criterias.
   * @param {string} search - the search string to match
   * @param {object} [options] - additional options to the query (excludeGroups and excludeLists)
   * @param {object[]} excludedCards - a list of Card objects that must be excluded from the results
   * @returns a collection of Cards instances
   */
  AddressBook.$filterAll = function(search, options, excludedCards) {
    var params = {search: search};

    if (!search) {
      // No query specified
      AddressBook.$cards = [];
      return AddressBook.$q.when(AddressBook.$cards);
    }
    if (angular.isUndefined(AddressBook.$cards)) {
      // First session query
      AddressBook.$cards = [];
    }
    else if (AddressBook.$query == search) {
      // Query hasn't changed
      return AddressBook.$q.when(AddressBook.$cards);
    }
    AddressBook.$query = search;

    angular.extend(params, options);

    return AddressBook.$$resource.fetch(null, 'allContactSearch', params).then(function(response) {
      var results, card, index,
          compareIds = function(data) {
            return this.id == data.id;
          };
      if (excludedCards) {
        // Remove excluded cards from results
        results = _.filter(response.contacts, function(data) {
          return _.isUndefined(_.find(excludedCards, compareIds, data));
        });
      }
      else {
        results = response.contacts;
      }
      // Remove cards that no longer match the search query
      for (index = AddressBook.$cards.length - 1; index >= 0; index--) {
        card = AddressBook.$cards[index];
        if (_.isUndefined(_.find(results, compareIds, card))) {
          AddressBook.$cards.splice(index, 1);
        }
      }
      // Add new cards matching the search query
      _.each(results, function(data, index) {
        if (_.isUndefined(_.find(AddressBook.$cards, compareIds, data))) {
          var card = new AddressBook.$Card(data, search);
          AddressBook.$cards.splice(index, 0, card);
        }
      });
      return AddressBook.$cards;
    });
  };

  /**
   * @memberof AddressBook
   * @desc Add a new addressbook to the static list of addressbooks
   * @param {AddressBook} addressbook - an Addressbook object instance
   */
  AddressBook.$add = function(addressbook) {
    // Insert new addressbook at proper index
    var list, sibling, i;

    list = addressbook.isSubscription? this.$subscriptions : this.$addressbooks;
    sibling = _.find(list, function(o) {
      return (addressbook.id == 'personal' ||
              (o.id != 'personal' &&
               o.name.localeCompare(addressbook.name) === 1));
    });
    i = sibling ? _.indexOf(_.pluck(list, 'id'), sibling.id) : 1;
    list.splice(i, 0, addressbook);
  };

  /**
   * @memberof AddressBook
   * @desc Set or get the list of addressbooks. Will instantiate a new AddressBook object for each item.
   * @param {array} [data] - the metadata of the addressbooks
   * @returns the list of addressbooks
   */
  AddressBook.$findAll = function(data) {
    var _this = this;
    if (data) {
      this.$addressbooks = [];
      this.$subscriptions = [];
      this.$remotes = [];
      // Instanciate AddressBook objects
      angular.forEach(data, function(o, i) {
        var addressbook = new AddressBook(o);
        if (addressbook.isRemote)
          _this.$remotes.push(addressbook);
        else if (addressbook.isSubscription)
          _this.$subscriptions.push(addressbook);
        else
          _this.$addressbooks.push(addressbook);
      });
    }
    return this.$addressbooks;
  };

  /**
   * @memberOf AddressBook
   * @desc Fetch list of cards and return an AddressBook instance.
   * @param {string} addressbookId - the addressbook identifier
   * @returns an AddressBook object instance
   */
  AddressBook.$find = function(addressbookId) {
    var futureAddressBookData = AddressBook.$$resource.fetch(addressbookId, 'view');

    return new AddressBook(futureAddressBookData);
  };

  /**
   * @memberOf AddressBook
   * @desc Subscribe to another user's addressbook and add it to the list of addressbooks.
   * @param {string} uid - user id
   * @param {string} path - path of folder for specified user
   * @returns a promise of the HTTP query result
   */
  AddressBook.$subscribe = function(uid, path) {
    var _this = this;
    return AddressBook.$$resource.userResource(uid).fetch(path, 'subscribe').then(function(addressbookData) {
      var addressbook = new AddressBook(addressbookData);
      if (_.isUndefined(_.find(_this.$subscriptions, function(o) {
        return o.id == addressbookData.id;
      }))) {
        // Not already subscribed
        AddressBook.$add(addressbook);
      }
      return addressbook;
    });
  };

  /**
   * @function init
   * @memberof AddressBook.prototype
   * @desc Extend instance with new data and compute additional attributes.
   * @param {object} data - attributes of addressbook
   */
  AddressBook.prototype.init = function(data) {
    this.$cards = [];
    this.cards = [];
    angular.extend(this, data);
    // Add 'isOwned' and 'isSubscription' attributes based on active user (TODO: add it server-side?)
    this.isOwned = AddressBook.activeUser.isSuperUser || this.owner == AddressBook.activeUser.login;
    this.isSubscription = !this.isRemote && this.owner != AddressBook.activeUser.login;
    this.$query = {search: 'name_or_address', value: '', sort: 'c_cn', asc: 'true'};
  };

  /**
   * @function $id
   * @memberof AddressBook.prototype
   * @desc Resolve the addressbook id.
   * @returns a promise of the addressbook id
   */
  AddressBook.prototype.$id = function() {
    if (this.id) {
      // Object already unwrapped
      return AddressBook.$q.when(this.id);
    }
    else {
      // Wait until object is unwrapped
      return this.$futureAddressBookData.then(function(addressbook) {
        return addressbook.id;
      });
    }
  };

  /**
   * @function $selectedCount
   * @memberof AddressBook.prototype
   * @desc Return the number of cards selected by the user.
   * @returns the number of selected cards
   */
  AddressBook.prototype.$selectedCount = function() {
    var count;

    count = 0;
    if (this.cards) {
      count = (_.filter(this.cards, function(card) { return card.selected; })).length;
    }
    return count;
  };

  /**
   * @function $startRefreshTimeout
   * @memberof AddressBook.prototype
   * @desc Starts the refresh timeout for the current selected address book
   */
  AddressBook.prototype.$startRefreshTimeout = function() {
    var _this = this;

    if (AddressBook.$refreshTimeout)
      AddressBook.$timeout.cancel(AddressBook.$refreshTimeout);

    AddressBook.$Preferences.ready().then(function() {
      // Restart the refresh timer, if needed
      var refreshViewCheck = AddressBook.$Preferences.defaults.SOGoRefreshViewCheck;
      if (refreshViewCheck && refreshViewCheck != 'manually') {
        var f = angular.bind(_this, AddressBook.prototype.$reload);
        AddressBook.$refreshTimeout = AddressBook.$timeout(f, refreshViewCheck.timeInterval()*1000);
      }
    });
  };

  /**
   * @function $reload
   * @memberof AddressBook.prototype
   * @desc Reload list of cards
   * @returns a promise of the Cards instances
   */
  AddressBook.prototype.$reload = function() {
    var _this = this;

    this.$startRefreshTimeout();

    return AddressBook.$$resource.fetch(this.id, 'view')
      .then(function(response) {
        var index, card,
            results = response.cards,
            cards = _this.cards,
            compareIds = function(data) {
              return this.id == data.id;
            };

        // Remove cards that no longer exist
        for (index = cards.length - 1; index >= 0; index--) {
          card = cards[index];
          if (_.isUndefined(_.find(results, compareIds, card))) {
            cards.splice(index, 1);
          }
        }

        // Add new cards
        _.each(results, function(data, index) {
          if (_.isUndefined(_.find(cards, compareIds, data))) {
            var card = new AddressBook.$Card(data);
            cards.splice(index, 0, card);
          }
        });

        return cards;
      });
  };

    /**
   * @function $filter
   * @memberof AddressBook.prototype
   * @desc Search for cards matching some criterias
   * @param {string} search - the search string to match
   * @param {object} [options] - additional options to the query
   * @returns a collection of Cards instances
   */
  AddressBook.prototype.$filter = function(search, options, excludedCards) {
    var _this = this;

    if (options) {
      angular.extend(this.$query, options);

      if (options.dry) {
        if (!search) {
          // No query specified
          this.$cards = [];
          return AddressBook.$q.when(this.$cards);
        }
        else if (this.$query.value == search) {
          // Query hasn't changed
          return AddressBook.$q.when(this.$cards);
        }
      }
    }

    this.$query.value = search;

    return this.$id().then(function(addressbookId) {
      return AddressBook.$$resource.fetch(addressbookId, 'view', _this.$query);
    }).then(function(response) {
      var results, cards, card, index,
          compareIds = function(data) {
            return this.id == data.id;
          };
      if (options && options.dry) {
        // Don't keep a copy of the resulting cards.
        // This is usefull when doing autocompletion.
        cards = _this.$cards;
      }
      else {
        cards = _this.cards;
      }
      if (excludedCards) {
        // Remove excluded cards from results
        results = _.filter(response.cards, function(card) {
          return _.isUndefined(_.find(excludedCards, compareIds, card));
        });
      }
      else {
        results = response.cards;
      }
      // Remove cards that no longer match the search query
      for (index = cards.length - 1; index >= 0; index--) {
        card = cards[index];
        if (_.isUndefined(_.find(results, compareIds, card))) {
          cards.splice(index, 1);
        }
      }
      // Add new cards matching the search query
      _.each(results, function(data, index) {
        if (_.isUndefined(_.find(cards, compareIds, data))) {
          var card = new AddressBook.$Card(data, search);
          cards.splice(index, 0, card);
        }
      });
      // Respect the order of the results
      _.each(results, function(data, index) {
        var oldIndex, removedCards;
        if (cards[index].id != data.id) {
          oldIndex = _.findIndex(cards, compareIds, data);
          removedCards = cards.splice(oldIndex, 1);
          cards.splice(index, 0, removedCards[0]);
        }
      });
      return cards;
    });
  };

  /**
   * @function $rename
   * @memberof AddressBook.prototype
   * @desc Rename the addressbook and keep the list sorted
   * @param {string} name - the new name
   * @returns a promise of the HTTP operation
   */
  AddressBook.prototype.$rename = function(name) {
    var i = _.indexOf(_.pluck(AddressBook.$addressbooks, 'id'), this.id);
    this.name = name;
    AddressBook.$addressbooks.splice(i, 1);
    AddressBook.$add(this);
    return this.$save();
  };

  /**
   * @function $delete
   * @memberof AddressBook.prototype
   * @desc Delete the addressbook from the server and the static list of addressbooks.
   * @returns a promise of the HTTP operation
   */
  AddressBook.prototype.$delete = function() {
    var _this = this,
        d = AddressBook.$q.defer(),
        list,
        promise;

    if (this.isSubscription) {
      promise = AddressBook.$$resource.fetch(this.id, 'unsubscribe');
      list = AddressBook.$subscriptions;
    }
    else {
      promise = AddressBook.$$resource.remove(this.id);
      list = AddressBook.$addressbooks;
    }

    promise.then(function() {
      var i = _.indexOf(_.pluck(list, 'id'), _this.id);
      list.splice(i, 1);
      d.resolve();
    }, function(data, status) {
      d.reject(data);
    });
    return d.promise;
  };

  /**
   * @function $deleteCards
   * @memberof AddressBook.prototype
   * @desc Delete multiple cards from addressbook.
   * @return a promise of the HTTP operation
   */
  AddressBook.prototype.$deleteCards = function(cards) {

    var uids = _.map(cards, function(card) { return card.id; });
    var _this = this;
    
    return AddressBook.$$resource.post(this.id, 'batchDelete', {uids: uids}).then(function() {
      _this.cards = _.difference(_this.cards, cards);
    });
  };

  /**
   * @function $copyCards
   * @memberof AddressBook.prototype
   * @desc Copy multiple cards from addressbook to an other one.
   * @return a promise of the HTTP operation
   */
  AddressBook.prototype.$copyCards = function(cards, folder) {
    var uids = _.map(cards, function(card) { return card.id; });
    return AddressBook.$$resource.post(this.id, 'copy', {uids: uids, folder: folder});
  };

  /**
   * @function $save
   * @memberof AddressBook.prototype
   * @desc Save the addressbook to the server. This currently can only affect the name of the addressbook.
   * @returns a promise of the HTTP operation
   */
  AddressBook.prototype.$save = function() {
    return AddressBook.$$resource.save(this.id, this.$omit()).then(function(data) {
      return data;
    });
  };

  /**
   * @function $getCard
   * @memberof AddressBook.prototype
   * @desc Fetch the card attributes from the server.
   * @returns a promise of the HTTP operation
   */
  AddressBook.prototype.$getCard = function(cardId) {
    return this.$id().then(function(addressbookId) {
      return AddressBook.$Card.$find(addressbookId, cardId);
    });
  };

  /**
   * @function $unwrap
   * @memberof AddressBook.prototype
   * @desc Unwrap a promise and instanciate new Card objects using received data.
   * @param {promise} futureAddressBookData - a promise of the AddressBook's data
   */
  AddressBook.prototype.$unwrap = function(futureAddressBookData) {
    var _this = this;

    // Expose and sesolve the promise
    this.$futureAddressBookData = futureAddressBookData.then(function(data) {
      return AddressBook.$timeout(function() {
        // Extend AddressBook instance from data of addressbooks list.
        // Will inherit attributes such as isEditable and isRemote.
        angular.forEach(AddressBook.$findAll(), function(o, i) {
          if (o.id == data.id) {
            angular.extend(_this, o);
          }
        });
        // Extend AddressBook instance with received data
        _this.init(data);
        // Instanciate Card objects
        angular.forEach(_this.cards, function(o, i) {
          _this.cards[i] = new AddressBook.$Card(o);
        });
        // Instanciate Acl object
        _this.$acl = new AddressBook.$$Acl('Contacts/' + _this.id);

        _this.$startRefreshTimeout();

        return _this;
      });
    }, function(data) {
      _this.isError = true;
      if (angular.isObject(data)) {
        AddressBook.$timeout(function() {
          angular.extend(_this, data);
        });
      }
    });
  };

  /**
   * @function $omit
   * @memberof AddressBook.prototype
   * @desc Return a sanitized object used to send to the server.
   * @return an object literal copy of the Addressbook instance
   */
  AddressBook.prototype.$omit = function() {
    var addressbook = {};
    angular.forEach(this, function(value, key) {
      if (key != 'constructor' &&
          key != 'cards' &&
          key[0] != '$') {
        addressbook[key] = value;
      }
    });
    return addressbook;
  };
})();
