<?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 bindings>

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

  <binding id="tooltip" extends="chrome://global/content/bindings/popup.xml#tooltip">
    <resources>
      <stylesheet src="chrome://chat/content/imtooltip.css"/>
      <stylesheet src="chrome://chat/skin/imtooltip.css"/>
    </resources>
    <content noautohide="true" orient="vertical">
      <children>
        <xul:label class="tooltip-label" xbl:inherits="xbl:text=label" flex="1"/>
      </children>
      <xul:vbox id="largeTooltip">
        <xul:hbox align="start" crop="end" flex="1">
          <xul:vbox flex="1">
            <xul:stack>
              <!-- The box around the user icon is a workaround for bug 955673. -->
              <xul:box class="userIconHolder" xbl:inherits="userIcon">
                <xul:image class="userIcon" anonid="userIcon"
                           xbl:inherits="src=userIcon"/>
              </xul:box>
              <xul:image class="statusTypeIcon" xbl:inherits="status,left"/>
            </xul:stack>
            <xul:spacer flex="1"/>
          </xul:vbox>
          <xul:stack class="displayNameMessageBox" flex="1">
            <xul:vbox flex="1">
              <xul:hbox align="start" flex="1">
                <xul:description class="tooltipDisplayName" flex="1" crop="end"
                                 xbl:inherits="value=displayname"/>
                <xul:image class="tooltipProtoIcon"
                           xbl:inherits="src=iconPrpl,status"/>
              </xul:hbox>
              <xul:spacer flex="1"/>
            </xul:vbox>
            <xul:description class="tooltipMessage" anonid="tooltipMessage"
                             xbl:inherits="noTopic"/>
          </xul:stack>
        </xul:hbox>
        <xul:grid>
          <xul:columns>
            <xul:column/>
            <xul:column flex="1"/>
          </xul:columns>
          <xul:rows class="tooltipRows" anonid="tooltiprows"/>
        </xul:grid>
        <xul:vbox class="tooltipBuddies" anonid="buddies"/>
      </xul:vbox>
    </content>
    <implementation implements="nsIObserver, nsIDOMEventListener">
     <property name="bundle">
       <getter>
         <![CDATA[
          if (!this._bundle) {
            this._bundle =
              Services.strings.createBundle("chrome://chat/locale/imtooltip.properties");
          }
          return this._bundle;
         ]]>
       </getter>
     </property>

     <field name="_buddy">null</field>
     <property name="buddy" onget="return this._buddy;">
       <setter>
         <![CDATA[
           if (val == this._buddy)
             return val;

           if (!val)
             this._buddy.buddy.removeObserver(this);
           else
             val.buddy.addObserver(this);

           return (this._buddy = val);
         ]]>
       </setter>
     </property>

     <field name="_contact">null</field>
     <property name="contact" onget="return this._contact;">
       <setter>
         <![CDATA[
           if (val == this._contact)
             return val;

           if (!val)
             this._contact.removeObserver(this);
           else
             val.addObserver(this);

           return (this._contact = val);
         ]]>
       </setter>
     </property>

     <property name="rows">
       <getter>
         <![CDATA[
           if (!("_rows" in this)) {
             this._rows =
               document.getAnonymousElementByAttribute(this, "anonid",
                                                       "tooltiprows");
           }
           return this._rows;
         ]]>
       </getter>
     </property>

     <method name="setBuddyIcon">
       <parameter name="aSrc"/>
       <body>
       <![CDATA[
         var img = document.getAnonymousElementByAttribute(this, "anonid",
                                                           "userIcon");
         if (aSrc)
           this.setAttribute("userIcon", aSrc);
         else
           this.removeAttribute("userIcon");
       ]]>
       </body>
     </method>

     <method name="setMessage">
       <parameter name="aMessage"/>
       <body>
       <![CDATA[
         // Setting the textContent directly allows text wrapping.
         var msg = document.getAnonymousElementByAttribute(this, "anonid",
                                                           "tooltipMessage");
         msg.textContent = aMessage;
       ]]>
       </body>
     </method>

     <method name="reset">
       <body>
       <![CDATA[
         while (this.rows.hasChildNodes())
           this.rows.lastChild.remove();
       ]]>
       </body>
     </method>

     <method name="addRow">
       <parameter name="aLabel"/>
       <parameter name="aValue"/>
       <body>
       <![CDATA[
         let description;
         let row = [...this.rows.querySelectorAll("row")].find(row => {
           return row.querySelector("label").getAttribute("value") == aLabel;
         });
         if (!row) {
           // Create a new row for this label.
           const XULNS =
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
           row = document.createElementNS(XULNS, "row");
           let label = document.createElementNS(XULNS, "label");
           label.className = "header";
           label.setAttribute("value", aLabel);
           row.appendChild(label);
           description = document.createElementNS(XULNS, "description");
           row.appendChild(description);
           row.setAttribute("align", "baseline");
           this.rows.appendChild(row);
         }
         else {
           // Row with this label already exists - just update.
           description = row.querySelector("description");
         }
         description.textContent = aValue;
       ]]>
       </body>
     </method>

     <method name="addSeparator">
       <body>
       <![CDATA[
         const XULNS =
           "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

         var row = document.createElementNS(XULNS, "row");
         var separator = document.createElementNS(XULNS, "separator");
         separator.className = "thin";
         row.appendChild(separator);
         this.rows.appendChild(row);
       ]]>
       </body>
     </method>

     <method name="requestBuddyInfo">
       <parameter name="aAccount"/>
       <parameter name="aObservedName"/>
       <body>
       <![CDATA[
         // Libpurple prpls don't necessarily return data in response to
         // requestBuddyInfo that is suitable for displaying inside a
         // tooltip (e.g. too many objects, or <img> and <a> tags),
         // so we only use it for JS prpls.
         // This is a terrible, terrible hack to work around the fact that
         // ClassInfo.implementationLanguage has gone.
         if (!aAccount.prplAccount.wrappedJSObject)
           return;
         this.observedUserInfo = aObservedName;
         Services.obs.addObserver(this, "user-info-received", false);
         aAccount.requestBuddyInfo(aObservedName);
       ]]>
       </body>
     </method>

     <method name="updateTooltipFromBuddy">
       <parameter name="aBuddy"/>
       <body>
       <![CDATA[
         this.buddy = aBuddy;

         this.reset();
         let name = aBuddy.userName;
         let displayName = aBuddy.displayName;
         this.setAttribute("displayname", displayName);
         let account = aBuddy.account;
         this.setAttribute("iconPrpl", account.protocol.iconBaseURI + "icon.png");
         this.setBuddyIcon(aBuddy.buddyIconFilename);

         let statusType = aBuddy.statusType;
         this.setAttribute("status", Status.toAttribute(statusType));
         this.setMessage(Status.toLabel(statusType, aBuddy.statusText));

         if (displayName != name)
           this.addRow(this.bundle.GetStringFromName("buddy.username"), name);

         this.addRow(this.bundle.GetStringFromName("buddy.account"), account.name);

         this.requestBuddyInfo(account, aBuddy.normalizedName);

         var tooltipInfo = aBuddy.getTooltipInfo();
         if (tooltipInfo)
           this.updateTooltipInfo(tooltipInfo);
         return true;
       ]]>
       </body>
     </method>

     <method name="updateTooltipInfo">
       <parameter name="aTooltipInfo"/>
       <body>
       <![CDATA[
         while (aTooltipInfo.hasMoreElements()) {
           var elt =
             aTooltipInfo.getNext().QueryInterface(Ci.prplITooltipInfo);
           switch (elt.type) {
             case Ci.prplITooltipInfo.pair:
             case Ci.prplITooltipInfo.sectionHeader:
               this.addRow(elt.label, elt.value);
               break;
             case Ci.prplITooltipInfo.sectionBreak:
               this.addSeparator();
               break;
             case Ci.prplITooltipInfo.status:
               let statusType = parseInt(elt.label);
               this.setAttribute("status", Status.toAttribute(statusType));
               this.setMessage(Status.toLabel(statusType, elt.value));
               break;
             case Ci.prplITooltipInfo.icon:
               this.setBuddyIcon(elt.value);
               break;
           }
         }
       ]]>
       </body>
     </method>

     <method name="updateTooltipFromContact">
       <parameter name="aContact"/>
       <body>
       <![CDATA[
         this.contact = aContact;

         this.updateTooltipFromBuddy(aContact.preferredBuddy.preferredAccountBuddy);

         this.addRow(this.bundle.GetStringFromName("contact.tags"),
                     aContact.getTags().map(t => t.name).join(", "));

         let buddies = this.contact.getBuddies();
         if (buddies.length <= 1)
           return true;

         let buddiesElt = document.getAnonymousElementByAttribute(this, "anonid",
                                                                  "buddies");
         let sep = document.createElement("separator");
         sep.setAttribute("class", "groove tooltipSeparator");
         buddiesElt.appendChild(sep);

         for (let buddy of buddies) {
           let buddyElt = document.createElement("buddy");
           buddiesElt.appendChild(buddyElt);
           buddyElt.getBoundingClientRect(); // force binding attachment
           buddyElt.build(buddy, this);
         }

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

     <method name="updateTooltipFromConversation">
       <parameter name="aConv"/>
       <body>
       <![CDATA[
         if (!aConv.isChat && aConv.buddy)
           return this.updateTooltipFromBuddy(aConv.buddy);

         this.reset();
         this.setAttribute("displayname", aConv.name);
         let account = aConv.account;
         this.setAttribute("iconPrpl", account.protocol.iconBaseURI + "icon.png");
         this.setBuddyIcon(null);
         if (aConv.isChat) {
           this.setAttribute("status", "chat");
           if (!aConv.account.connected || aConv.left)
             this.setAttribute("left", true);
           let topic = aConv.topic;
           if (!topic) {
             this.setAttribute("noTopic", true);
             topic = aConv.noTopicString;
           }
           this.setMessage(topic);
         }
         else {
           this.setAttribute("status", "unknown");
           this.setMessage(Status.toLabel("unknown"));
           // Last ditch attempt to get some tooltip info. This call relies on
           // the account's requestBuddyInfo implementation working correctly
           // with aConv.normalizedName.
           this.requestBuddyInfo(account, aConv.normalizedName);
         }
         this.addRow(this.bundle.GetStringFromName("buddy.account"), account.name);
         return true;
       ]]>
       </body>
     </method>

     <method name="updateTooltipFromParticipant">
       <parameter name="aNick"/>
       <parameter name="aConv"/>
       <parameter name="aParticipant"/>
       <body>
       <![CDATA[
         if (!aConv.target)
           return false;     // We're viewing a log.
         if (!aParticipant)
           aParticipant = aConv.target.getParticipant(aNick);

         let account = aConv.account;
         let normalizedNick = aConv.target.getNormalizedChatBuddyName(aNick);
         // To try to ensure that we aren't misidentifying a nick with a
         // contact, we require at least that the normalizedChatBuddyName of
         // the nick is normalized like a normalizedName for contacts.
         if (normalizedNick == account.normalize(normalizedNick)) {
           let accountBuddy =
             Services.contacts.getAccountBuddyByNameAndAccount(normalizedNick,
                                                               account);
           if (accountBuddy)
             return this.updateTooltipFromBuddy(accountBuddy);
         }

         this.reset();
         this.setAttribute("displayname", aNick);
         this.setAttribute("iconPrpl",
                           account.protocol.iconBaseURI + "icon.png");
         this.setAttribute("status", "unknown");
         this.setMessage(Status.toLabel("unknown"));
         this.setBuddyIcon(aParticipant ? aParticipant.buddyIconFilename : null);
         this.requestBuddyInfo(account, normalizedNick);
         return true;
       ]]>
       </body>
     </method>

     <!-- nsIObserver implementation -->
     <method name="observe">
       <parameter name="aSubject"/>
       <parameter name="aTopic"/>
       <parameter name="aData"/>
       <body>
       <![CDATA[
         if (aSubject == this.buddy &&
             (aTopic == "account-buddy-status-changed" ||
              aTopic == "account-buddy-status-detail-changed" ||
              aTopic == "account-buddy-display-name-changed" ||
              aTopic == "account-buddy-icon-changed"))
           this.updateTooltipFromBuddy(this.buddy);
         else if (aTopic == "contact-preferred-buddy-changed" &&
                  aSubject.id == this.contact.id) {
           let buddy = this.contact.preferredBuddy;
           this.updateTooltipFromBuddy(buddy.preferredAccountBuddy);
         }
         else if (aTopic == "user-info-received" &&
                  aData == this.observedUserInfo) {
           this.updateTooltipInfo(aSubject.QueryInterface(Ci.nsISimpleEnumerator));
         }
       ]]>
       </body>
     </method>
    </implementation>
    <handlers>
     <handler event="popupshowing">
       <![CDATA[
         // No tooltip above the context menu.
         if (document.popupNode)
           return false;

         let elt = document.tooltipNode;
         // No tooltip for elements that have already been removed.
         if (!elt.parentNode)
           return false;

         // Reset tooltip.
         let largeTooltip = document.getElementById("largeTooltip");
         largeTooltip.hidden = false;
         this.removeAttribute("label");

         let localName = elt.localName;

#ifdef MOZ_THUNDERBIRD
         if (localName == "imconv" && elt.conv)
           return updateTooltipFromConversation(elt.conv);

         if (localName == "imcontact")
           return updateTooltipFromBuddy(elt.contact.preferredBuddy.preferredAccountBuddy);

         if (localName == "listitem") {
           let contactlistbox = document.getElementById("contactlistbox");
           let conv = contactlistbox.selectedItem.conv;
           return updateTooltipFromParticipant(elt.chatBuddy.name, conv,
                                               elt.chatBuddy);
         }
#else
         if (localName == "tab") {
           if (!elt.linkedConversation || !elt.linkedConversation.conv)
             return false;
           return updateTooltipFromConversation(elt.linkedConversation.conv);
         }

         if (localName == "conv")
           return updateTooltipFromConversation(elt.conv);

         if (localName == "buddy")
           return updateTooltipFromBuddy(elt.buddy.preferredAccountBuddy);

         if (localName == "contact")
           return updateTooltipFromContact(elt.contact);

         if (localName == "listitem") {
           let conv = document.getBindingParent(elt).conv;
           return updateTooltipFromParticipant(elt.chatBuddy.name, conv,
                                               elt.chatBuddy);
         }
#endif

         let classList = elt.classList;
         if (classList.contains("ib-nick") ||
             classList.contains("ib-sender") ||
             classList.contains("ib-person")) {
           let conv = getBrowser()._conv;
           if (conv.isChat)
             return updateTooltipFromParticipant(elt.textContent, conv);
           else if (elt.textContent == conv.name)
             return updateTooltipFromConversation(conv);
         }

         // Are we over a message?
         for (let node = elt; node; node = node.parentNode) {
           if (!node._originalMsg)
             continue;
           // It's a message, so add a date/time tooltip.
           let date = new Date(node._originalMsg.time * 1000);
           let text;
           if ((new Date()).toDateString() == date.toDateString()) {
             text = date.toLocaleTimeString();
           }
           else {
             let sdf =
               Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
                         .getService(Ci.nsIScriptableDateFormat);
             text = sdf.FormatDateTime("", sdf.dateFormatShort,
               sdf.timeFormatSeconds, date.getFullYear(),
               date.getMonth() + 1, date.getDate(),
               date.getHours(), date.getMinutes(), date.getSeconds());
           }
           // Setting the attribute on this node means that if the element
           // we are pointing at carries a title set by the prpl,
           // that title won't be overridden.
           node.setAttribute("title", text);
           break;
         }

         // Use the default content tooltip.
         largeTooltip.hidden = true;
         return fillInPageTooltip(elt);
       ]]>
     </handler>
     <handler event="popuphiding">
       <![CDATA[
       this.buddy = null;
       this.contact = null;
       this.removeAttribute("noTopic");
       this.removeAttribute("left");
       if ("observedUserInfo" in this && this.observedUserInfo) {
         Services.obs.removeObserver(this, "user-info-received");
         delete this.observedUserInfo;
       }

       let buddies = document.getAnonymousElementByAttribute(this, "anonid",
                                                             "buddies");
       while (buddies.hasChildNodes())
         buddies.lastChild.remove();

       // Since Mozilla 22, tooltips don't seem to shrink automatically after
       // being shown once at a large size. Not sure if it's temporary or not.
       this.removeAttribute("width");
       this.removeAttribute("height");
       ]]>
     </handler>
    </handlers>
  </binding>
</bindings>
