/* 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/. */

/**
 * Asynchronous front-end for OS.File.
 *
 * This front-end is meant to be imported from the main thread. In turn,
 * it spawns one worker (perhaps more in the future) and delegates all
 * disk I/O to this worker.
 *
 * Documentation note: most of the functions and methods in this module
 * return promises. For clarity, we denote as follows a promise that may resolve
 * with type |A| and some value |value| or reject with type |B| and some
 * reason |reason|
 * @resolves {A} value
 * @rejects {B} reason
 */

"use strict";

this.EXPORTED_SYMBOLS = ["OS"];

const Cu = Components.utils;
const Ci = Components.interfaces;

var SharedAll = {};
Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);


// Boilerplate, to simplify the transition to require()
var LOG = SharedAll.LOG.bind(SharedAll, "Controller");
var isTypedArray = SharedAll.isTypedArray;

// The constructor for file errors.
var SysAll = {};
if (SharedAll.Constants.Win) {
  Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll);
} else if (SharedAll.Constants.libc) {
  Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll);
} else {
  throw new Error("I am neither under Windows nor under a Posix system");
}
var OSError = SysAll.Error;
var Type = SysAll.Type;

var Path = {};
Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);

// The library of promises.
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);

// The implementation of communications
Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
var Native = Cu.import("resource://gre/modules/osfile/osfile_native.jsm", {});


// It's possible for osfile.jsm to get imported before the profile is
// set up. In this case, some path constants aren't yet available.
// Here, we make them lazy loaders.

function lazyPathGetter(constProp, dirKey) {
  return function () {
    let path;
    try {
      path = Services.dirsvc.get(dirKey, Ci.nsIFile).path;
      delete SharedAll.Constants.Path[constProp];
      SharedAll.Constants.Path[constProp] = path;
    } catch (ex) {
      // Ignore errors if the value still isn't available. Hopefully
      // the next access will return it.
    }

    return path;
  };
}

for (let [constProp, dirKey] of [
  ["localProfileDir", "ProfLD"],
  ["profileDir", "ProfD"],
  ["userApplicationDataDir", "UAppData"],
  ["winAppDataDir", "AppData"],
  ["winStartMenuProgsDir", "Progs"],
  ]) {

  if (constProp in SharedAll.Constants.Path) {
    continue;
  }

  LOG("Installing lazy getter for OS.Constants.Path." + constProp +
      " because it isn't defined and profile may not be loaded.");
  Object.defineProperty(SharedAll.Constants.Path, constProp, {
    get: lazyPathGetter(constProp, dirKey),
  });
}

/**
 * Return a shallow clone of the enumerable properties of an object.
 */
var clone = SharedAll.clone;

/**
 * Extract a shortened version of an object, fit for logging.
 *
 * This function returns a copy of the original object in which all
 * long strings, Arrays, TypedArrays, ArrayBuffers are removed and
 * replaced with placeholders. Use this function to sanitize objects
 * if you wish to log them or to keep them in memory.
 *
 * @param {*} obj The obj to shorten.
 * @return {*} array A shorter object, fit for logging.
 */
function summarizeObject(obj) {
  if (!obj) {
    return null;
  }
  if (typeof obj == "string") {
    if (obj.length > 1024) {
      return {"Long string": obj.length};
    }
    return obj;
  }
  if (typeof obj == "object") {
    if (Array.isArray(obj)) {
      if (obj.length > 32) {
        return {"Long array": obj.length};
      }
      return obj.map(summarizeObject);
    }
    if ("byteLength" in obj) {
      // Assume TypedArray or ArrayBuffer
      return {"Binary Data": obj.byteLength};
    }
    let result = {};
    for (let k of Object.keys(obj)) {
      result[k] = summarizeObject(obj[k]);
    }
    return result;
  }
  return obj;
}

// In order to expose Scheduler to the unfiltered Cu.import return value variant
// on B2G we need to save it to `this`.  This does not make it public;
// EXPORTED_SYMBOLS still controls that in all cases.
var Scheduler = this.Scheduler = {

  /**
   * |true| once we have sent at least one message to the worker.
   * This field is unaffected by resetting the worker.
   */
  launched: false,

  /**
   * |true| once shutdown has begun i.e. we should reject any
   * message, including resets.
   */
  shutdown: false,

  /**
   * A promise resolved once all currently pending operations are complete.
   *
   * This promise is never rejected and the result is always undefined.
   */
  queue: Promise.resolve(),

  /**
   * A promise resolved once all currently pending `kill` operations
   * are complete.
   *
   * This promise is never rejected and the result is always undefined.
   */
  _killQueue: Promise.resolve(),

  /**
   * Miscellaneous debugging information
   */
  Debugging: {
    /**
     * The latest message sent and still waiting for a reply.
     */
    latestSent: undefined,

    /**
     * The latest reply received, or null if we are waiting for a reply.
     */
    latestReceived: undefined,

    /**
     * Number of messages sent to the worker. This includes the
     * initial SET_DEBUG, if applicable.
     */
    messagesSent: 0,

    /**
     * Total number of messages ever queued, including the messages
     * sent.
     */
    messagesQueued: 0,

    /**
     * Number of messages received from the worker.
     */
    messagesReceived: 0,
  },

  /**
   * A timer used to automatically shut down the worker after some time.
   */
  resetTimer: null,

  /**
   * The worker to which to send requests.
   *
   * If the worker has never been created or has been reset, this is a
   * fresh worker, initialized with osfile_async_worker.js.
   *
   * @type {PromiseWorker}
   */
  get worker() {
    if (!this._worker) {
      // Either the worker has never been created or it has been
      // reset.  In either case, it is time to instantiate the worker.
      this._worker = new BasePromiseWorker("resource://gre/modules/osfile/osfile_async_worker.js");
      this._worker.log = LOG;
      this._worker.ExceptionHandlers["OS.File.Error"] = OSError.fromMsg;
    }
    return this._worker;
  },

  _worker: null,

  /**
   * Prepare to kill the OS.File worker after a few seconds.
   */
  restartTimer: function(arg) {
    let delay;
    try {
      delay = Services.prefs.getIntPref("osfile.reset_worker_delay");
    } catch(e) {
      // Don't auto-shutdown if we don't have a delay preference set.
      return;
    }

    if (this.resetTimer) {
      clearTimeout(this.resetTimer);
    }
    this.resetTimer = setTimeout(
      () => Scheduler.kill({reset: true, shutdown: false}),
      delay
    );
  },

  /**
   * Shutdown OS.File.
   *
   * @param {*} options
   * - {boolean} shutdown If |true|, reject any further request. Otherwise,
   *   further requests will resurrect the worker.
   * - {boolean} reset If |true|, instruct the worker to shutdown if this
   *   would not cause leaks. Otherwise, assume that the worker will be shutdown
   *   through some other mean.
   */
  kill: function({shutdown, reset}) {
    // Grab the kill queue to make sure that we
    // cannot be interrupted by another call to `kill`.
    let killQueue = this._killQueue;

    // Deactivate the queue, to ensure that no message is sent
    // to an obsolete worker (we reactivate it in the `finally`).
    // This needs to be done right now so that we maintain relative
    // ordering with calls to post(), etc.
    let deferred = Promise.defer();
    let savedQueue = this.queue;
    this.queue = deferred.promise;

    return this._killQueue = Task.spawn(function*() {

      yield killQueue;
      // From this point, and until the end of the Task, we are the
      // only call to `kill`, regardless of any `yield`.

      yield savedQueue;

      try {
        // Enter critical section: no yield in this block
        // (we want to make sure that we remain the only
        // request in the queue).

        if (!this.launched || this.shutdown || !this._worker) {
          // Nothing to kill
          this.shutdown = this.shutdown || shutdown;
          this._worker = null;
          return null;
        }

        // Exit critical section

        let message = ["Meta_shutdown", [reset]];

        Scheduler.latestReceived = [];
        Scheduler.latestSent = [Date.now(),
          Task.Debugging.generateReadableStack(new Error().stack),
          ...message];

        // Wait for result
        let resources;
        try {
          resources = yield this._worker.post(...message);

          Scheduler.latestReceived = [Date.now(), message];
        } catch (ex) {
          LOG("Could not dispatch Meta_reset", ex);
          // It's most likely a programmer error, but we'll assume that
          // the worker has been shutdown, as it's less risky than the
          // opposite stance.
          resources = {openedFiles: [], openedDirectoryIterators: [], killed: true};

          Scheduler.latestReceived = [Date.now(), message, ex];
        }

        let {openedFiles, openedDirectoryIterators, killed} = resources;
        if (!reset
          && (openedFiles && openedFiles.length
            || ( openedDirectoryIterators && openedDirectoryIterators.length))) {
          // The worker still holds resources. Report them.

          let msg = "";
          if (openedFiles.length > 0) {
            msg += "The following files are still open:\n" +
              openedFiles.join("\n");
          }
          if (openedDirectoryIterators.length > 0) {
            msg += "The following directory iterators are still open:\n" +
              openedDirectoryIterators.join("\n");
          }

          LOG("WARNING: File descriptors leaks detected.\n" + msg);
        }

        // Make sure that we do not leave an invalid |worker| around.
        if (killed || shutdown) {
          this._worker = null;
        }

        this.shutdown = shutdown;

        return resources;

      } finally {
        // Resume accepting messages. If we have set |shutdown| to |true|,
        // any pending/future request will be rejected. Otherwise, any
        // pending/future request will spawn a new worker if necessary.
        deferred.resolve();
      }

    }.bind(this));
  },

  /**
   * Push a task at the end of the queue.
   *
   * @param {function} code A function returning a Promise.
   * This function will be executed once all the previously
   * pushed tasks have completed.
   * @return {Promise} A promise with the same behavior as
   * the promise returned by |code|.
   */
  push: function(code) {
    let promise = this.queue.then(code);
    // By definition, |this.queue| can never reject.
    this.queue = promise.then(null, () => undefined);
    // Fork |promise| to ensure that uncaught errors are reported
    return promise.then(null, null);
  },

  /**
   * Post a message to the worker thread.
   *
   * @param {string} method The name of the method to call.
   * @param {...} args The arguments to pass to the method. These arguments
   * must be clonable.
   * @return {Promise} A promise conveying the result/error caused by
   * calling |method| with arguments |args|.
   */
  post: function post(method, args = undefined, closure = undefined) {
    if (this.shutdown) {
      LOG("OS.File is not available anymore. The following request has been rejected.",
        method, args);
      return Promise.reject(new Error("OS.File has been shut down. Rejecting post to " + method));
    }
    let firstLaunch = !this.launched;
    this.launched = true;

    if (firstLaunch && SharedAll.Config.DEBUG) {
      // If we have delayed sending SET_DEBUG, do it now.
      this.worker.post("SET_DEBUG", [true]);
      Scheduler.Debugging.messagesSent++;
    }

    Scheduler.Debugging.messagesQueued++;
    return this.push(Task.async(function*() {
      if (this.shutdown) {
	LOG("OS.File is not available anymore. The following request has been rejected.",
	  method, args);
	throw new Error("OS.File has been shut down. Rejecting request to " + method);
      }

      // Update debugging information. As |args| may be quite
      // expensive, we only keep a shortened version of it.
      Scheduler.Debugging.latestReceived = null;
      Scheduler.Debugging.latestSent = [Date.now(), method, summarizeObject(args)];

      // Don't kill the worker just yet
      Scheduler.restartTimer();


      let reply;
      try {
        try {
          Scheduler.Debugging.messagesSent++;
          Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2);
          reply = yield this.worker.post(method, args, closure);
          Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)];
          return reply;
        } finally {
          Scheduler.Debugging.messagesReceived++;
        }
      } catch (error) {
        Scheduler.Debugging.latestReceived = [Date.now(), error.message, error.fileName, error.lineNumber];
        throw error;
      } finally {
        if (firstLaunch) {
          Scheduler._updateTelemetry();
        }
        Scheduler.restartTimer();
      }
    }.bind(this)));
  },

  /**
   * Post Telemetry statistics.
   *
   * This is only useful on first launch.
   */
  _updateTelemetry: function() {
    let worker = this.worker;
    let workerTimeStamps = worker.workerTimeStamps;
    if (!workerTimeStamps) {
      // If the first call to OS.File results in an uncaught errors,
      // the timestamps are absent. As this case is a developer error,
      // let's not waste time attempting to extract telemetry from it.
      return;
    }
    let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById("OSFILE_WORKER_LAUNCH_MS");
    HISTOGRAM_LAUNCH.add(worker.workerTimeStamps.entered - worker.launchTimeStamp);

    let HISTOGRAM_READY = Services.telemetry.getHistogramById("OSFILE_WORKER_READY_MS");
    HISTOGRAM_READY.add(worker.workerTimeStamps.loaded - worker.launchTimeStamp);
  }
};

const PREF_OSFILE_LOG = "toolkit.osfile.log";
const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect";

/**
 * Safely read a PREF_OSFILE_LOG preference.
 * Returns a value read or, in case of an error, oldPref or false.
 *
 * @param bool oldPref
 *        An optional value that the DEBUG flag was set to previously.
 */
function readDebugPref(prefName, oldPref = false) {
  let pref = oldPref;
  try {
    pref = Services.prefs.getBoolPref(prefName);
  } catch (x) {
    // In case of an error when reading a pref keep it as is.
  }
  // If neither pref nor oldPref were set, default it to false.
  return pref;
};

/**
 * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag
 * appropriately.
 */
Services.prefs.addObserver(PREF_OSFILE_LOG,
  function prefObserver(aSubject, aTopic, aData) {
    SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG);
    if (Scheduler.launched) {
      // Don't start the worker just to set this preference.
      Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]);
    }
  }, false);
SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false);

Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT,
  function prefObserver(aSubject, aTopic, aData) {
    SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST);
  }, false);
SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false);


/**
 * If |true|, use the native implementaiton of OS.File methods
 * whenever possible. Otherwise, force the use of the JS version.
 */
var nativeWheneverAvailable = true;
const PREF_OSFILE_NATIVE = "toolkit.osfile.native";
Services.prefs.addObserver(PREF_OSFILE_NATIVE,
  function prefObserver(aSubject, aTopic, aData) {
    nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable);
  }, false);


// Update worker's DEBUG flag if it's true.
// Don't start the worker just for this, though.
if (SharedAll.Config.DEBUG && Scheduler.launched) {
  Scheduler.post("SET_DEBUG", [true]);
}

// Observer topics used for monitoring shutdown
const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown";

// Preference used to configure test shutdown observer.
const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER =
  "toolkit.osfile.test.shutdown.observer";

AsyncShutdown.webWorkersShutdown.addBlocker(
  "OS.File: flush pending requests, warn about unclosed files, shut down service.",
  Task.async(function*() {
    // Give clients a last chance to enqueue requests.
    yield Barriers.shutdown.wait({crashAfterMS: null});

    // Wait until all requests are complete and kill the worker.
    yield Scheduler.kill({reset: false, shutdown: true});
  }),
  () => {
    let details = Barriers.getDetails();
    details.clients = Barriers.shutdown.state;
    return details;
  }
);


// Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or
// disable the test shutdown event observer.
// Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset.
// Note: This is meant to be used for testing purposes only.
Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER,
  function prefObserver() {
    // The temporary phase topic used to trigger the unclosed
    // phase warning.
    let TOPIC = null;
    try {
      TOPIC = Services.prefs.getCharPref(
        PREF_OSFILE_TEST_SHUTDOWN_OBSERVER);
    } catch (x) {
    }
    if (TOPIC) {
      // Generate a phase, add a blocker.
      // Note that this can work only if AsyncShutdown itself has been
      // configured for testing by the testsuite.
      let phase = AsyncShutdown._getPhase(TOPIC);
      phase.addBlocker(
        "(for testing purposes) OS.File: warn about unclosed files",
        () => Scheduler.kill({shutdown: false, reset: false})
      );
    }
  }, false);

/**
 * Representation of a file, with asynchronous methods.
 *
 * @param {*} fdmsg The _message_ representing the platform-specific file
 * handle.
 *
 * @constructor
 */
var File = function File(fdmsg) {
  // FIXME: At the moment, |File| does not close on finalize
  // (see bug 777715)
  this._fdmsg = fdmsg;
  this._closeResult = null;
  this._closed = null;
};


File.prototype = {
  /**
   * Close a file asynchronously.
   *
   * This method is idempotent.
   *
   * @return {promise}
   * @resolves {null}
   * @rejects {OS.File.Error}
   */
  close: function close() {
    if (this._fdmsg != null) {
      let msg = this._fdmsg;
      this._fdmsg = null;
      return this._closeResult =
        Scheduler.post("File_prototype_close", [msg], this);
    }
    return this._closeResult;
  },

  /**
   * Fetch information about the file.
   *
   * @return {promise}
   * @resolves {OS.File.Info} The latest information about the file.
   * @rejects {OS.File.Error}
   */
  stat: function stat() {
    return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then(
      File.Info.fromMsg
    );
  },

  /**
   * Write bytes from a buffer to this file.
   *
   * Note that, by default, this function may perform several I/O
   * operations to ensure that the buffer is fully written.
   *
   * @param {Typed array | C pointer} buffer The buffer in which the
   * the bytes are stored. The buffer must be large enough to
   * accomodate |bytes| bytes. Using the buffer before the operation
   * is complete is a BAD IDEA.
   * @param {*=} options Optionally, an object that may contain the
   * following fields:
   * - {number} bytes The number of |bytes| to write from the buffer. If
   * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
   * if |buffer| is a C pointer.
   *
   * @return {number} The number of bytes actually written.
   */
  write: function write(buffer, options = {}) {
    // If |buffer| is a typed array and there is no |bytes| options,
    // we need to extract the |byteLength| now, as it will be lost
    // by communication.
    // Options might be a nullish value, so better check for that before using
    // the |in| operator.
    if (isTypedArray(buffer) && !(options && "bytes" in options)) {
      // Preserve reference to option |outExecutionDuration|, if it is passed.
      options = clone(options, ["outExecutionDuration"]);
      options.bytes = buffer.byteLength;
    }
    return Scheduler.post("File_prototype_write",
      [this._fdmsg,
       Type.void_t.in_ptr.toMsg(buffer),
       options],
       buffer/*Ensure that |buffer| is not gc-ed*/);
  },

  /**
   * Read bytes from this file to a new buffer.
   *
   * @param {number=} bytes If unspecified, read all the remaining bytes from
   * this file. If specified, read |bytes| bytes, or less if the file does not
   * contain that many bytes.
   * @param {JSON} options
   * @return {promise}
   * @resolves {Uint8Array} An array containing the bytes read.
   */
  read: function read(nbytes, options = {}) {
    let promise = Scheduler.post("File_prototype_read",
      [this._fdmsg,
       nbytes, options]);
    return promise.then(
      function onSuccess(data) {
        return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
      });
  },

  /**
   * Return the current position in the file, as bytes.
   *
   * @return {promise}
   * @resolves {number} The current position in the file,
   * as a number of bytes since the start of the file.
   */
  getPosition: function getPosition() {
    return Scheduler.post("File_prototype_getPosition",
      [this._fdmsg]);
  },

  /**
   * Set the current position in the file, as bytes.
   *
   * @param {number} pos A number of bytes.
   * @param {number} whence The reference position in the file,
   * which may be either POS_START (from the start of the file),
   * POS_END (from the end of the file) or POS_CUR (from the
   * current position in the file).
   *
   * @return {promise}
   */
  setPosition: function setPosition(pos, whence) {
    return Scheduler.post("File_prototype_setPosition",
      [this._fdmsg, pos, whence]);
  },

  /**
   * Flushes the file's buffers and causes all buffered data
   * to be written.
   * Disk flushes are very expensive and therefore should be used carefully,
   * sparingly and only in scenarios where it is vital that data survives
   * system crashes. Even though the function will be executed off the
   * main-thread, it might still affect the overall performance of any running
   * application.
   *
   * @return {promise}
   */
  flush: function flush() {
    return Scheduler.post("File_prototype_flush",
      [this._fdmsg]);
  },

  /**
   * Set the file's access permissions.  This does nothing on Windows.
   *
   * This operation is likely to fail if applied to a file that was
   * not created by the currently running program (more precisely,
   * if it was created by a program running under a different OS-level
   * user account).  It may also fail, or silently do nothing, if the
   * filesystem containing the file does not support access permissions.
   *
   * @param {*=} options Object specifying the requested permissions:
   *
   * - {number} unixMode The POSIX file mode to set on the file.  If omitted,
   *  the POSIX file mode is reset to the default used by |OS.file.open|.  If
   *  specified, the permissions will respect the process umask as if they
   *  had been specified as arguments of |OS.File.open|, unless the
   *  |unixHonorUmask| parameter tells otherwise.
   * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
   *  modified by the process umask, as |OS.File.open| would have done.  If
   *  false, the exact value of |unixMode| will be applied.
   */
  setPermissions: function setPermissions(options = {}) {
    return Scheduler.post("File_prototype_setPermissions",
                          [this._fdmsg, options]);
  }
};


if (SharedAll.Constants.Sys.Name != "Android" && SharedAll.Constants.Sys.Name != "Gonk") {
   /**
   * Set the last access and modification date of the file.
   * The time stamp resolution is 1 second at best, but might be worse
   * depending on the platform.
   *
   * WARNING: This method is not implemented on Android/B2G. On Android/B2G,
   * you should use File.setDates instead.
   *
   * @return {promise}
   * @rejects {TypeError}
   * @rejects {OS.File.Error}
   */
  File.prototype.setDates = function(accessDate, modificationDate) {
    return Scheduler.post("File_prototype_setDates",
      [this._fdmsg, accessDate, modificationDate], this);
  };
}


/**
 * Open a file asynchronously.
 *
 * @return {promise}
 * @resolves {OS.File}
 * @rejects {OS.Error}
 */
File.open = function open(path, mode, options) {
  return Scheduler.post(
    "open", [Type.path.toMsg(path), mode, options],
    path
  ).then(
    function onSuccess(msg) {
      return new File(msg);
    }
  );
};

/**
 * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name.
 *
 * @param {string} path The path to the file.
 * @param {*=} options Additional options for file opening. This
 * implementation interprets the following fields:
 *
 * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
 *  If |false| use HEX numbers ie: filename-A65BC0.ext
 * - {number} maxReadableNumber Used to limit the amount of tries after a failed
 *  file creation. Default is 20.
 *
 * @return {Object} contains A file object{file} and the path{path}.
 * @throws {OS.File.Error} If the file could not be opened.
 */
File.openUnique = function openUnique(path, options) {
  return Scheduler.post(
      "openUnique", [Type.path.toMsg(path), options],
      path
    ).then(
    function onSuccess(msg) {
      return {
        path: msg.path,
        file: new File(msg.file)
      };
    }
  );
};

/**
 * Get the information on the file.
 *
 * @return {promise}
 * @resolves {OS.File.Info}
 * @rejects {OS.Error}
 */
File.stat = function stat(path, options) {
  return Scheduler.post(
    "stat", [Type.path.toMsg(path), options],
    path).then(File.Info.fromMsg);
};


/**
 * Set the last access and modification date of the file.
 * The time stamp resolution is 1 second at best, but might be worse
 * depending on the platform.
 *
 * @return {promise}
 * @rejects {TypeError}
 * @rejects {OS.File.Error}
 */
File.setDates = function setDates(path, accessDate, modificationDate) {
  return Scheduler.post("setDates",
                        [Type.path.toMsg(path), accessDate, modificationDate],
                        this);
};

/**
 * Set the file's access permissions.  This does nothing on Windows.
 *
 * This operation is likely to fail if applied to a file that was
 * not created by the currently running program (more precisely,
 * if it was created by a program running under a different OS-level
 * user account).  It may also fail, or silently do nothing, if the
 * filesystem containing the file does not support access permissions.
 *
 * @param {string} path The path to the file.
 * @param {*=} options Object specifying the requested permissions:
 *
 * - {number} unixMode The POSIX file mode to set on the file.  If omitted,
 *  the POSIX file mode is reset to the default used by |OS.file.open|.  If
 *  specified, the permissions will respect the process umask as if they
 *  had been specified as arguments of |OS.File.open|, unless the
 *  |unixHonorUmask| parameter tells otherwise.
 * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
 *  modified by the process umask, as |OS.File.open| would have done.  If
 *  false, the exact value of |unixMode| will be applied.
 */
File.setPermissions = function setPermissions(path, options = {}) {
  return Scheduler.post("setPermissions",
                        [Type.path.toMsg(path), options]);
};

/**
 * Fetch the current directory
 *
 * @return {promise}
 * @resolves {string} The current directory, as a path usable with OS.Path
 * @rejects {OS.Error}
 */
File.getCurrentDirectory = function getCurrentDirectory() {
  return Scheduler.post(
    "getCurrentDirectory"
  ).then(Type.path.fromMsg);
};

/**
 * Change the current directory
 *
 * @param {string} path The OS-specific path to the current directory.
 * You should use the methods of OS.Path and the constants of OS.Constants.Path
 * to build OS-specific paths in a portable manner.
 *
 * @return {promise}
 * @resolves {null}
 * @rejects {OS.Error}
 */
File.setCurrentDirectory = function setCurrentDirectory(path) {
  return Scheduler.post(
    "setCurrentDirectory", [Type.path.toMsg(path)], path
  );
};

/**
 * Copy a file to a destination.
 *
 * @param {string} sourcePath The platform-specific path at which
 * the file may currently be found.
 * @param {string} destPath The platform-specific path at which the
 * file should be copied.
 * @param {*=} options An object which may contain the following fields:
 *
 * @option {bool} noOverwrite - If true, this function will fail if
 * a file already exists at |destPath|. Otherwise, if this file exists,
 * it will be erased silently.
 *
 * @rejects {OS.File.Error} In case of any error.
 *
 * General note: The behavior of this function is defined only when
 * it is called on a single file. If it is called on a directory, the
 * behavior is undefined and may not be the same across all platforms.
 *
 * General note: The behavior of this function with respect to metadata
 * is unspecified. Metadata may or may not be copied with the file. The
 * behavior may not be the same across all platforms.
*/
File.copy = function copy(sourcePath, destPath, options) {
  return Scheduler.post("copy", [Type.path.toMsg(sourcePath),
    Type.path.toMsg(destPath), options], [sourcePath, destPath]);
};

/**
 * Move a file to a destination.
 *
 * @param {string} sourcePath The platform-specific path at which
 * the file may currently be found.
 * @param {string} destPath The platform-specific path at which the
 * file should be moved.
 * @param {*=} options An object which may contain the following fields:
 *
 * @option {bool} noOverwrite - If set, this function will fail if
 * a file already exists at |destPath|. Otherwise, if this file exists,
 * it will be erased silently.
 *
 * @returns {Promise}
 * @rejects {OS.File.Error} In case of any error.
 *
 * General note: The behavior of this function is defined only when
 * it is called on a single file. If it is called on a directory, the
 * behavior is undefined and may not be the same across all platforms.
 *
 * General note: The behavior of this function with respect to metadata
 * is unspecified. Metadata may or may not be moved with the file. The
 * behavior may not be the same across all platforms.
 */
File.move = function move(sourcePath, destPath, options) {
  return Scheduler.post("move", [Type.path.toMsg(sourcePath),
    Type.path.toMsg(destPath), options], [sourcePath, destPath]);
};

/**
 * Create a symbolic link to a source.
 *
 * @param {string} sourcePath The platform-specific path to which
 * the symbolic link should point.
 * @param {string} destPath The platform-specific path at which the
 * symbolic link should be created.
 *
 * @returns {Promise}
 * @rejects {OS.File.Error} In case of any error.
 */
if (!SharedAll.Constants.Win) {
  File.unixSymLink = function unixSymLink(sourcePath, destPath) {
    return Scheduler.post("unixSymLink", [Type.path.toMsg(sourcePath),
      Type.path.toMsg(destPath)], [sourcePath, destPath]);
  };
}

/**
 * Gets the number of bytes available on disk to the current user.
 *
 * @param {string} Platform-specific path to a directory on the disk to
 * query for free available bytes.
 *
 * @return {number} The number of bytes available for the current user.
 * @throws {OS.File.Error} In case of any error.
 */
File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) {
  return Scheduler.post("getAvailableFreeSpace",
    [Type.path.toMsg(sourcePath)], sourcePath
  ).then(Type.uint64_t.fromMsg);
};

/**
 * Remove an empty directory.
 *
 * @param {string} path The name of the directory to remove.
 * @param {*=} options Additional options.
 *   - {bool} ignoreAbsent If |true|, do not fail if the
 *     directory does not exist yet.
 */
File.removeEmptyDir = function removeEmptyDir(path, options) {
  return Scheduler.post("removeEmptyDir",
    [Type.path.toMsg(path), options], path);
};

/**
 * Remove an existing file.
 *
 * @param {string} path The name of the file.
 * @param {*=} options Additional options.
 *   - {bool} ignoreAbsent If |false|, throw an error if the file does
 *     not exist. |true| by default.
 *
 * @throws {OS.File.Error} In case of I/O error.
 */
File.remove = function remove(path, options) {
  return Scheduler.post("remove",
    [Type.path.toMsg(path), options], path);
};



/**
 * Create a directory and, optionally, its parent directories.
 *
 * @param {string} path The name of the directory.
 * @param {*=} options Additional options.
 *
 * - {string} from If specified, the call to |makeDir| creates all the
 * ancestors of |path| that are descendants of |from|. Note that |path|
 * must be a descendant of |from|, and that |from| and its existing
 * subdirectories present in |path|  must be user-writeable.
 * Example:
 *   makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
 *  creates directories profileDir/foo, profileDir/foo/bar
 * - {bool} ignoreExisting If |false|, throw an error if the directory
 * already exists. |true| by default. Ignored if |from| is specified.
 * - {number} unixMode Under Unix, if specified, a file creation mode,
 * as per libc function |mkdir|. If unspecified, dirs are
 * created with a default mode of 0700 (dir is private to
 * the user, the user can read, write and execute). Ignored under Windows
 * or if the file system does not support file creation modes.
 * - {C pointer} winSecurity Under Windows, if specified, security
 * attributes as per winapi function |CreateDirectory|. If
 * unspecified, use the default security descriptor, inherited from
 * the parent directory. Ignored under Unix or if the file system
 * does not support security descriptors.
 */
File.makeDir = function makeDir(path, options) {
  return Scheduler.post("makeDir",
    [Type.path.toMsg(path), options], path);
};

/**
 * Return the contents of a file
 *
 * @param {string} path The path to the file.
 * @param {number=} bytes Optionally, an upper bound to the number of bytes
 * to read. DEPRECATED - please use options.bytes instead.
 * @param {JSON} options Additional options.
 * - {boolean} sequential A flag that triggers a population of the page cache
 * with data from a file so that subsequent reads from that file would not
 * block on disk I/O. If |true| or unspecified, inform the system that the
 * contents of the file will be read in order. Otherwise, make no such
 * assumption. |true| by default.
 * - {number} bytes An upper bound to the number of bytes to read.
 * - {string} compression If "lz4" and if the file is compressed using the lz4
 * compression algorithm, decompress the file contents on the fly.
 *
 * @resolves {Uint8Array} A buffer holding the bytes
 * read from the file.
 */
File.read = function read(path, bytes, options = {}) {
  if (typeof bytes == "object") {
    // Passing |bytes| as an argument is deprecated.
    // We should now be passing it as a field of |options|.
    options = bytes || {};
  } else {
    options = clone(options, ["outExecutionDuration"]);
    if (typeof bytes != "undefined") {
      options.bytes = bytes;
    }
  }

  if (options.compression || !nativeWheneverAvailable) {
    // We need to use the JS implementation.
    let promise = Scheduler.post("read",
      [Type.path.toMsg(path), bytes, options], path);
    return promise.then(
      function onSuccess(data) {
        if (typeof data == "string") {
          return data;
        }
        return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
      });
  }

  // Otherwise, use the native implementation.
  return Scheduler.push(() => Native.read(path, options));
};

/**
 * Find outs if a file exists.
 *
 * @param {string} path The path to the file.
 *
 * @return {bool} true if the file exists, false otherwise.
 */
File.exists = function exists(path) {
  return Scheduler.post("exists",
    [Type.path.toMsg(path)], path);
};

/**
 * Write a file, atomically.
 *
 * By opposition to a regular |write|, this operation ensures that,
 * until the contents are fully written, the destination file is
 * not modified.
 *
 * Limitation: In a few extreme cases (hardware failure during the
 * write, user unplugging disk during the write, etc.), data may be
 * corrupted. If your data is user-critical (e.g. preferences,
 * application data, etc.), you may wish to consider adding options
 * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
 * detailed below. Note that no combination of options can be
 * guaranteed to totally eliminate the risk of corruption.
 *
 * @param {string} path The path of the file to modify.
 * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
 * @param {*=} options Optionally, an object determining the behavior
 * of this function. This object may contain the following fields:
 * - {number} bytes The number of bytes to write. If unspecified,
 * |buffer.byteLength|. Required if |buffer| is a C pointer.
 * - {string} tmpPath If |null| or unspecified, write all data directly
 * to |path|. If specified, write all data to a temporary file called
 * |tmpPath| and, once this write is complete, rename the file to
 * replace |path|. Performing this additional operation is a little
 * slower but also a little safer.
 * - {bool} noOverwrite - If set, this function will fail if a file already
 * exists at |path|.
 * - {bool} flush - If |false| or unspecified, return immediately once the
 * write is complete. If |true|, before writing, force the operating system
 * to write its internal disk buffers to the disk. This is considerably slower
 * (not just for the application but for the whole system) but also safer:
 * if the system shuts down improperly (typically due to a kernel freeze
 * or a power failure) or if the device is disconnected before the buffer
 * is flushed, the file has more chances of not being corrupted.
 * - {string} backupTo - If specified, backup the destination file as |backupTo|.
 * Note that this function renames the destination file before overwriting it.
 * If the process or the operating system freezes or crashes
 * during the short window between these operations,
 * the destination file will have been moved to its backup.
 *
 * @return {promise}
 * @resolves {number} The number of bytes actually written.
 */
File.writeAtomic = function writeAtomic(path, buffer, options = {}) {
  // Copy |options| to avoid modifying the original object but preserve the
  // reference to |outExecutionDuration| option if it is passed.
  options = clone(options, ["outExecutionDuration"]);
  // As options.tmpPath is a path, we need to encode it as |Type.path| message
  if ("tmpPath" in options) {
    options.tmpPath = Type.path.toMsg(options.tmpPath);
  };
  if (isTypedArray(buffer) && (!("bytes" in options))) {
    options.bytes = buffer.byteLength;
  };
  let refObj = {};
  TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj);
  let promise = Scheduler.post("writeAtomic",
    [Type.path.toMsg(path),
     Type.void_t.in_ptr.toMsg(buffer),
     options], [options, buffer, path]);
  TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj);
  return promise;
};

File.removeDir = function(path, options = {}) {
  return Scheduler.post("removeDir",
    [Type.path.toMsg(path), options], path);
};

/**
 * Information on a file, as returned by OS.File.stat or
 * OS.File.prototype.stat
 *
 * @constructor
 */
File.Info = function Info(value) {
  // Note that we can't just do this[k] = value[k] because our
  // prototype defines getters for all of these fields.
  for (let k in value) {
    if (k != "creationDate") {
      Object.defineProperty(this, k, {value: value[k]});
    }
  }
  Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]});
};
File.Info.prototype = SysAll.AbstractInfo.prototype;

// Deprecated
Object.defineProperty(File.Info.prototype, "creationDate", {
  get: function creationDate() {
    let {Deprecated} = Cu.import("resource://gre/modules/Deprecated.jsm", {});
    Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes");
    return this._deprecatedCreationDate;
  }
});

File.Info.fromMsg = function fromMsg(value) {
  return new File.Info(value);
};

/**
 * Get worker's current DEBUG flag.
 * Note: This is used for testing purposes.
 */
File.GET_DEBUG = function GET_DEBUG() {
  return Scheduler.post("GET_DEBUG");
};

/**
 * Iterate asynchronously through a directory
 *
 * @constructor
 */
var DirectoryIterator = function DirectoryIterator(path, options) {
  /**
   * Open the iterator on the worker thread
   *
   * @type {Promise}
   * @resolves {*} A message accepted by the methods of DirectoryIterator
   * in the worker thread
   * @rejects {StopIteration} If all entries have already been visited
   * or the iterator has been closed.
   */
  this.__itmsg = Scheduler.post(
    "new_DirectoryIterator", [Type.path.toMsg(path), options],
    path
  );
  this._isClosed = false;
};
DirectoryIterator.prototype = {
  iterator: function () {
    return this;
  },
  __iterator__: function () {
    return this;
  },

  // Once close() is called, _itmsg should reject with a
  // StopIteration. However, we don't want to create the promise until
  // it's needed because it might never be used. In that case, we
  // would get a warning on the console.
  get _itmsg() {
    if (!this.__itmsg) {
      this.__itmsg = Promise.reject(StopIteration);
    }
    return this.__itmsg;
  },

  /**
   * Determine whether the directory exists.
   *
   * @resolves {boolean}
   */
  exists: function exists() {
    return this._itmsg.then(
      function onSuccess(iterator) {
        return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]);
      }
    );
  },
  /**
   * Get the next entry in the directory.
   *
   * @return {Promise}
   * @resolves {OS.File.Entry}
   * @rejects {StopIteration} If all entries have already been visited.
   */
  next: function next() {
    let self = this;
    let promise = this._itmsg;

    // Get the iterator, call _next
    promise = promise.then(
      function withIterator(iterator) {
        return self._next(iterator);
      });

    return promise;
  },
  /**
   * Get several entries at once.
   *
   * @param {number=} length If specified, the number of entries
   * to return. If unspecified, return all remaining entries.
   * @return {Promise}
   * @resolves {Array} An array containing the |length| next entries.
   */
  nextBatch: function nextBatch(size) {
    if (this._isClosed) {
      return Promise.resolve([]);
    }
    let promise = this._itmsg;
    promise = promise.then(
      function withIterator(iterator) {
        return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]);
      });
    promise = promise.then(
      function withEntries(array) {
        return array.map(DirectoryIterator.Entry.fromMsg);
      });
    return promise;
  },
  /**
   * Apply a function to all elements of the directory sequentially.
   *
   * @param {Function} cb This function will be applied to all entries
   * of the directory. It receives as arguments
   *  - the OS.File.Entry corresponding to the entry;
   *  - the index of the entry in the enumeration;
   *  - the iterator itself - return |iterator.close()| to stop the loop.
   *
   * If the callback returns a promise, iteration waits until the
   * promise is resolved before proceeding.
   *
   * @return {Promise} A promise resolved once the loop has reached
   * its end.
   */
  forEach: function forEach(cb, options) {
    if (this._isClosed) {
      return Promise.resolve();
    }

    let self = this;
    let position = 0;
    let iterator;

    // Grab iterator
    let promise = this._itmsg.then(
      function(aIterator) {
        iterator = aIterator;
      }
    );

    // Then iterate
    let loop = function loop() {
      if (self._isClosed) {
        return Promise.resolve();
      }
      return self._next(iterator).then(
        function onSuccess(value) {
          return Promise.resolve(cb(value, position++, self)).then(loop);
        },
        function onFailure(reason) {
          if (reason == StopIteration) {
            return;
          }
          throw reason;
        }
      );
    };

    return promise.then(loop);
  },
  /**
   * Auxiliary method: fetch the next item
   *
   * @rejects {StopIteration} If all entries have already been visited
   * or the iterator has been closed.
   */
  _next: function _next(iterator) {
    if (this._isClosed) {
      return this._itmsg;
    }
    let self = this;
    let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
    promise = promise.then(
      DirectoryIterator.Entry.fromMsg,
      function onReject(reason) {
        if (reason == StopIteration) {
          self.close();
          throw StopIteration;
        }
        throw reason;
      });
    return promise;
  },
  /**
   * Close the iterator
   */
  close: function close() {
    if (this._isClosed) {
      return Promise.resolve();
    }
    this._isClosed = true;
    let self = this;
    return this._itmsg.then(
      function withIterator(iterator) {
        // Set __itmsg to null so that the _itmsg getter returns a
        // rejected StopIteration promise if it's ever used.
        self.__itmsg = null;
        return Scheduler.post("DirectoryIterator_prototype_close", [iterator]);
      }
    );
  }
};

DirectoryIterator.Entry = function Entry(value) {
  return value;
};
DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);

DirectoryIterator.Entry.fromMsg = function fromMsg(value) {
  return new DirectoryIterator.Entry(value);
};

File.resetWorker = function() {
  return Task.spawn(function*() {
    let resources = yield Scheduler.kill({shutdown: false, reset: true});
    if (resources && !resources.killed) {
        throw new Error("Could not reset worker, this would leak file descriptors: " + JSON.stringify(resources));
    }
  });
};

// Constants
File.POS_START = SysAll.POS_START;
File.POS_CURRENT = SysAll.POS_CURRENT;
File.POS_END = SysAll.POS_END;

// Exports
File.Error = OSError;
File.DirectoryIterator = DirectoryIterator;

this.OS = {};
this.OS.File = File;
this.OS.Constants = SharedAll.Constants;
this.OS.Shared = {
  LOG: SharedAll.LOG,
  Type: SysAll.Type,
  get DEBUG() {
    return SharedAll.Config.DEBUG;
  },
  set DEBUG(x) {
    return SharedAll.Config.DEBUG = x;
  }
};
Object.freeze(this.OS.Shared);
this.OS.Path = Path;

// Returns a resolved promise when all the queued operation have been completed.
Object.defineProperty(OS.File, "queue", {
  get: function() {
    return Scheduler.queue;
  }
});

// `true` if this is a content process, `false` otherwise.
// It would be nicer to go through `Services.appInfo`, but some tests need to be
// able to replace that field with a custom implementation before it is first
// called.
const isContent = Components.classes["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;

/**
 * Shutdown barriers, to let clients register to be informed during shutdown.
 */
var Barriers = {
  shutdown: new AsyncShutdown.Barrier("OS.File: Waiting for clients before full shutdown"),
  /**
   * Return the shutdown state of OS.File
   */
  getDetails: function() {
    let result = {
      launched: Scheduler.launched,
      shutdown: Scheduler.shutdown,
      worker: !!Scheduler._worker,
      pendingReset: !!Scheduler.resetTimer,
      latestSent: Scheduler.Debugging.latestSent,
      latestReceived: Scheduler.Debugging.latestReceived,
      messagesSent: Scheduler.Debugging.messagesSent,
      messagesReceived: Scheduler.Debugging.messagesReceived,
      messagesQueued: Scheduler.Debugging.messagesQueued,
      DEBUG: SharedAll.Config.DEBUG,
    };
    // Convert dates to strings for better readability
    for (let key of ["latestSent", "latestReceived"]) {
      if (result[key] && typeof result[key][0] == "number") {
        result[key][0] = Date(result[key][0]);
      }
    }
    return result;
  }
};

function setupShutdown(phaseName) {
  Barriers[phaseName] = new AsyncShutdown.Barrier(`OS.File: Waiting for clients before ${phaseName}`),
  File[phaseName] = Barriers[phaseName].client;

  // Auto-flush OS.File during `phaseName`. This ensures that any I/O
  // that has been queued *before* `phaseName` is properly completed.
  // To ensure that I/O queued *during* `phaseName` change is completed,
  // clients should register using AsyncShutdown.addBlocker.
  AsyncShutdown[phaseName].addBlocker(
    `OS.File: flush I/O queued before ${phaseName}`,
    Task.async(function*() {
      // Give clients a last chance to enqueue requests.
      yield Barriers[phaseName].wait({crashAfterMS: null});

      // Wait until all currently enqueued requests are completed.
      yield Scheduler.queue;
    }),
    () => {
      let details = Barriers.getDetails();
      details.clients = Barriers[phaseName].state;
      return details;
    }
  );
}

// profile-before-change only exists in the parent, and OS.File should
// not be used in the child process anyways.
if (!isContent) {
  setupShutdown("profileBeforeChange")
}
File.shutdown = Barriers.shutdown.client;
