/* 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://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");

const XRE_OS_UPDATE_APPLY_TO_DIR = "OSUpdApplyToD"
const UPDATE_ARCHIVE_DIR = "UpdArchD"
const LOCAL_DIR = "/data/local";
const UPDATES_DIR = "updates/0";
const FOTA_DIR = "updates/fota";
const COREAPPSDIR_PREF = "b2g.coreappsdir"

XPCOMUtils.defineLazyServiceGetter(Services, "env",
                                   "@mozilla.org/process/environment;1",
                                   "nsIEnvironment");

XPCOMUtils.defineLazyServiceGetter(Services, "um",
                                   "@mozilla.org/updates/update-manager;1",
                                   "nsIUpdateManager");

XPCOMUtils.defineLazyServiceGetter(Services, "volumeService",
                                   "@mozilla.org/telephony/volume-service;1",
                                   "nsIVolumeService");

XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                   "@mozilla.org/childprocessmessagemanager;1",
                                   "nsISyncMessageSender");

XPCOMUtils.defineLazyGetter(this, "gExtStorage", function dp_gExtStorage() {
    return Services.env.get("EXTERNAL_STORAGE");
});

// This exists to mark the affected code for bug 828858.
const gUseSDCard = true;

const VERBOSE = 1;
var log =
  VERBOSE ?
  function log_dump(msg) { dump("DirectoryProvider: " + msg + "\n"); } :
  function log_noop(msg) { };

function DirectoryProvider() {
}

DirectoryProvider.prototype = {
  classID: Components.ID("{9181eb7c-6f87-11e1-90b1-4f59d80dd2e5}"),

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
  _xpcom_factory: XPCOMUtils.generateSingletonFactory(DirectoryProvider),

  _profD: null,

  getFile: function(prop, persistent) {
    if (AppConstants.platform === "gonk") {
      return this.getFileOnGonk(prop, persistent);
    }
    return this.getFileNotGonk(prop, persistent);
  },

  getFileOnGonk: function(prop, persistent) {
    let localProps = ["cachePDir", "webappsDir", "PrefD", "indexedDBPDir",
                      "permissionDBPDir", "UpdRootD"];
    if (localProps.indexOf(prop) != -1) {
      let file = Cc["@mozilla.org/file/local;1"]
                   .createInstance(Ci.nsILocalFile)
      file.initWithPath(LOCAL_DIR);
      persistent.value = true;
      return file;
    }
    if (prop == "ProfD") {
      let dir = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile);
      dir.initWithPath(LOCAL_DIR+"/tests/profile");
      if (dir.exists()) {
        persistent.value = true;
        return dir;
      }
    }
    if (prop == "coreAppsDir") {
      let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile)
      file.initWithPath("/system/b2g");
      persistent.value = true;
      return file;
    }
    if (prop == UPDATE_ARCHIVE_DIR) {
      // getUpdateDir will set persistent to false since it may toggle between
      // /data/local/ and /mnt/sdcard based on free space and/or availability
      // of the sdcard.
      // before download, check if free space is 2.1 times of update.mar
      return this.getUpdateDir(persistent, UPDATES_DIR, 2.1);
    }
    if (prop == XRE_OS_UPDATE_APPLY_TO_DIR) {
      // getUpdateDir will set persistent to false since it may toggle between
      // /data/local/ and /mnt/sdcard based on free space and/or availability
      // of the sdcard.
      // before apply, check if free space is 1.1 times of update.mar
      return this.getUpdateDir(persistent, FOTA_DIR, 1.1);
    }
    return null;
  },

  getFileNotGonk: function(prop, persistent) {
    // In desktop builds, coreAppsDir is the same as the profile
    // directory unless otherwise specified. We just need to get the
    // path from the parent, and it is then used to build
    // jar:remoteopenfile:// uris.
    if (prop == "coreAppsDir") {
      let coreAppsDirPref;
      try {
        coreAppsDirPref = Services.prefs.getCharPref(COREAPPSDIR_PREF);
      } catch (e) {
        // coreAppsDirPref may not exist if we're on an older version
        // of gaia, so just fail silently.
      }
      let appsDir;
      // If pref doesn't exist or isn't set, default to old value
      if (!coreAppsDirPref || coreAppsDirPref == "") {
        appsDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
        appsDir.append("webapps");
      } else {
        appsDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile)
        appsDir.initWithPath(coreAppsDirPref);
      }
      persistent.value = true;
      return appsDir;
    } else if (prop == "ProfD") {
      let inParent = Cc["@mozilla.org/xre/app-info;1"]
                       .getService(Ci.nsIXULRuntime)
                       .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
      if (inParent) {
        // Just bail out to use the default from toolkit.
        return null;
      }
      if (!this._profD) {
        this._profD = cpmm.sendSyncMessage("getProfD", {})[0];
      }
      let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
      file.initWithPath(this._profD);
      persistent.value = true;
      return file;
    }
    return null;
  },

  // The VolumeService only exists on the device, and not on desktop
  volumeHasFreeSpace: function dp_volumeHasFreeSpace(volumePath, requiredSpace) {
    if (!volumePath) {
      return false;
    }
    if (!Services.volumeService) {
      return false;
    }
    let volume = Services.volumeService.createOrGetVolumeByPath(volumePath);
    if (!volume || volume.state !== Ci.nsIVolume.STATE_MOUNTED) {
      return false;
    }
    let stat = volume.getStats();
    if (!stat) {
      return false;
    }
    return requiredSpace <= stat.freeBytes;
  },

  findUpdateDirWithFreeSpace: function dp_findUpdateDirWithFreeSpace(requiredSpace, subdir) {
    if (!Services.volumeService) {
      return this.createUpdatesDir(LOCAL_DIR, subdir);
    }

    let activeUpdate = Services.um.activeUpdate;
    if (gUseSDCard) {
      if (this.volumeHasFreeSpace(gExtStorage, requiredSpace)) {
        let extUpdateDir = this.createUpdatesDir(gExtStorage, subdir);
        if (extUpdateDir !== null) {
          return extUpdateDir;
        }
        log("Warning: " + gExtStorage + " has enough free space for update " +
            activeUpdate.name + ", but is not writable");
      }
    }

    if (this.volumeHasFreeSpace(LOCAL_DIR, requiredSpace)) {
      let localUpdateDir = this.createUpdatesDir(LOCAL_DIR, subdir);
      if (localUpdateDir !== null) {
        return localUpdateDir;
      }
      log("Warning: " + LOCAL_DIR + " has enough free space for update " +
          activeUpdate.name + ", but is not writable");
    }

    return null;
  },

  getUpdateDir: function dp_getUpdateDir(persistent, subdir, multiple) {
    let defaultUpdateDir = this.getDefaultUpdateDir();
    persistent.value = false;

    let activeUpdate = Services.um.activeUpdate;
    if (!activeUpdate) {
      log("Warning: No active update found, using default update dir: " +
          defaultUpdateDir);
      return defaultUpdateDir;
    }

    let selectedPatch = activeUpdate.selectedPatch;
    if (!selectedPatch) {
      log("Warning: No selected patch, using default update dir: " +
          defaultUpdateDir);
      return defaultUpdateDir;
    }

    let requiredSpace = selectedPatch.size * multiple;
    let updateDir = this.findUpdateDirWithFreeSpace(requiredSpace, subdir);
    if (updateDir) {
      return updateDir;
    }

    // If we've gotten this far, there isn't enough free space to download the patch
    // on either external storage or /data/local. All we can do is report the
    // error and let upstream code handle it more gracefully.
    log("Error: No volume found with " + requiredSpace + " bytes for downloading"+
        " update " + activeUpdate.name);
    activeUpdate.errorCode = Cr.NS_ERROR_FILE_TOO_BIG;
    return null;
  },

  createUpdatesDir: function dp_createUpdatesDir(root, subdir) {
      let dir = Cc["@mozilla.org/file/local;1"]
                   .createInstance(Ci.nsILocalFile);
      dir.initWithPath(root);
      if (!dir.isWritable()) {
        log("Error: " + dir.path + " isn't writable");
        return null;
      }
      dir.appendRelativePath(subdir);
      if (dir.exists()) {
        if (dir.isDirectory() && dir.isWritable()) {
          return dir;
        }
        // subdir is either a file or isn't writable. In either case we
        // can't use it.
        log("Error: " + dir.path + " is a file or isn't writable");
        return null;
      }
      // subdir doesn't exist, and the parent is writable, so try to
      // create it. This can fail if a file named updates exists.
      try {
        dir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0770', 8));
      } catch (e) {
        // The create failed for some reason. We can't use it.
        log("Error: " + dir.path + " unable to create directory");
        return null;
      }
      return dir;
  },

  getDefaultUpdateDir: function dp_getDefaultUpdateDir() {
    let path = gExtStorage;
    if (!path) {
      path = LOCAL_DIR;
    }

    if (Services.volumeService) {
      let extVolume = Services.volumeService.createOrGetVolumeByPath(path);
      if (!extVolume) {
        path = LOCAL_DIR;
      }
    }

    let dir = Cc["@mozilla.org/file/local;1"]
                 .createInstance(Ci.nsILocalFile)
    dir.initWithPath(path);

    if (!dir.exists() && path != LOCAL_DIR) {
      // Fallback to LOCAL_DIR if we didn't fallback earlier
      dir.initWithPath(LOCAL_DIR);

      if (!dir.exists()) {
        throw Cr.NS_ERROR_FILE_NOT_FOUND;
      }
    }

    dir.appendRelativePath("updates");
    return dir;
  }
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DirectoryProvider]);