/* 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";

var {Ci, Cu, CC} = require("chrome");
const promise = require("promise");

const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
const Services = require("Services");
const {Task} = require("devtools/shared/task");
var XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
var strings = Services.strings.createBundle("chrome://devtools/locale/app-manager.properties");

function AppValidator({ type, location }) {
  this.type = type;
  this.location = location;
  this.errors = [];
  this.warnings = [];
}

AppValidator.prototype.error = function (message) {
  this.errors.push(message);
};

AppValidator.prototype.warning = function (message) {
  this.warnings.push(message);
};

AppValidator.prototype._getPackagedManifestFile = function () {
  let manifestFile = FileUtils.File(this.location);
  if (!manifestFile.exists()) {
    this.error(strings.GetStringFromName("validator.nonExistingFolder"));
    return null;
  }
  if (!manifestFile.isDirectory()) {
    this.error(strings.GetStringFromName("validator.expectProjectFolder"));
    return null;
  }

  let appManifestFile = manifestFile.clone();
  appManifestFile.append("manifest.webapp");

  let jsonManifestFile = manifestFile.clone();
  jsonManifestFile.append("manifest.json");

  let hasAppManifest = appManifestFile.exists() && appManifestFile.isFile();
  let hasJsonManifest = jsonManifestFile.exists() && jsonManifestFile.isFile();

  if (!hasAppManifest && !hasJsonManifest) {
    this.error(strings.GetStringFromName("validator.noManifestFile"));
    return null;
  }

  return hasAppManifest ? appManifestFile : jsonManifestFile;
};

AppValidator.prototype._getPackagedManifestURL = function () {
  let manifestFile = this._getPackagedManifestFile();
  if (!manifestFile) {
    return null;
  }
  return Services.io.newFileURI(manifestFile).spec;
};

AppValidator.checkManifest = function (manifestURL) {
  let deferred = promise.defer();
  let error;

  let req = new XMLHttpRequest();
  req.overrideMimeType("text/plain");

  try {
    req.open("GET", manifestURL, true);
    req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
  } catch (e) {
    error = strings.formatStringFromName("validator.invalidManifestURL", [manifestURL], 1);
    deferred.reject(error);
    return deferred.promise;
  }

  req.onload = function () {
    let manifest = null;
    try {
      manifest = JSON.parse(req.responseText);
    } catch (e) {
      error = strings.formatStringFromName("validator.invalidManifestJSON", [e, manifestURL], 2);
      deferred.reject(error);
    }

    deferred.resolve({manifest, manifestURL});
  };

  req.onerror = function () {
    error = strings.formatStringFromName("validator.noAccessManifestURL", [req.statusText, manifestURL], 2);
    deferred.reject(error);
  };

  try {
    req.send(null);
  } catch (e) {
    error = strings.formatStringFromName("validator.noAccessManifestURL", [e, manifestURL], 2);
    deferred.reject(error);
  }

  return deferred.promise;
};

AppValidator.findManifestAtOrigin = function (manifestURL) {
  let fixedManifest = Services.io.newURI(manifestURL, null, null).prePath + "/manifest.webapp";
  return AppValidator.checkManifest(fixedManifest);
};

AppValidator.findManifestPath = function (manifestURL) {
  let deferred = promise.defer();

  if (manifestURL.endsWith("manifest.webapp")) {
    deferred.reject();
  } else {
    let fixedManifest = manifestURL + "/manifest.webapp";
    deferred.resolve(AppValidator.checkManifest(fixedManifest));
  }

  return deferred.promise;
};

AppValidator.checkAlternateManifest = function (manifestURL) {
  return Task.spawn(function* () {
    let result;
    try {
      result = yield AppValidator.findManifestPath(manifestURL);
    } catch (e) {
      result = yield AppValidator.findManifestAtOrigin(manifestURL);
    }

    return result;
  });
};

AppValidator.prototype._fetchManifest = function (manifestURL) {
  let deferred = promise.defer();
  this.manifestURL = manifestURL;

  AppValidator.checkManifest(manifestURL)
              .then(({manifest, manifestURL}) => {
                deferred.resolve(manifest);
              }, error => {
                AppValidator.checkAlternateManifest(manifestURL)
                            .then(({manifest, manifestURL}) => {
                              this.manifestURL = manifestURL;
                              deferred.resolve(manifest);
                            }, () => {
                              this.error(error);
                              deferred.resolve(null);
                            });
              });

  return deferred.promise;
};

AppValidator.prototype._getManifest = function () {
  let manifestURL;
  if (this.type == "packaged") {
    manifestURL = this._getPackagedManifestURL();
    if (!manifestURL)
      return promise.resolve(null);
  } else if (this.type == "hosted") {
    manifestURL = this.location;
    try {
      Services.io.newURI(manifestURL, null, null);
    } catch (e) {
      this.error(strings.formatStringFromName("validator.invalidHostedManifestURL", [manifestURL, e.message], 2));
      return promise.resolve(null);
    }
  } else {
    this.error(strings.formatStringFromName("validator.invalidProjectType", [this.type], 1));
    return promise.resolve(null);
  }
  return this._fetchManifest(manifestURL);
};

AppValidator.prototype.validateManifest = function (manifest) {
  if (!manifest.name) {
    this.error(strings.GetStringFromName("validator.missNameManifestProperty"));
  }

  if (!manifest.icons || Object.keys(manifest.icons).length === 0) {
    this.warning(strings.GetStringFromName("validator.missIconsManifestProperty"));
  } else if (!manifest.icons["128"]) {
    this.warning(strings.GetStringFromName("validator.missIconMarketplace2"));
  }
};

AppValidator.prototype._getOriginURL = function () {
  if (this.type == "packaged") {
    let manifestURL = Services.io.newURI(this.manifestURL, null, null);
    return Services.io.newURI(".", null, manifestURL).spec;
  } else if (this.type == "hosted") {
    return Services.io.newURI(this.location, null, null).prePath;
  }
};

AppValidator.prototype.validateLaunchPath = function (manifest) {
  let deferred = promise.defer();
  // The launch_path field has to start with a `/`
  if (manifest.launch_path && manifest.launch_path[0] !== "/") {
    this.error(strings.formatStringFromName("validator.nonAbsoluteLaunchPath", [manifest.launch_path], 1));
    deferred.resolve();
    return deferred.promise;
  }
  let origin = this._getOriginURL();
  let path;
  if (this.type == "packaged") {
    path = "." + (manifest.launch_path || "/index.html");
  } else if (this.type == "hosted") {
    path = manifest.launch_path || "/";
  }
  let indexURL;
  try {
    indexURL = Services.io.newURI(path, null, Services.io.newURI(origin, null, null)).spec;
  } catch (e) {
    this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [origin + path], 1));
    deferred.resolve();
    return deferred.promise;
  }

  let req = new XMLHttpRequest();
  req.overrideMimeType("text/plain");
  try {
    req.open("HEAD", indexURL, true);
    req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
  } catch (e) {
    this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
    deferred.resolve();
    return deferred.promise;
  }
  req.onload = () => {
    if (req.status >= 400)
      this.error(strings.formatStringFromName("validator.accessFailedLaunchPathBadHttpCode", [indexURL, req.status], 2));
    deferred.resolve();
  };
  req.onerror = () => {
    this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
    deferred.resolve();
  };

  try {
    req.send(null);
  } catch (e) {
    this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
    deferred.resolve();
  }

  return deferred.promise;
};

AppValidator.prototype.validateType = function (manifest) {
  let appType = manifest.type || "web";
  if (["web", "privileged", "certified"].indexOf(appType) === -1) {
    this.error(strings.formatStringFromName("validator.invalidAppType", [appType], 1));
  } else if (this.type == "hosted" &&
             ["certified", "privileged"].indexOf(appType) !== -1) {
    this.error(strings.formatStringFromName("validator.invalidHostedPriviledges", [appType], 1));
  }

  // certified app are not fully supported on the simulator
  if (appType === "certified") {
    this.warning(strings.GetStringFromName("validator.noCertifiedSupport"));
  }
};

AppValidator.prototype.validate = function () {
  this.errors = [];
  this.warnings = [];
  return this._getManifest().
    then((manifest) => {
      if (manifest) {
        this.manifest = manifest;

        // Skip validations for add-ons
        if (manifest.role === "addon" || manifest.manifest_version) {
          return promise.resolve();
        }

        this.validateManifest(manifest);
        this.validateType(manifest);
        return this.validateLaunchPath(manifest);
      }
    });
};

exports.AppValidator = AppValidator;
