/* 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/. */

"use strict";

/* global MozXULElement, MozElements */

// Wrap in a block to prevent leaking to window scope.
{
  /**
   * The MozChatGroupRichlistitem widget displays chat group name and behave as a
   * expansion twisty for groups such as "Conversations",
   * "Online Contacts" and "Offline Contacts".
   *
   * @augments {MozElements.MozRichlistitem}
   */
  class MozChatGroupRichlistitem extends MozElements.MozRichlistitem {
    static get inheritedAttributes() {
      return {
        label: "value=name",
      };
    }
    connectedCallback() {
      if (this.delayConnectedCallback() || this.hasChildNodes()) {
        return;
      }

      this.setAttribute("is", "chat-group-richlistitem");
      this.setAttribute("collapsed", "true");

      /* Here we use a div, rather than the usual img because the icon image
       * relies on CSS -moz-locale-dir(rtl). The corresponding icon
       * twisty-collapsed-rtl icon is not a simple mirror transformation of
       * twisty-collapsed.
       * Currently, CSS sets the background-image based on the "closed" state.
       * The element is a visual decoration and does not require any alt text
       * since the aria-expanded attribute describes its state.
       */
      this._image = document.createElement("div");
      this._image.classList.add("twisty");

      this._label = document.createXULElement("label");
      this._label.setAttribute("flex", "1");
      this._label.setAttribute("crop", "end");

      this.appendChild(this._image);
      this.appendChild(this._label);

      this.contacts = [];

      this.contactsById = {};

      this.displayName = "";

      this.addEventListener("click", event => {
        // Check if there was 1 click on the image or 2 clicks on the label
        if (
          (event.detail == 1 && event.target.classList.contains("twisty")) ||
          (event.detail == 2 && event.target.localName == "label")
        ) {
          this.toggleClosed();
        } else if (event.target.localName == "button") {
          this.hide();
        }
      });

      this.addEventListener("contextmenu", event => {
        event.preventDefault();
      });

      if (this.classList.contains("closed")) {
        this.setAttribute("aria-expanded", "true");
      } else {
        this.setAttribute("aria-expanded", "false");
      }

      this.initializeAttributeInheritance();
    }

    /**
     * Takes as input two contact elements (imIContact type) and compares
     * their nicknames alphabetically (case insensitive). This method
     * behaves as a callback that Array.prototype.sort accepts as a
     * parameter.
     */
    sortComparator(contactA, contactB) {
      if (contactA.statusType != contactB.statusType) {
        return contactB.statusType - contactA.statusType;
      }
      let a = contactA.displayName.toLowerCase();
      let b = contactB.displayName.toLowerCase();
      return a.localeCompare(b);
    }

    addContact(contact, tagName) {
      if (this.contactsById.hasOwnProperty(contact.id)) {
        return null;
      }

      let contactElt;
      if (tagName) {
        contactElt = document.createXULElement("richlistitem", {
          is: "chat-imconv-richlistitem",
        });
      } else {
        contactElt = document.createXULElement("richlistitem", {
          is: "chat-contact-richlistitem",
        });
      }
      if (this.classList.contains("closed")) {
        contactElt.setAttribute("collapsed", "true");
      }

      let end = this.contacts.length;
      // Avoid the binary search loop if the contacts were already sorted.
      if (
        end != 0 &&
        this.sortComparator(contact, this.contacts[end - 1].contact) < 0
      ) {
        let start = 0;
        while (start < end) {
          let middle = start + Math.floor((end - start) / 2);
          if (this.sortComparator(contact, this.contacts[middle].contact) < 0) {
            end = middle;
          } else {
            start = middle + 1;
          }
        }
      }
      let last = end == 0 ? this : this.contacts[end - 1];
      this.parentNode.insertBefore(contactElt, last.nextElementSibling);
      contactElt.build(contact);
      contactElt.group = this;
      this.contacts.splice(end, 0, contactElt);
      this.contactsById[contact.id] = contactElt;
      this.removeAttribute("collapsed");
      this._updateGroupLabel();
      return contactElt;
    }

    updateContactPosition(subject, tagName) {
      let contactElt = this.contactsById[subject.id];
      let index = this.contacts.indexOf(contactElt);
      if (index == -1) {
        // Sometimes we get a display-name-changed notification for
        // an offline contact, if it's not in the list, just ignore it.
        return;
      }
      // See if the position of the contact should be changed.
      if (
        (index != 0 &&
          this.sortComparator(
            contactElt.contact,
            this.contacts[index - 1].contact
          ) < 0) ||
        (index != this.contacts.length - 1 &&
          this.sortComparator(
            contactElt.contact,
            this.contacts[index + 1].contact
          ) > 0)
      ) {
        let list = this.parentNode;
        let selectedItem = list.selectedItem;
        let oldItem = this.removeContact(subject);
        let newItem = this.addContact(subject, tagName);
        if (selectedItem == oldItem) {
          list.selectedItem = newItem;
        }
      }
    }

    removeContact(contactForID) {
      let contact = this.contactsById[contactForID.id];
      if (!contact) {
        throw new Error("Can't remove contact for id=" + contactForID.id);
      }

      // create a new array to remove without breaking for each loops.
      this.contacts = this.contacts.filter(c => c !== contact);
      delete this.contactsById[contact.contact.id];

      contact.destroy();

      // Check if some contacts remain in the group, if empty hide it.
      if (!this.contacts.length) {
        this.setAttribute("collapsed", "true");
      } else {
        this._updateGroupLabel();
      }

      return contact;
    }

    _updateClosedState(closed) {
      for (let contact of this.contacts) {
        contact.collapsed = closed;
      }
    }

    toggleClosed() {
      if (this.classList.contains("closed")) {
        this.classList.remove("closed");
        this.setAttribute("aria-expanded", "true");
        this._updateClosedState(false);
      } else {
        this.classList.add("closed");
        this.setAttribute("aria-expanded", "false");
        this._updateClosedState(true);
      }

      this._updateGroupLabel();
    }

    _updateGroupLabel() {
      if (!this.displayName) {
        this.displayName = this.getAttribute("name");
      }
      let name = this.displayName;
      if (this.classList.contains("closed")) {
        name += " (" + this.contacts.length + ")";
      }

      this.setAttribute("name", name);
    }

    keyPress(event) {
      switch (event.keyCode) {
        case event.DOM_VK_RETURN:
          this.toggleClosed();
          break;

        case event.DOM_VK_LEFT:
          if (!this.classList.contains("closed")) {
            this.toggleClosed();
          }
          break;

        case event.DOM_VK_RIGHT:
          if (this.classList.contains("closed")) {
            this.toggleClosed();
          }
          break;
      }
    }
  }

  MozXULElement.implementCustomInterface(MozChatGroupRichlistitem, [
    Ci.nsIDOMXULSelectControlItemElement,
  ]);

  customElements.define("chat-group-richlistitem", MozChatGroupRichlistitem, {
    extends: "richlistitem",
  });
}
