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

/**
 * Code shared by OS.File front-ends.
 *
 * This code is meant to be included by another library. It is also meant to
 * be executed only on a worker thread.
 */

if (typeof Components != "undefined") {
  throw new Error("osfile_shared_front.jsm cannot be used from the main thread");
}
(function(exports) {

let LOG = exports.OS.Shared.LOG.bind(OS.Shared, "Shared front-end");

const noOptions = {};

/**
 * Code shared by implementations of File.
 *
 * @param {*} fd An OS-specific file handle.
 * @constructor
 */
let AbstractFile = function AbstractFile(fd) {
  this._fd = fd;
};

AbstractFile.prototype = {
  /**
   * Return the file handle.
   *
   * @throw OS.File.Error if the file has been closed.
   */
  get fd() {
    if (this._fd) {
      return this._fd;
    }
    throw OS.File.Error.closed();
  },
  /**
   * 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.
   * @return {buffer: ArrayBuffer, bytes: bytes} A buffer containing the
   * bytes read and the number of bytes read. Note that |buffer| may be
   * larger than the number of bytes actually read.
   */
  readAll: function readAll(bytes) {
    if (bytes == null) {
      bytes = this.stat().size;
    }
    let buffer = new ArrayBuffer(bytes);
    let size = this.readTo(buffer, bytes);
    return {
      buffer: buffer,
      bytes: size
    };
  },

  /**
   * Read bytes from this file to an existing buffer.
   *
   * Note that, by default, this function may perform several I/O
   * operations to ensure that the buffer is as full as possible.
   *
   * @param {ArrayBuffer | C pointer} buffer The buffer in which to
   * store the bytes. If options.offset is not given, bytes are stored
   * from the start of the array. The buffer must be large enough to
   * accomodate |bytes| bytes.
   * @param {*=} options Optionally, an object that may contain the
   * following fields:
   * - {number} offset The offset in |buffer| at which to start placing
   * data
   *
   * @return {number} The number of bytes actually read, which may be
   * less than |bytes| if the file did not contain that many bytes left
   * or if |options.once| was set.
   */
  readTo: function readTo(buffer, bytes, options) {
    options = options || noOptions;

    let pointer = AbstractFile.normalizeToPointer(buffer, bytes,
      options.offset);
    let pos = 0;
    while (pos < bytes) {
      let chunkSize = this.read(pointer, bytes - pos, options);
      if (chunkSize == 0) {
        break;
      }
      pos += chunkSize;
      pointer = exports.OS.Shared.offsetBy(pointer, chunkSize);
    }

    return pos;
  },

  /**
   * 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 {ArrayBuffer | C pointer} buffer The buffer in which the
   * the bytes are stored. If options.offset is not given, bytes are stored
   * from the start of the array. The buffer must be large enough to
   * accomodate |bytes| bytes.
   * @param {*=} options Optionally, an object that may contain the
   * following fields:
   * - {number} offset The offset in |buffer| at which to start extracting
   * data
   *
   * @return {number} The number of bytes actually written, which may be
   * less than |bytes| if |options.once| was set.
   */
  writeFrom: function writeFrom(buffer, bytes, options) {
    options = options || noOptions;

    let pointer = AbstractFile.normalizeToPointer(buffer, bytes,
      options.offset);

    let pos = 0;
    while (pos < bytes) {
      let chunkSize = this.write(pointer, bytes - pos, options);
      pos += chunkSize;
      pointer = exports.OS.Shared.offsetBy(pointer, chunkSize);
    }
    return pos;
  }
};

/**
 * Utility function used to normalize a ArrayBuffer or C pointer into a uint8_t
 * C pointer.
 *
 * Future versions might extend this to other data structures.
 *
 * @param {ArrayBuffer|C pointer} candidate Either an ArrayBuffer or a
 * non-null C pointer.
 * @param {number} bytes The number of bytes that |candidate| should contain.
 * Used for sanity checking if the size of |candidate| can be determined.
 * @param {number=} offset Optionally, a number of bytes by which to shift
 * |candidate|.
 *
 * @return {C pointer} A C pointer of type uint8_t, corresponding to the
 * start of |candidate| + |offset| bytes.
 */
AbstractFile.normalizeToPointer = function normalizeToPointer(candidate, bytes, offset) {
  if (!candidate) {
    throw new TypeError("Expecting a C pointer or an ArrayBuffer");
  }
  if (offset == null) {
    offset = 0;
  }
  let ptr;
  if ("isNull" in candidate) {
    if (candidate.isNull()) {
      throw new TypeError("Expecting a non-null pointer");
    }
    ptr = exports.OS.Shared.Type.uint8_t.out_ptr.cast(candidate);
  } else if ("byteLength" in candidate) {
    ptr = exports.OS.Shared.Type.uint8_t.out_ptr.implementation(candidate);
    if (candidate.byteLength < offset + bytes) {
      throw new TypeError("Buffer is too short. I need at least " +
                         (offset + bytes) +
                         " bytes but I have only " +
                         buffer.byteLength +
                          "bytes");
    }
  } else {
    throw new TypeError("Expecting a C pointer or an ArrayBuffer");
  }
  if (offset != 0) {
    ptr = exports.OS.Shared.offsetBy(ptr, offset);
  }
  return ptr;
};

/**
 * Utility function shared by implementations of |OS.File.open|:
 * extract read/write/trunc/create/existing flags from a |mode|
 * object.
 *
 * @param {*=} mode An object that may contain fields |read|,
 * |write|, |truncate|, |create|, |existing|. These fields
 * are interpreted only if true-ish.
 * @return {{read:bool, write:bool, trunc:bool, create:bool,
 * existing:bool}} an object recapitulating the options set
 * by |mode|.
 * @throws {TypeError} If |mode| contains other fields, or
 * if it contains both |create| and |truncate|, or |create|
 * and |existing|.
 */
AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) {
  let result = {
    read: false,
    write: false,
    trunc: false,
    create: false,
    existing: false
  };
  for (let key in mode) {
    if (!mode[key]) continue; // Only interpret true-ish keys
    switch (key) {
    case "read":
      result.read = true;
      break;
    case "write":
      result.write = true;
      break;
    case "truncate": // fallthrough
    case "trunc":
      result.trunc = true;
      result.write = true;
      break;
    case "create":
      result.create = true;
      result.write = true;
      break;
    case "existing": // fallthrough
    case "exist":
      result.existing = true;
      break;
    default:
      throw new TypeError("Mode " + key + " not understood");
    }
  }
  // Reject opposite modes
  if (result.existing && result.create) {
    throw new TypeError("Cannot specify both existing:true and create:true");
  }
  if (result.trunc && result.create) {
    throw new TypeError("Cannot specify both trunc:true and create:true");
  }
  // Handle read/write
  if (!result.write) {
    result.read = true;
  }
  return result;
};

   exports.OS.Shared.AbstractFile = AbstractFile;
})(this);