/* 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, activityManager */

// Wrap in a block to prevent leaking to window scope.
{
  const { makeFriendlyDateAgo } = ChromeUtils.import(
    "resource:///modules/TemplateUtils.jsm"
  );

  let activityStrings = Services.strings.createBundle(
    "chrome://messenger/locale/activity.properties"
  );

  /**
   * The ActivityItemBase widget is the base class for all the activity item.
   * It initializes activity details: i.e. id, status, icon, name, progress,
   * date etc. for the activity widgets.
   *
   * @abstract
   * @augments HTMLLIElement
   */
  class ActivityItemBase extends HTMLLIElement {
    connectedCallback() {
      if (!this.hasChildNodes()) {
        // fetch the activity and set the base attributes
        this.log = console.createInstance({
          prefix: "mail.activity",
          maxLogLevel: "Warn",
          maxLogLevelPref: "mail.activity.loglevel",
        });
        let actID = this.getAttribute("actID");
        this._activity = activityManager.getActivity(actID);
        this._activity.QueryInterface(this.constructor.activityInterface);

        // Construct the children.
        this.classList.add("activityitem");

        let icon = document.createElement("img");
        icon.setAttribute(
          "src",
          this._activity.iconClass
            ? `chrome://messenger/skin/icons/new/activity/${this._activity.iconClass}Icon.svg`
            : this.constructor.defaultIconSrc
        );
        icon.setAttribute("alt", "");
        this.appendChild(icon);

        let display = document.createElement("span");
        display.classList.add("displayText");
        this.appendChild(display);

        if (this.isEvent || this.isWarning) {
          let time = document.createElement("time");
          time.classList.add("dateTime");
          this.appendChild(time);
        }

        if (this.isProcess) {
          let progress = document.createElement("progress");
          progress.setAttribute("value", "0");
          progress.setAttribute("max", "100");
          progress.classList.add("progressmeter");
          this.appendChild(progress);
        }

        let statusText = document.createElement("span");
        statusText.setAttribute("role", "note");
        statusText.classList.add("statusText");
        this.appendChild(statusText);
      }
      // (Re-)Attach the listener.
      this.attachToActivity();
    }

    disconnectedCallback() {
      this.detachFromActivity();
    }

    get isProcess() {
      return this.constructor.activityInterface == Ci.nsIActivityProcess;
    }

    get isEvent() {
      return this.constructor.activityInterface == Ci.nsIActivityEvent;
    }

    get isWarning() {
      return this.constructor.activityInterface == Ci.nsIActivityWarning;
    }

    get isGroup() {
      return false;
    }

    get activity() {
      return this._activity;
    }

    detachFromActivity() {
      if (this.activityListener) {
        this._activity.removeListener(this.activityListener);
      }
    }

    attachToActivity() {
      if (this.activityListener) {
        this._activity.addListener(this.activityListener);
      }
    }

    static _dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
      dateStyle: "long",
      timeStyle: "short",
    });

    /**
     * The time the activity occurred.
     *
     * @type {number} - The time in milliseconds since the epoch.
     */
    set dateTime(time) {
      let element = this.querySelector(".dateTime");
      if (!element) {
        return;
      }
      time = new Date(parseInt(time));

      element.setAttribute("datetime", time.toISOString());
      element.textContent = makeFriendlyDateAgo(time);
      element.setAttribute(
        "title",
        this.constructor._dateTimeFormatter.format(time)
      );
    }

    /**
     * The text that describes additional information to the user.
     *
     * @type {string}
     */
    set statusText(val) {
      this.querySelector(".statusText").textContent = val;
    }

    get statusText() {
      return this.querySelector(".statusText").textContent;
    }

    /**
     * The text that describes the activity to the user.
     *
     * @type {string}
     */
    set displayText(val) {
      this.querySelector(".displayText").textContent = val;
    }

    get displayText() {
      return this.querySelector(".displayText").textContent;
    }
  }

  /**
   * The MozActivityEvent widget displays information about events (like
   * deleting or moving the message): e.g image, name, date and description.
   * It is typically used in Activity Manager window.
   *
   * @augments ActivityItemBase
   */
  class ActivityEventItem extends ActivityItemBase {
    static defaultIconSrc =
      "chrome://messenger/skin/icons/new/activity/defaultEventIcon.svg";
    static activityInterface = Ci.nsIActivityEvent;

    connectedCallback() {
      super.connectedCallback();
      this.setAttribute("is", "activity-event-item");

      this.displayText = this.activity.displayText;
      this.statusText = this.activity.statusText;
      this.dateTime = this.activity.completionTime;
    }
  }

  customElements.define("activity-event-item", ActivityEventItem, {
    extends: "li",
  });

  /**
   * The ActivityGroupItem widget displays information about the activities of
   * the group: e.g. name of the group, list of the activities with their name,
   * progress and icon. It is shown in Activity Manager window. It gets removed
   * when there is no activities from the group.
   *
   * @augments HTMLLIElement
   */
  class ActivityGroupItem extends HTMLLIElement {
    constructor() {
      super();

      let heading = document.createElement("h2");
      heading.classList.add("contextDisplayText");
      this.appendChild(heading);

      let list = document.createElement("ul");
      list.classList.add("activitygroup-list", "activityview");
      this.appendChild(list);

      this.classList.add("activitygroup");
      this.setAttribute("is", "activity-group-item");
    }

    /**
     * The text heading for the group, as seen by the user.
     *
     * @type {string}
     */
    set contextDisplayText(val) {
      this.querySelector(".contextDisplayText").textContent = val;
    }

    get contextDisplayText() {
      return this.querySelctor(".contextDisplayText").textContent;
    }

    get isGroup() {
      return true;
    }
  }

  customElements.define("activity-group-item", ActivityGroupItem, {
    extends: "li",
  });

  /**
   * The ActivityProcessItem widget displays information about the internal
   * process : e.g image, progress, name, date and description.
   * It is typically used in Activity Manager window.
   *
   * @augments ActivityItemBase
   */
  class ActivityProcessItem extends ActivityItemBase {
    static defaultIconSrc =
      "chrome://messenger/skin/icons/new/activity/deafultProcessIcon.svg";
    static activityInterface = Ci.nsIActivityProcess;
    static textMap = {
      paused: activityStrings.GetStringFromName("paused2"),
      canceled: activityStrings.GetStringFromName("canceled"),
      failed: activityStrings.GetStringFromName("failed"),
      waitingforinput: activityStrings.GetStringFromName("waitingForInput"),
      waitingforretry: activityStrings.GetStringFromName("waitingForRetry"),
    };

    constructor() {
      super();

      this.activityListener = {
        onStateChanged: (activity, oldState) => {
          // change the view of the element according to the new state
          // default states for each item
          let hideProgressMeter = false;
          let statusText = this.statusText;

          switch (this.activity.state) {
            case Ci.nsIActivityProcess.STATE_INPROGRESS:
              statusText = "";
              break;
            case Ci.nsIActivityProcess.STATE_COMPLETED:
              hideProgressMeter = true;
              statusText = "";
              break;
            case Ci.nsIActivityProcess.STATE_CANCELED:
              hideProgressMeter = true;
              statusText = this.constructor.textMap.canceled;
              break;
            case Ci.nsIActivityProcess.STATE_PAUSED:
              statusText = this.constructor.textMap.paused;
              break;
            case Ci.nsIActivityProcess.STATE_WAITINGFORINPUT:
              statusText = this.constructor.textMap.waitingforinput;
              break;
            case Ci.nsIActivityProcess.STATE_WAITINGFORRETRY:
              hideProgressMeter = true;
              statusText = this.constructor.textMap.waitingforretry;
              break;
          }

          // Set the visibility
          let meter = this.querySelector(".progressmeter");
          meter.hidden = hideProgressMeter;

          // Ensure progress meter not active when hidden
          if (hideProgressMeter) {
            meter.value = 0;
          }

          // Update Status text and Display Text Areas
          // In some states we need to modify Display Text area of
          // the process (e.g. Failure).
          this.statusText = statusText;
        },
        onProgressChanged: (
          activity,
          statusText,
          workUnitsComplete,
          totalWorkUnits
        ) => {
          let element = document.querySelector(".progressmeter");
          if (totalWorkUnits == 0) {
            element.removeAttribute("value");
          } else {
            let _percentComplete = (100.0 * workUnitsComplete) / totalWorkUnits;
            element.value = _percentComplete;
          }
          this.statusText = statusText;
        },
      };
    }

    connectedCallback() {
      super.connectedCallback();
      this.setAttribute("is", "activity-process-item");

      this.displayText = this.activity.displayText;
      // make sure that custom element reflects the latest state of the process
      this.activityListener.onStateChanged(
        this.activity.state,
        Ci.nsIActivityProcess.STATE_NOTSTARTED
      );
      this.activityListener.onProgressChanged(
        this.activity,
        this.activity.lastStatusText,
        this.activity.workUnitComplete,
        this.activity.totalWorkUnits
      );
    }

    get inProgress() {
      return this.activity.state == Ci.nsIActivityProcess.STATE_INPROGRESS;
    }

    get isRemovable() {
      return (
        this.activity.state == Ci.nsIActivityProcess.STATE_COMPLETED ||
        this.activity.state == Ci.nsIActivityProcess.STATE_CANCELED
      );
    }
  }

  customElements.define("activity-process-item", ActivityProcessItem, {
    extends: "li",
  });

  /**
   * The ActivityWarningItem widget displays information about
   * warnings : e.g image, name, date and description.
   * It is typically used in Activity Manager window.
   *
   * @augments ActivityItemBase
   */
  class ActivityWarningItem extends ActivityItemBase {
    static defaultIconSrc =
      "chrome://messenger/skin/icons/new/activity/warning.svg";
    static activityInterface = Ci.nsIActivityWarning;

    connectedCallback() {
      super.connectedCallback();
      this.setAttribute("is", "activity-warning-item");

      this.displayText = this.activity.displayText;
      this.dateTime = this.activity.time;
      this.statusText = this.activity.recoveryTipText;
    }
  }

  customElements.define("activity-warning-item", ActivityWarningItem, {
    extends: "li",
  });
}
