/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * 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/. */

const activityManager = Cc["@mozilla.org/activity-manager;1"].getService(
  Ci.nsIActivityManager
);

var ACTIVITY_LIMIT = 250;

var activityObject = {
  _activityMgrListener: null,
  _activitiesView: null,
  _activityLogger: console.createInstance({
    prefix: "mail.activity",
    maxLogLevel: "Warn",
    maxLogLevelPref: "mail.activity.loglevel",
  }),
  _ignoreNotifications: false,
  _groupCache: new Map(),

  // Utility Functions for Activity element management

  /**
   * Creates the proper element for the given activity
   */
  createActivityWidget(type) {
    let element = document.createElement("li", {
      is: type.bindingName,
    });

    if (element) {
      element.setAttribute("actID", type.id);
    }

    return element;
  },

  /**
   * Returns the activity group element that matches the context_type
   * and context of the given activity, if any.
   */
  getActivityGroupElementByContext(aContextType, aContextObj) {
    return this._groupCache.get(aContextType + ":" + aContextObj);
  },

  /**
   * Inserts the given element into the correct position on the
   * activity manager window.
   */
  placeActivityElement(element) {
    if (element.isGroup || element.isProcess) {
      this._activitiesView.insertBefore(
        element,
        this._activitiesView.firstElementChild
      );
    } else {
      let next = this._activitiesView.firstElementChild;
      while (next && (next.isWarning || next.isProcess || next.isGroup)) {
        next = next.nextElementSibling;
      }
      if (next) {
        this._activitiesView.insertBefore(element, next);
      } else {
        this._activitiesView.appendChild(element);
      }
    }
    if (element.isGroup) {
      this._groupCache.set(
        element.contextType + ":" + element.contextObj,
        element
      );
    }
    while (this._activitiesView.children.length > ACTIVITY_LIMIT) {
      this.removeActivityElement(
        this._activitiesView.lastElementChild.getAttribute("actID")
      );
    }
  },

  /**
   * Adds a new element to activity manager window for the
   * given activity. It is called by ActivityMgrListener when
   * a new activity is added into the activity manager's internal
   * list.
   */
  addActivityElement(aID, aActivity) {
    try {
      this._activityLogger.info(`Adding ActivityElement: ${aID}, ${aActivity}`);
      // get |groupingStyle| of the activity. Grouping style determines
      // whether we show the activity standalone or grouped by context in
      // the activity manager window.
      let isGroupByContext =
        aActivity.groupingStyle == Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT;

      // find out if an activity group has already been created for this context
      let group = null;
      if (isGroupByContext) {
        group = this.getActivityGroupElementByContext(
          aActivity.contextType,
          aActivity.contextObj
        );
        // create a group if it's not already created.
        if (!group) {
          group = document.createElement("li", {
            is: "activity-group-item",
          });
          this._activityLogger.info("created group element");
          // Set the context type and object of the newly created group
          group.contextType = aActivity.contextType;
          group.contextObj = aActivity.contextObj;
          group.contextDisplayText = aActivity.contextDisplayText;

          // add group into the list
          this.placeActivityElement(group);
        }
      }

      // create the appropriate element for the activity
      let actElement = this.createActivityWidget(aActivity);
      this._activityLogger.info("created activity element");

      if (group) {
        // get the inner list element of the group
        let groupView = group.querySelector(".activitygroup-list");
        groupView.appendChild(actElement);
      } else {
        this.placeActivityElement(actElement);
      }
    } catch (e) {
      this._activityLogger.error("addActivityElement: " + e);
      throw e;
    }
  },

  /**
   * Removes the activity element from the activity manager window.
   * It is called by ActivityMgrListener when the activity in question
   * is removed from the activity manager's internal list.
   */
  removeActivityElement(aID) {
    this._activityLogger.info("removing Activity ID: " + aID);
    let item = this._activitiesView.querySelector(`[actID="${aID}"]`);

    if (item) {
      let group = item.closest(".activitygroup");
      item.remove();
      if (group && !group.querySelector(".activityitem")) {
        // Empty group is removed.
        this._groupCache.delete(group.contextType + ":" + group.contextObj);
        group.remove();
      }
    }
  },

  // -----------------
  // Startup, Shutdown

  startup() {
    try {
      this._activitiesView = document.getElementById("activityView");

      let activities = activityManager.getActivities();
      for (
        let iActivity = Math.max(0, activities.length - ACTIVITY_LIMIT);
        iActivity < activities.length;
        iActivity++
      ) {
        let activity = activities[iActivity];
        this.addActivityElement(activity.id, activity);
      }

      // start listening changes in the activity manager's
      // internal list
      this._activityMgrListener = new this.ActivityMgrListener();
      activityManager.addListener(this._activityMgrListener);
    } catch (e) {
      this._activityLogger.error("Exception: " + e);
    }
  },

  rebuild() {
    let activities = activityManager.getActivities();
    for (let activity of activities) {
      this.addActivityElement(activity.id, activity);
    }
  },

  shutdown() {
    activityManager.removeListener(this._activityMgrListener);
  },

  // -----------------
  // Utility Functions

  /**
   * Remove all activities not in-progress from the activity list.
   */
  clearActivityList() {
    this._activityLogger.debug("clearActivityList");

    this._ignoreNotifications = true;
    // If/when we implement search, we'll want to remove just the items
    // that are on the search display, however for now, we'll just clear up
    // everything.
    activityManager.cleanUp();

    while (this._activitiesView.lastChild) {
      this._activitiesView.lastChild.remove();
    }

    this._groupCache.clear();
    this.rebuild();
    this._ignoreNotifications = false;
    this._activitiesView.focus();
  },
};

// An object to monitor nsActivityManager operations. This class acts as
// binding layer between nsActivityManager and nsActivityManagerUI objects.
activityObject.ActivityMgrListener = function () {};
activityObject.ActivityMgrListener.prototype = {
  onAddedActivity(aID, aActivity) {
    activityObject._activityLogger.info(`added activity: ${aID} ${aActivity}`);
    if (!activityObject._ignoreNotifications) {
      activityObject.addActivityElement(aID, aActivity);
    }
  },

  onRemovedActivity(aID) {
    if (!activityObject._ignoreNotifications) {
      activityObject.removeActivityElement(aID);
    }
  },
};

window.addEventListener("load", () => activityObject.startup());
window.addEventListener("unload", () => activityObject.shutdown());
