/* 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 {Cu, Cc, Ci} = require("chrome");

const promise = require("promise");
const { Task } = require("devtools/shared/task");
const { TextDecoder, OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
const Subprocess = require("sdk/system/child_process/subprocess");

const ProjectBuilding = exports.ProjectBuilding = {
  fetchPackageManifest: Task.async(function* (project) {
    let manifestPath = OS.Path.join(project.location, "package.json");
    let exists = yield OS.File.exists(manifestPath);
    if (!exists) {
      // No explicit manifest, try to generate one if possible
      return this.generatePackageManifest(project);
    }

    let data = yield OS.File.read(manifestPath);
    data = new TextDecoder().decode(data);
    let manifest;
    try {
      manifest = JSON.parse(data);
    } catch (e) {
      throw new Error("Error while reading WebIDE manifest at: '" + manifestPath +
                      "', invalid JSON: " + e.message);
    }
    return manifest;
  }),

  /**
   * For common frameworks in the community, attempt to detect the build
   * settings if none are defined.  This makes it much easier to get started
   * with WebIDE.  Later on, perhaps an add-on could define such things for
   * different frameworks.
   */
  generatePackageManifest: Task.async(function* (project) {
    // Cordova
    let cordovaConfigPath = OS.Path.join(project.location, "config.xml");
    let exists = yield OS.File.exists(cordovaConfigPath);
    if (!exists) {
      return;
    }
    let data = yield OS.File.read(cordovaConfigPath);
    data = new TextDecoder().decode(data);
    if (data.contains("cordova.apache.org")) {
      return {
        "webide": {
          "prepackage": "cordova prepare",
          "packageDir": "./platforms/firefoxos/www"
        }
      };
    }
  }),

  hasPrepackage: Task.async(function* (project) {
    let manifest = yield ProjectBuilding.fetchPackageManifest(project);
    return manifest && manifest.webide && "prepackage" in manifest.webide;
  }),

  // If the app depends on some build step, run it before pushing the app
  build: Task.async(function* ({ project, logger }) {
    if (!(yield this.hasPrepackage(project))) {
      return;
    }

    let manifest = yield ProjectBuilding.fetchPackageManifest(project);

    logger("start");
    try {
      yield this._build(project, manifest, logger);
      logger("succeed");
    } catch (e) {
      logger("failed", e);
    }
  }),

  _build: Task.async(function* (project, manifest, logger) {
    // Look for `webide` property
    manifest = manifest.webide;

    let command, cwd, args = [], env = [];

    // Copy frequently used env vars
    let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
    ["HOME", "PATH"].forEach(key => {
      let value = envService.get(key);
      if (value) {
        env.push(key + "=" + value);
      }
    });

    if (typeof (manifest.prepackage) === "string") {
      command = manifest.prepackage.replace(/%project%/g, project.location);
    } else if (manifest.prepackage.command) {
      command = manifest.prepackage.command;

      args = manifest.prepackage.args || [];
      args = args.map(a => a.replace(/%project%/g, project.location));

      env = env.concat(manifest.prepackage.env || []);
      env = env.map(a => a.replace(/%project%/g, project.location));

      if (manifest.prepackage.cwd) {
        // Normalize path for Windows support (converts / to \)
        let path = OS.Path.normalize(manifest.prepackage.cwd);
        // Note that Path.join also support absolute path and argument.
        // So that if cwd is absolute, it will return cwd.
        let rel = OS.Path.join(project.location, path);
        let exists = yield OS.File.exists(rel);
        if (exists) {
          cwd = rel;
        }
      }
    } else {
      throw new Error("pre-package manifest is invalid, missing or invalid " +
                      "`prepackage` attribute");
    }

    if (!cwd) {
      cwd = project.location;
    }

    logger("Running pre-package hook '" + command + "' " +
           args.join(" ") +
           " with ENV=[" + env.join(", ") + "]" +
           " at " + cwd);

    // Run the command through a shell command in order to support non absolute
    // paths.
    // On Windows `ComSpec` env variable is going to refer to cmd.exe,
    // Otherwise, on Linux and Mac, SHELL env variable should refer to
    // the user chosen shell program.
    // (We do not check for OS, as on windows, with cygwin, ComSpec isn't set)
    let shell = envService.get("ComSpec") || envService.get("SHELL");
    args.unshift(command);

    // For cmd.exe, we have to pass the `/C` option,
    // but for unix shells we need -c.
    // That to interpret next argument as a shell command.
    if (envService.exists("ComSpec")) {
      args.unshift("/C");
    } else {
      args.unshift("-c");
    }

    // Subprocess changes CWD, we have to save and restore it.
    let originalCwd = yield OS.File.getCurrentDirectory();
    try {
      let defer = promise.defer();
      Subprocess.call({
        command: shell,
        arguments: args,
        environment: env,
        workdir: cwd,

        stdout: data =>
          logger(data),
        stderr: data =>
          logger(data),

        done: result => {
          logger("Terminated with error code: " + result.exitCode);
          if (result.exitCode == 0) {
            defer.resolve();
          } else {
            defer.reject("pre-package command failed with error code " + result.exitCode);
          }
        }
      });
      defer.promise.then(() => {
        OS.File.setCurrentDirectory(originalCwd);
      });
      yield defer.promise;
    } catch (e) {
      throw new Error("Unable to run pre-package command '" + command + "' " +
                      args.join(" ") + ":\n" + (e.message || e));
    }
  }),

  getPackageDir: Task.async(function* (project) {
    let manifest = yield ProjectBuilding.fetchPackageManifest(project);
    if (!manifest || !manifest.webide || !manifest.webide.packageDir) {
      return project.location;
    }
    manifest = manifest.webide;

    let packageDir = OS.Path.join(project.location, manifest.packageDir);
    // On Windows, replace / by \\
    packageDir = OS.Path.normalize(packageDir);
    let exists = yield OS.File.exists(packageDir);
    if (exists) {
      return packageDir;
    }
    throw new Error("Unable to resolve application package directory: '" + manifest.packageDir + "'");
  })
};
