/* 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 ObservableObject = require("devtools/shared/observable-object");
const promise = require("devtools/toolkit/deprecated-sync-thenables");
const {Connection} = require("devtools/client/connection-manager");

const {Cu} = require("chrome");
const dbgClient = Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
const _knownWebappsStores = new WeakMap();

let WebappsStore;

module.exports = WebappsStore = function(connection) {
  // If we already know about this connection,
  // let's re-use the existing store.
  if (_knownWebappsStores.has(connection)) {
    return _knownWebappsStores.get(connection);
  }

  _knownWebappsStores.set(connection, this);

  ObservableObject.call(this, {});

  this._resetStore();

  this.destroy = this.destroy.bind(this);
  this._onStatusChanged = this._onStatusChanged.bind(this);

  this._connection = connection;
  this._connection.once(Connection.Events.DESTROYED, this.destroy);
  this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
  this._onStatusChanged();
  return this;
}

WebappsStore.prototype = {
  destroy: function() {
    if (this._connection) {
      // While this.destroy is bound using .once() above, that event may not
      // have occurred when the WebappsStore client calls destroy, so we
      // manually remove it here.
      this._connection.off(Connection.Events.DESTROYED, this.destroy);
      this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
      _knownWebappsStores.delete(this._connection);
      this._connection = null;
    }
  },

  _resetStore: function() {
    this.object.all = []; // list of app objects
    this.object.running = []; // list of manifests
  },

  _getAppFromManifest: function(manifest) {
    for (let app of this.object.all) {
      if (app.manifestURL == manifest) {
        return app;
      }
    }
    return null;
  },

  _onStatusChanged: function() {
    if (this._connection.status == Connection.Status.CONNECTED) {
      this._listTabs();
    } else {
      this._resetStore();
    }
  },

  _listTabs: function() {
    this._connection.client.listTabs((resp) => {
      this._webAppsActor = resp.webappsActor;
      this._feedStore().then(() => {
        this.emit("store-ready");
      });
    });
  },

  _feedStore: function() {
    if (!this._webAppsActor) {
      return promise.resolve();
    }
    this._listenToApps();
    return this._getAllApps()
               .then(this._getRunningApps.bind(this))
               .then(this._getAppsIcons.bind(this));
  },

  _listenToApps: function() {
    let deferred = promise.defer();
    let client = this._connection.client;

    let request = {
      to: this._webAppsActor,
      type: "watchApps"
    };

    client.request(request, (res) => {
      if (res.error) {
        return deferred.reject(res.error);
      }

      client.addListener("appOpen", (type, { manifestURL }) => {
        this._onAppOpen(manifestURL);
      });

      client.addListener("appClose", (type, { manifestURL }) => {
        this._onAppClose(manifestURL);
      });

      client.addListener("appInstall", (type, { manifestURL }) => {
        this._onAppInstall(manifestURL);
      });

      client.addListener("appUninstall", (type, { manifestURL }) => {
        this._onAppUninstall(manifestURL);
      });

      return deferred.resolve();
    })
    return deferred.promise;
  },

  _getAllApps: function() {
    let deferred = promise.defer();
    let request = {
      to: this._webAppsActor,
      type: "getAll"
    };

    this._connection.client.request(request, (res) => {
      if (res.error) {
        return deferred.reject(res.error);
      }
      let apps = res.apps;
      for (let a of apps) {
        a.running = false;
      }
      this.object.all = apps;
      return deferred.resolve();
    });
    return deferred.promise;
  },

  _getRunningApps: function() {
    let deferred = promise.defer();
    let request = {
      to: this._webAppsActor,
      type: "listRunningApps"
    };

    this._connection.client.request(request, (res) => {
      if (res.error) {
        return deferred.reject(res.error);
      }

      let manifests = res.apps;
      this.object.running = manifests;

      for (let m of manifests) {
        let a = this._getAppFromManifest(m);
        if (a) {
          a.running = true;
        } else {
          return deferred.reject("Unexpected manifest: " + m);
        }
      }

      return deferred.resolve();
    });
    return deferred.promise;
  },

  _getAppsIcons: function() {
    let deferred = promise.defer();
    let allApps = this.object.all;

    let request = {
      to: this._webAppsActor,
      type: "getIconAsDataURL"
    };

    let client = this._connection.client;

    let idx = 0;
    (function getIcon() {
      if (idx == allApps.length) {
        return deferred.resolve();
      }
      let a = allApps[idx++];
      request.manifestURL = a.manifestURL;
      return client.request(request, (res) => {
        if (res.error) {
          Cu.reportError(res.message || res.error);
        }

        if (res.url) {
          a.iconURL = res.url;
        }
        getIcon();
      });
    })();

    return deferred.promise;
  },

  _onAppOpen: function(manifest) {
    let a = this._getAppFromManifest(manifest);
    a.running = true;
    let running = this.object.running;
    if (running.indexOf(manifest) < 0) {
      this.object.running.push(manifest);
    }
  },

  _onAppClose: function(manifest) {
    let a = this._getAppFromManifest(manifest);
    a.running = false;
    let running = this.object.running;
    this.object.running = running.filter((m) => {
      return m != manifest;
    });
  },

  _onAppInstall: function(manifest) {
    let client = this._connection.client;
    let request = {
      to: this._webAppsActor,
      type: "getApp",
      manifestURL: manifest
    };

    client.request(request, (res) => {
      if (res.error) {
        if (res.error == "forbidden") {
          // We got a notification for an app we don't have access to.
          // Ignore.
          return;
        }
        Cu.reportError(res.message || res.error);
        return;
      }

      let app = res.app;
      app.running = false;

      let notFound = true;
      let proxifiedApp;
      for (let i = 0; i < this.object.all.length; i++) {
        let storedApp = this.object.all[i];
        if (storedApp.manifestURL == app.manifestURL) {
          this.object.all[i] = app;
          proxifiedApp = this.object.all[i];
          notFound = false;
          break;
        }
      }
      if (notFound) {
        this.object.all.push(app);
        proxifiedApp = this.object.all[this.object.all.length - 1];
      }

      request.type = "getIconAsDataURL";
      client.request(request, (res) => {
        if (res.url) {
          proxifiedApp.iconURL = res.url;
        }
      });

      // This app may have been running while being installed, so check the list
      // of running apps again to get the right answer.
      this._getRunningApps();
    });
  },

  _onAppUninstall: function(manifest) {
    this.object.all = this.object.all.filter((app) => {
      return (app.manifestURL != manifest);
    });
  },
}
