/* 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource:///modules/devtools/gDevTools.jsm");
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
const {AppProjects} = require("devtools/app-manager/app-projects");
const {AppValidator} = require("devtools/app-manager/app-validator");
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
const {installHosted, installPackaged, getTargetForApp,
       reloadApp, launchApp, closeApp} = require("devtools/app-actor-front");
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");

const promise = require("devtools/toolkit/deprecated-sync-thenables");

const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled";

window.addEventListener("message", function(event) {
  try {
    let json = JSON.parse(event.data);
    if (json.name == "connection") {
      let cid = parseInt(json.cid);
      for (let c of ConnectionManager.connections) {
        if (c.uid == cid) {
          UI.connection = c;
          UI.onNewConnection();
          break;
        }
      }
    }
  } catch(e) {}
});

window.addEventListener("unload", function onUnload() {
  window.removeEventListener("unload", onUnload);
  UI.destroy();
});

let UI = {
  isReady: false,

  onload: function() {
    if (Services.prefs.getBoolPref(MANIFEST_EDITOR_ENABLED)) {
      document.querySelector("#lense").setAttribute("manifest-editable", "");
    }

    this.template = new Template(document.body, AppProjects.store, Utils.l10n);
    this.template.start();

    AppProjects.load().then(() => {
      AppProjects.store.object.projects.forEach(UI.validate);
      this.isReady = true;
      this.emit("ready");
    });
  },

  destroy: function() {
    if (this.connection) {
      this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
    }
    this.template.destroy();
  },

  onNewConnection: function() {
    this.connection.on(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
    this._onConnectionStatusChange();
  },

  _onConnectionStatusChange: function() {
    if (this.connection.status != Connection.Status.CONNECTED) {
      document.body.classList.remove("connected");
      this.listTabsResponse = null;
    } else {
      document.body.classList.add("connected");
      this.connection.client.listTabs(
        response => {this.listTabsResponse = response}
      );
    }
  },

  get connected() { return !!this.listTabsResponse; },

  _selectFolder: function() {
    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    fp.init(window, Utils.l10n("project.filePickerTitle"), Ci.nsIFilePicker.modeGetFolder);
    let res = fp.show();
    if (res != Ci.nsIFilePicker.returnCancel)
      return fp.file;
    return null;
  },

  addPackaged: function(folder) {
    if (!folder) {
      folder = this._selectFolder();
    }
    if (!folder)
      return;
    return AppProjects.addPackaged(folder)
                      .then(function (project) {
                        UI.validate(project);
                        UI.selectProject(project.location);
                      });
  },

  addHosted: function() {
    let form = document.querySelector("#new-hosted-project-wrapper");
    if (!form.checkValidity())
      return;

    let urlInput = document.querySelector("#url-input");
    let manifestURL = urlInput.value;
    return AppProjects.addHosted(manifestURL)
                      .then(function (project) {
                        UI.validate(project);
                        UI.selectProject(project.location);
                      });
  },

  _getLocalIconURL: function(project, manifest) {
    let icon;
    if (manifest.icons) {
      let size = Object.keys(manifest.icons).sort(function(a, b) b - a)[0];
      if (size) {
        icon = manifest.icons[size];
      }
    }
    if (!icon)
      return "chrome://browser/skin/devtools/app-manager/default-app-icon.png";
    if (project.type == "hosted") {
      let manifestURL = Services.io.newURI(project.location, null, null);
      let origin = Services.io.newURI(manifestURL.prePath, null, null);
      return Services.io.newURI(icon, null, origin).spec;
    } else if (project.type == "packaged") {
      let projectFolder = FileUtils.File(project.location);
      let folderURI = Services.io.newFileURI(projectFolder).spec;
      return folderURI + icon.replace(/^\/|\\/, "");
    }
  },

  validate: function(project) {
    let validation = new AppValidator(project);
    return validation.validate()
      .then(function () {
        if (validation.manifest) {
          project.icon = UI._getLocalIconURL(project, validation.manifest);
          project.manifest = validation.manifest;
        }

        project.validationStatus = "valid";

        if (validation.warnings.length > 0) {
          project.warningsCount = validation.warnings.length;
          project.warnings = validation.warnings.join(",\n ");
          project.validationStatus = "warning";
        } else {
          project.warnings = "";
          project.warningsCount = 0;
        }

        if (validation.errors.length > 0) {
          project.errorsCount = validation.errors.length;
          project.errors = validation.errors.join(",\n ");
          project.validationStatus = "error";
        } else {
          project.errors = "";
          project.errorsCount = 0;
        }

        if (project.warningsCount && project.errorsCount) {
          project.validationStatus = "error warning";
        }

      });

  },

  update: function(button, location) {
    button.disabled = true;
    let project = AppProjects.get(location);

    // Update the manifest editor view, in case the manifest was modified
    // outside of the app manager.  This can happen in parallel with the other
    // steps.
    this._showManifestEditor(project);

    this.validate(project)
        .then(() => {
           // Install the app to the device if we are connected,
           // and there is no error
           if (project.errorsCount == 0 && this.connected) {
             return this.install(project);
           }
         })
        .then(() => {
           button.disabled = false;
           // Finally try to reload the app if it is already opened
           if (this.connected) {
             this.reload(project);
           }
        },
        (res) => {
          button.disabled = false;
          let message = res.error + ": " + res.message;
          alert(message);
          this.connection.log(message);
        });
  },

  saveManifest: function(button) {
    button.disabled = true;
    this.manifestEditor.save().then(() => button.disabled = false);
  },

  reload: function (project) {
    if (!this.connected) {
      return promise.reject();
    }
    return reloadApp(this.connection.client,
              this.listTabsResponse.webappsActor,
              this._getProjectManifestURL(project)).
      then(() => {
        this.connection.log("App reloaded");
      });
  },

  remove: function(location, event) {
    if (event) {
      // We don't want the "click" event to be propagated to the project item.
      // That would trigger `selectProject()`.
      event.stopPropagation();
    }

    let item = document.getElementById(location);

    let toSelect = document.querySelector(".project-item.selected");
    toSelect = toSelect ? toSelect.id : "";

    if (toSelect == location) {
      toSelect = null;
      let sibling;
      if (item.previousElementSibling) {
        sibling = item.previousElementSibling;
      } else {
        sibling = item.nextElementSibling;
      }
      if (sibling && !!AppProjects.get(sibling.id)) {
        toSelect = sibling.id;
      }
    }

    AppProjects.remove(location).then(() => {
      this.selectProject(toSelect);
    });
  },

  _getProjectManifestURL: function (project) {
    if (project.type == "packaged") {
      return "app://" + project.packagedAppOrigin + "/manifest.webapp";
    } else if (project.type == "hosted") {
      return project.location;
    }
  },

  install: function(project) {
    if (!this.connected) {
      return promise.reject();
    }
    this.connection.log("Installing the " + project.manifest.name + " app...");
    let installPromise;
    if (project.type == "packaged") {
      installPromise = installPackaged(this.connection.client, this.listTabsResponse.webappsActor, project.location, project.packagedAppOrigin)
        .then(({ appId }) => {
          // If the packaged app specified a custom origin override,
          // we need to update the local project origin
          project.packagedAppOrigin = appId;
          // And ensure the indexed db on disk is also updated
          AppProjects.update(project);
        });
    } else {
      let manifestURLObject = Services.io.newURI(project.location, null, null);
      let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
      let appId = origin.host;
      let metadata = {
        origin: origin.spec,
        manifestURL: project.location
      };
      installPromise = installHosted(this.connection.client, this.listTabsResponse.webappsActor, appId, metadata, project.manifest);
    }

    installPromise.then(() => {
      this.connection.log("Install completed.");
    }, () => {
      this.connection.log("Install failed.");
    });

    return installPromise;
  },

  start: function(project) {
    if (!this.connected) {
      return promise.reject();
    }
    let manifestURL = this._getProjectManifestURL(project);
    return launchApp(this.connection.client,
                     this.listTabsResponse.webappsActor,
                     manifestURL);
  },

  stop: function(location) {
    if (!this.connected) {
      return promise.reject();
    }
    let project = AppProjects.get(location);
    let manifestURL = this._getProjectManifestURL(project);
    return closeApp(this.connection.client,
                    this.listTabsResponse.webappsActor,
                    manifestURL);
  },

  debug: function(button, location) {
    if (!this.connected) {
      return promise.reject();
    }
    button.disabled = true;
    let project = AppProjects.get(location);

    let onFailedToStart = (error) => {
      // If not installed, install and open it
      if (error == "NO_SUCH_APP") {
        return this.install(project);
      } else {
        throw error;
      }
    };
    let onStarted = () => {
      // Once we asked the app to launch, the app isn't necessary completely loaded.
      // launch request only ask the app to launch and immediatly returns.
      // We have to keep trying to get app tab actors required to create its target.
      let deferred = promise.defer();
      let loop = (count) => {
        // Ensure not looping for ever
        if (count >= 100) {
          deferred.reject("Unable to connect to the app");
          return;
        }
        // Also, in case the app wasn't installed yet, we also have to keep asking the
        // app to launch, as launch request made right after install may race.
        this.start(project);
        getTargetForApp(
          this.connection.client,
          this.listTabsResponse.webappsActor,
          this._getProjectManifestURL(project)).
            then(deferred.resolve,
                 (err) => {
                   if (err == "appNotFound")
                     setTimeout(loop, 500, count + 1);
                   else
                     deferred.reject(err);
                 });
      };
      loop(0);
      return deferred.promise;
    };

    // First try to open the app
    this.start(project)
        .then(null, onFailedToStart)
        .then(onStarted)
        .then((target) =>
          top.UI.openAndShowToolboxForTarget(target,
                                             project.manifest.name,
                                             project.icon))
        .then(() => {
           // And only when the toolbox is opened, release the button
           button.disabled = false;
         },
         (err) => {
           button.disabled = false;
           let message = err.error ? err.error + ": " + err.message : String(err);
           alert(message);
           this.connection.log(message);
         });
  },

  reveal: function(location) {
    let project = AppProjects.get(location);
    if (project.type == "packaged") {
      let projectFolder = FileUtils.File(project.location);
      projectFolder.reveal();
    } else {
      // TODO: eventually open hosted apps in firefox
      // when permissions are correctly supported by firefox
    }
  },

  selectProject: function(location) {
    let projects = AppProjects.store.object.projects;
    let idx = 0;
    for (; idx < projects.length; idx++) {
      if (projects[idx].location == location) {
        break;
      }
    }

    let oldButton = document.querySelector(".project-item.selected");
    if (oldButton) {
      oldButton.classList.remove("selected");
    }

    if (idx == projects.length) {
      // Not found. Empty lense.
      let lense = document.querySelector("#lense");
      lense.setAttribute("template-for", '{"path":"","childSelector":""}');
      this.template._processFor(lense);
      return;
    }

    let button = document.getElementById(location);
    button.classList.add("selected");

    let template = '{"path":"projects.' + idx + '","childSelector":"#lense-template"}';

    let lense = document.querySelector("#lense");
    lense.setAttribute("template-for", template);
    this.template._processFor(lense);

    let project = projects[idx];
    this._showManifestEditor(project).then(() => this.emit("project-selected"));
  },

  _showManifestEditor: function(project) {
    if (this.manifestEditor) {
      this.manifestEditor.destroy();
    }
    let editorContainer = document.querySelector("#lense .manifest-editor");
    this.manifestEditor = new ManifestEditor(project);
    return this.manifestEditor.show(editorContainer);
  }
};

// This must be bound immediately, as it might be used via the message listener
// before UI.onload() has been called.
UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI);

EventEmitter.decorate(UI);
