import {ApiError, ErrorCode} from './api_error';
import Stats from './node_fs_stats';
import file = require('./file');
import {FileFlag, ActionType} from './file_flag';
import path = require('path');

/**
 * Interface for a filesystem. **All** BrowserFS FileSystems should implement
 * this interface.
 *
 * Below, we denote each API method as **Core**, **Supplemental**, or
 * **Optional**.
 *
 * ### Core Methods
 *
 * **Core** API methods *need* to be implemented for basic read/write
 * functionality.
 *
 * Note that read-only FileSystems can choose to not implement core methods
 * that mutate files or metadata. The default implementation will pass a
 * NOT_SUPPORTED error to the callback.
 *
 * ### Supplemental Methods
 *
 * **Supplemental** API methods do not need to be implemented by a filesystem.
 * The default implementation implements all of the supplemental API methods in
 * terms of the **core** API methods.
 *
 * Note that a file system may choose to implement supplemental methods for
 * efficiency reasons.
 *
 * The code for some supplemental methods was adapted directly from NodeJS's
 * fs.js source code.
 *
 * ### Optional Methods
 *
 * **Optional** API methods provide functionality that may not be available in
 * all filesystems. For example, all symlink/hardlink-related API methods fall
 * under this category.
 *
 * The default implementation will pass a NOT_SUPPORTED error to the callback.
 *
 * ### Argument Assumptions
 *
 * You can assume the following about arguments passed to each API method:
 *
 * * **Every path is an absolute path.** Meaning, `.`, `..`, and other items
 *   are resolved into an absolute form.
 * * **All arguments are present.** Any optional arguments at the Node API level
 *   have been passed in with their default values.
 * * **The callback will reset the stack depth.** When your filesystem calls the
 *   callback with the requested information, it will use `setImmediate` to
 *   reset the JavaScript stack depth before calling the user-supplied callback.
 * @class FileSystem
 */
export interface FileSystem {
  /**
   * **Optional**: Returns the name of the file system.
   * @method FileSystem#getName
   * @return {string}
   */
  getName(): string;
  /**
   * **Optional**: Passes the following information to the callback:
   *
   * * Total number of bytes available on this file system.
   * * number of free bytes available on this file system.
   *
   * @method FileSystem#diskSpace
   * @todo This info is not available through the Node API. Perhaps we could do a
   *   polyfill of diskspace.js, or add a new Node API function.
   * @param {string} path The path to the location that is being queried. Only
   *   useful for filesystems that support mount points.
   * @param {FileSystem~diskSpaceCallback} cb
   */
  diskSpace(p: string, cb: (total: number, free: number) => any): void;
  /**
   * **Core**: Is this filesystem read-only?
   * @method FileSystem#isReadOnly
   * @return {boolean} True if this FileSystem is inherently read-only.
   */
  isReadOnly(): boolean;
  /**
   * **Core**: Does the filesystem support optional symlink/hardlink-related
   *   commands?
   * @method FileSystem#supportsLinks
   * @return {boolean} True if the FileSystem supports the optional
   *   symlink/hardlink-related commands.
   */
  supportsLinks(): boolean;
  /**
   * **Core**: Does the filesystem support optional property-related commands?
   * @method FileSystem#supportsProps
   * @return {boolean} True if the FileSystem supports the optional
   *   property-related commands (permissions, utimes, etc).
   */
  supportsProps(): boolean;
  /**
   * **Core**: Does the filesystem support the optional synchronous interface?
   * @method FileSystem#supportsSynch
   * @return {boolean} True if the FileSystem supports synchronous operations.
   */
  supportsSynch(): boolean;
  // **CORE API METHODS**
  // File or directory operations
  /**
   * **Core**: Asynchronous rename. No arguments other than a possible exception
   * are given to the completion callback.
   * @method FileSystem#rename
   * @param {string} oldPath
   * @param {string} newPath
   * @param {FileSystem~nodeCallback} cb
   */
  rename(oldPath: string, newPath: string, cb: (err?: ApiError) => void): void;
  /**
   * **Core**: Synchronous rename.
   * @method FileSystem#renameSync
   * @param {string} oldPath
   * @param {string} newPath
   */
  renameSync(oldPath: string, newPath: string): void;
  /**
   * **Core**: Asynchronous `stat` or `lstat`.
   * @method FileSystem#stat
   * @param {string} path
   * @param {boolean} isLstat True if this is `lstat`, false if this is regular
   *   `stat`.
   * @param {FileSystem~nodeStatsCallback} cb
   */
  stat(p: string, isLstat: boolean, cb: (err: ApiError, stat?: Stats) => void): void;
  /**
   * **Core**: Synchronous `stat` or `lstat`.
   * @method FileSystem#statSync
   * @param {string} path
   * @param {boolean} isLstat True if this is `lstat`, false if this is regular
   *   `stat`.
   * @return {BrowserFS.node.fs.Stats}
   */
  statSync(p: string, isLstat: boolean): Stats;
  // File operations
  /**
   * **Core**: Asynchronous file open.
   * @see http://www.manpagez.com/man/2/open/
   * @method FileSystem#open
   * @param {string} path
   * @param {BrowserFS.FileMode} flags Handles the complexity of the various file
   *   modes. See its API for more details.
   * @param {number} mode Mode to use to open the file. Can be ignored if the
   *   filesystem doesn't support permissions.
   * @param {FileSystem~fileCallback} cb
   */
  open(p: string, flag:FileFlag, mode: number, cb: (err: ApiError, fd?: file.File) => any): void;
  /**
   * **Core**: Synchronous file open.
   * @see http://www.manpagez.com/man/2/open/
   * @method FileSystem#openSync
   * @param {string} path
   * @param {BrowserFS.FileMode} flags Handles the complexity of the various file
   *   modes. See its API for more details.
   * @param {number} mode Mode to use to open the file. Can be ignored if the
   *   filesystem doesn't support permissions.
   * @return {BrowserFS.File}
   */
  openSync(p: string, flag: FileFlag, mode: number): file.File;
  /**
   * **Core**: Asynchronous `unlink`.
   * @method FileSystem#unlink
   * @param [string] path
   * @param [FileSystem~nodeCallback] cb
   */
  unlink(p: string, cb: Function): void;
  /**
   * **Core**: Synchronous `unlink`.
   * @method FileSystem#unlinkSync
   * @param {string} path
   */
  unlinkSync(p: string): void;
  // Directory operations
  /**
   * **Core**: Asynchronous `rmdir`.
   * @method FileSystem#rmdir
   * @param {string} path
   * @param {FileSystem~nodeCallback} cb
   */
  rmdir(p: string, cb: Function): void;
  /**
   * **Core**: Synchronous `rmdir`.
   * @method FileSystem#rmdirSync
   * @param {string} path
   */
  rmdirSync(p: string): void;
  /**
   * **Core**: Asynchronous `mkdir`.
   * @method FileSystem#mkdir
   * @param {string} path
   * @param {number?} mode Mode to make the directory using. Can be ignored if
   *   the filesystem doesn't support permissions.
   * @param {FileSystem~nodeCallback} cb
   */
  mkdir(p: string, mode: number, cb: Function): void;
  /**
   * **Core**: Synchronous `mkdir`.
   * @method FileSystem#mkdirSync
   * @param {string} path
   * @param {number} mode Mode to make the directory using. Can be ignored if
   *   the filesystem doesn't support permissions.
   */
  mkdirSync(p: string, mode: number): void;
  /**
   * **Core**: Asynchronous `readdir`. Reads the contents of a directory.
   *
   * The callback gets two arguments `(err, files)` where `files` is an array of
   * the names of the files in the directory excluding `'.'` and `'..'`.
   * @method FileSystem#readdir
   * @param {string} path
   * @param {FileSystem~readdirCallback} cb
   */
  readdir(p: string, cb: (err: ApiError, files?: string[]) => void): void;
  /**
   * **Core**: Synchronous `readdir`. Reads the contents of a directory.
   * @method FileSystem#readdirSync
   * @param {string} path
   * @return {string[]}
   */
  readdirSync(p: string): string[];
  // **SUPPLEMENTAL INTERFACE METHODS**
  // File or directory operations
  /**
   * **Supplemental**: Test whether or not the given path exists by checking with
   * the file system. Then call the callback argument with either true or false.
   * @method FileSystem#exists
   * @param {string} path
   * @param {FileSystem~existsCallback} cb
   */
  exists(p: string, cb: (exists: boolean) => void): void;
  /**
   * **Supplemental**: Test whether or not the given path exists by checking with
   * the file system.
   * @method FileSystem#existsSync
   * @param {string} path
   * @return {boolean}
   */
  existsSync(p: string): boolean;
  /**
   * **Supplemental**: Asynchronous `realpath`. The callback gets two arguments
   * `(err, resolvedPath)`.
   *
   * Note that the Node API will resolve `path` to an absolute path.
   * @method FileSystem#realpath
   * @param {string} path
   * @param {Object} cache An object literal of mapped paths that can be used to
   *   force a specific path resolution or avoid additional `fs.stat` calls for
   *   known real paths. If not supplied by the user, it'll be an empty object.
   * @param {FileSystem~pathCallback} cb
   */
  realpath(p: string, cache: {[path: string]: string}, cb: (err: ApiError, resolvedPath?: string) => any): void;
  /**
   * **Supplemental**: Synchronous `realpath`.
   *
   * Note that the Node API will resolve `path` to an absolute path.
   * @method FileSystem#realpathSync
   * @param {string} path
   * @param {Object} cache An object literal of mapped paths that can be used to
   *   force a specific path resolution or avoid additional `fs.stat` calls for
   *   known real paths. If not supplied by the user, it'll be an empty object.
   * @return {string}
   */
  realpathSync(p: string, cache: {[path: string]: string}): string;
  // File operations
  /**
   *
   * **Supplemental**: Asynchronous `truncate`.
   * @method FileSystem#truncate
   * @param {string} path
   * @param {number} len
   * @param {FileSystem~nodeCallback} cb
   */
  truncate(p: string, len: number, cb: Function): void;
  /**
   * **Supplemental**: Synchronous `truncate`.
   * @method FileSystem#truncateSync
   * @param {string} path
   * @param {number} len
   */
  truncateSync(p: string, len: number): void;
  /**
   * **Supplemental**: Asynchronously reads the entire contents of a file.
   * @method FileSystem#readFile
   * @param {string} filename
   * @param {string} encoding If non-null, the file's contents should be decoded
   *   into a string using that encoding. Otherwise, if encoding is null, fetch
   *   the file's contents as a Buffer.
   * @param {BrowserFS.FileMode} flag
   * @param {FileSystem~readCallback} cb If no encoding is specified, then the
   *   raw buffer is returned.
   */
  readFile(fname: string, encoding: string, flag: FileFlag, cb: (err: ApiError, data?: any) => void): void;
  /**
   * **Supplemental**: Synchronously reads the entire contents of a file.
   * @method FileSystem#readFileSync
   * @param {string} filename
   * @param {string} encoding If non-null, the file's contents should be decoded
   *   into a string using that encoding. Otherwise, if encoding is null, fetch
   *   the file's contents as a Buffer.
   * @param {BrowserFS.FileMode} flag
   * @return {(string|BrowserFS.Buffer)}
   */
  readFileSync(fname: string, encoding: string, flag: FileFlag): any;
  /**
   * **Supplemental**: Asynchronously writes data to a file, replacing the file
   * if it already exists.
   *
   * The encoding option is ignored if data is a buffer.
   * @method FileSystem#writeFile
   * @param {string} filename
   * @param {(string | BrowserFS.node.Buffer)} data
   * @param {string} encoding
   * @param {BrowserFS.FileMode} flag
   * @param {number} mode
   * @param {FileSystem~nodeCallback} cb
   */
  writeFile(fname: string, data: any, encoding: string, flag: FileFlag, mode: number, cb: (err: ApiError) => void): void;
  /**
   * **Supplemental**: Synchronously writes data to a file, replacing the file
   * if it already exists.
   *
   * The encoding option is ignored if data is a buffer.
   * @method FileSystem#writeFileSync
   * @param {string} filename
   * @param {(string | BrowserFS.node.Buffer)} data
   * @param {string} encoding
   * @param {BrowserFS.FileMode} flag
   * @param {number} mode
   */
  writeFileSync(fname: string, data: any, encoding: string, flag: FileFlag, mode: number): void;
  /**
   * **Supplemental**: Asynchronously append data to a file, creating the file if
   * it not yet exists.
   * @method FileSystem#appendFile
   * @param {string} filename
   * @param {(string | BrowserFS.node.Buffer)} data
   * @param {string} encoding
   * @param {BrowserFS.FileMode} flag
   * @param {number} mode
   * @param {FileSystem~nodeCallback} cb
   */
  appendFile(fname: string, data: any, encoding: string, flag: FileFlag, mode: number, cb: (err: ApiError) => void): void;
  /**
   * **Supplemental**: Synchronously append data to a file, creating the file if
   * it not yet exists.
   * @method FileSystem#appendFileSync
   * @param {string} filename
   * @param {(string | BrowserFS.node.Buffer)} data
   * @param {string} encoding
   * @param {BrowserFS.FileMode} flag
   * @param {number} mode
   */
  appendFileSync(fname: string, data: any, encoding: string, flag: FileFlag, mode: number): void;
  // **OPTIONAL INTERFACE METHODS**
  // Property operations
  // This isn't always possible on some filesystem types (e.g. Dropbox).
  /**
   * **Optional**: Asynchronous `chmod` or `lchmod`.
   * @method FileSystem#chmod
   * @param {string} path
   * @param {boolean} isLchmod `True` if `lchmod`, false if `chmod`. Has no
   *   bearing on result if links aren't supported.
   * @param {number} mode
   * @param {FileSystem~nodeCallback} cb
   */
  chmod(p: string, isLchmod: boolean, mode: number, cb: Function): void;
  /**
   * **Optional**: Synchronous `chmod` or `lchmod`.
   * @method FileSystem#chmodSync
   * @param {string} path
   * @param {boolean} isLchmod `True` if `lchmod`, false if `chmod`. Has no
   *   bearing on result if links aren't supported.
   * @param {number} mode
   */
  chmodSync(p: string, isLchmod: boolean, mode: number): void;
  /**
   * **Optional**: Asynchronous `chown` or `lchown`.
   * @method FileSystem#chown
   * @param {string} path
   * @param {boolean} isLchown `True` if `lchown`, false if `chown`. Has no
   *   bearing on result if links aren't supported.
   * @param {number} uid
   * @param {number} gid
   * @param {FileSystem~nodeCallback} cb
   */
  chown(p: string, isLchown: boolean, uid: number, gid: number, cb: Function): void;
  /**
   * **Optional**: Synchronous `chown` or `lchown`.
   * @method FileSystem#chownSync
   * @param {string} path
   * @param {boolean} isLchown `True` if `lchown`, false if `chown`. Has no
   *   bearing on result if links aren't supported.
   * @param {number} uid
   * @param {number} gid
   */
  chownSync(p: string, isLchown: boolean, uid: number, gid: number): void;
  /**
   * **Optional**: Change file timestamps of the file referenced by the supplied
   * path.
   * @method FileSystem#utimes
   * @param {string} path
   * @param {Date} atime
   * @param {Date} mtime
   * @param {FileSystem~nodeCallback} cb
   */
  utimes(p: string, atime: Date, mtime: Date, cb: Function): void;
  /**
   * **Optional**: Change file timestamps of the file referenced by the supplied
   * path.
   * @method FileSystem#utimesSync
   * @param {string} path
   * @param {Date} atime
   * @param {Date} mtime
   */
  utimesSync(p: string, atime: Date, mtime: Date): void;
  // Symlink operations
  // Symlinks aren't always supported.
  /**
   * **Optional**: Asynchronous `link`.
   * @method FileSystem#link
   * @param {string} srcpath
   * @param {string} dstpath
   * @param {FileSystem~nodeCallback} cb
   */
  link(srcpath: string, dstpath: string, cb: Function): void;
  /**
   * **Optional**: Synchronous `link`.
   * @method FileSystem#linkSync
   * @param {string} srcpath
   * @param {string} dstpath
   */
  linkSync(srcpath: string, dstpath: string): void;
  /**
   * **Optional**: Asynchronous `symlink`.
   * @method FileSystem#symlink
   * @param {string} srcpath
   * @param {string} dstpath
   * @param {string} type can be either `'dir'` or `'file'`
   * @param {FileSystem~nodeCallback} cb
   */
  symlink(srcpath: string, dstpath: string, type: string, cb: Function): void;
  /**
   * **Optional**: Synchronous `symlink`.
   * @method FileSystem#symlinkSync
   * @param {string} srcpath
   * @param {string} dstpath
   * @param {string} type can be either `'dir'` or `'file'`
   */
  symlinkSync(srcpath: string, dstpath: string, type: string): void;
  /**
   * **Optional**: Asynchronous readlink.
   * @method FileSystem#readlink
   * @param {string} path
   * @param {FileSystem~pathCallback} callback
   */
  readlink(p: string, cb: Function): void;
  /**
   * **Optional**: Synchronous readlink.
   * @method FileSystem#readlinkSync
   * @param {string} path
   */
  readlinkSync(p: string): string;
}

/**
 * Contains typings for static functions on the file system constructor.
 */
export interface FileSystemConstructor {
  /**
   * **Core**: Returns 'true' if this filesystem is available in the current
   * environment. For example, a `localStorage`-backed filesystem will return
   * 'false' if the browser does not support that API.
   *
   * Defaults to 'false', as the FileSystem base class isn't usable alone.
   * @method FileSystem.isAvailable
   * @return {boolean}
   */
  isAvailable(): boolean;
}

/**
 * Basic filesystem class. Most filesystems should extend this class, as it
 * provides default implementations for a handful of methods.
 */
export class BaseFileSystem {
  public supportsLinks(): boolean {
    return false;
  }
  public diskSpace(p: string, cb: (total: number, free: number) => any): void {
    cb(0, 0);
  }
  /**
   * Opens the file at path p with the given flag. The file must exist.
   * @param p The path to open.
   * @param flag The flag to use when opening the file.
   */
  public openFile(p: string, flag: FileFlag, cb: (e: ApiError, file?: file.File) => void): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  /**
   * Create the file at path p with the given mode. Then, open it with the given
   * flag.
   */
  public createFile(p: string, flag: FileFlag, mode: number, cb: (e: ApiError, file?: file.File) => void): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public open(p: string, flag:FileFlag, mode: number, cb: (err: ApiError, fd?: file.BaseFile) => any): void {
    var must_be_file = (e: ApiError, stats?: Stats): void => {
      if (e) {
        // File does not exist.
        switch (flag.pathNotExistsAction()) {
          case ActionType.CREATE_FILE:
            // Ensure parent exists.
            return this.stat(path.dirname(p), false, (e: ApiError, parentStats?: Stats) => {
              if (e) {
                cb(e);
              } else if (!parentStats.isDirectory()) {
                cb(ApiError.ENOTDIR(path.dirname(p)));
              } else {
                this.createFile(p, flag, mode, cb);
              }
            });
          case ActionType.THROW_EXCEPTION:
            return cb(ApiError.ENOENT(p));
          default:
            return cb(new ApiError(ErrorCode.EINVAL, 'Invalid FileFlag object.'));
        }
      } else {
        // File exists.
        if (stats.isDirectory()) {
          return cb(ApiError.EISDIR(p));
        }
        switch (flag.pathExistsAction()) {
          case ActionType.THROW_EXCEPTION:
            return cb(ApiError.EEXIST(p));
          case ActionType.TRUNCATE_FILE:
            // NOTE: In a previous implementation, we deleted the file and
            // re-created it. However, this created a race condition if another
            // asynchronous request was trying to read the file, as the file
            // would not exist for a small period of time.
            return this.openFile(p, flag, (e: ApiError, fd?: file.File): void => {
              if (e) {
                cb(e);
              } else {
                fd.truncate(0, () => {
                  fd.sync(() => {
                    cb(null, fd);
                  });
                });
              }
            });
          case ActionType.NOP:
            return this.openFile(p, flag, cb);
          default:
            return cb(new ApiError(ErrorCode.EINVAL, 'Invalid FileFlag object.'));
        }
      }
    };
    this.stat(p, false, must_be_file);
  }
  public rename(oldPath: string, newPath: string, cb: (err?: ApiError) => void): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public renameSync(oldPath: string, newPath: string): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public stat(p: string, isLstat: boolean, cb: (err: ApiError, stat?: Stats) => void): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public statSync(p: string, isLstat: boolean): Stats {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  /**
   * Opens the file at path p with the given flag. The file must exist.
   * @param p The path to open.
   * @param flag The flag to use when opening the file.
   * @return A File object corresponding to the opened file.
   */
  public openFileSync(p: string, flag: FileFlag, mode: number): file.File {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  /**
   * Create the file at path p with the given mode. Then, open it with the given
   * flag.
   */
  public createFileSync(p: string, flag: FileFlag, mode: number): file.File {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public openSync(p: string, flag: FileFlag, mode: number): file.File {
    // Check if the path exists, and is a file.
    var stats: Stats;
    try {
      stats = this.statSync(p, false);
    } catch (e) {
      // File does not exist.
      switch (flag.pathNotExistsAction()) {
        case ActionType.CREATE_FILE:
          // Ensure parent exists.
          var parentStats = this.statSync(path.dirname(p), false);
          if (!parentStats.isDirectory()) {
            throw ApiError.ENOTDIR(path.dirname(p));
          }
          return this.createFileSync(p, flag, mode);
        case ActionType.THROW_EXCEPTION:
          throw ApiError.ENOENT(p);
        default:
          throw new ApiError(ErrorCode.EINVAL, 'Invalid FileFlag object.');
      }
    }

    // File exists.
    if (stats.isDirectory()) {
      throw ApiError.EISDIR(p);
    }
    switch (flag.pathExistsAction()) {
      case ActionType.THROW_EXCEPTION:
        throw ApiError.EEXIST(p);
      case ActionType.TRUNCATE_FILE:
        // Delete file.
        this.unlinkSync(p);
        // Create file. Use the same mode as the old file.
        // Node itself modifies the ctime when this occurs, so this action
        // will preserve that behavior if the underlying file system
        // supports those properties.
        return this.createFileSync(p, flag, stats.mode);
      case ActionType.NOP:
        return this.openFileSync(p, flag, mode);
      default:
        throw new ApiError(ErrorCode.EINVAL, 'Invalid FileFlag object.');
    }
  }
  public unlink(p: string, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public unlinkSync(p: string): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public rmdir(p: string, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public rmdirSync(p: string): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public mkdir(p: string, mode: number, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public mkdirSync(p: string, mode: number): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public readdir(p: string, cb: (err: ApiError, files?: string[]) => void): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public readdirSync(p: string): string[] {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public exists(p: string, cb: (exists: boolean) => void): void {
    this.stat(p, null, function(err) {
      cb(err == null);
    });
  }
  public existsSync(p: string): boolean {
    try {
      this.statSync(p, true);
      return true;
    } catch (e) {
      return false;
    }
  }
  public realpath(p: string, cache: {[path: string]: string}, cb: (err: ApiError, resolvedPath?: string) => any): void {
    if (this.supportsLinks()) {
      // The path could contain symlinks. Split up the path,
      // resolve any symlinks, return the resolved string.
      var splitPath = p.split(path.sep);
      // TODO: Simpler to just pass through file, find sep and such.
      for (var i = 0; i < splitPath.length; i++) {
        var addPaths = splitPath.slice(0, i + 1);
        splitPath[i] = path.join.apply(null, addPaths);
      }
    } else {
      // No symlinks. We just need to verify that it exists.
      this.exists(p, function(doesExist) {
        if (doesExist) {
          cb(null, p);
        } else {
          cb(ApiError.ENOENT(p));
        }
      });
    }
  }
  public realpathSync(p: string, cache: {[path: string]: string}): string {
    if (this.supportsLinks()) {
      // The path could contain symlinks. Split up the path,
      // resolve any symlinks, return the resolved string.
      var splitPath = p.split(path.sep);
      // TODO: Simpler to just pass through file, find sep and such.
      for (var i = 0; i < splitPath.length; i++) {
        var addPaths = splitPath.slice(0, i + 1);
        splitPath[i] = path.join.apply(null, addPaths);
      }
    } else {
      // No symlinks. We just need to verify that it exists.
      if (this.existsSync(p)) {
        return p;
      } else {
        throw ApiError.ENOENT(p);
      }
    }
  }
  public truncate(p: string, len: number, cb: Function): void {
    this.open(p, FileFlag.getFileFlag('r+'), 0x1a4, (function(er: ApiError, fd?: file.File) {
      if (er) {
        return cb(er);
      }
      fd.truncate(len, (function(er: any) {
        fd.close((function(er2: any) {
          cb(er || er2);
        }));
      }));
    }));
  }
  public truncateSync(p: string, len: number): void {
    var fd = this.openSync(p, FileFlag.getFileFlag('r+'), 0x1a4);
    // Need to safely close FD, regardless of whether or not truncate succeeds.
    try {
      fd.truncateSync(len);
    } catch (e) {
      throw e;
    } finally {
      fd.closeSync();
    }
  }
  public readFile(fname: string, encoding: string, flag: FileFlag, cb: (err: ApiError, data?: any) => void): void {
    // Wrap cb in file closing code.
    var oldCb = cb;
    // Get file.
    this.open(fname, flag, 0x1a4, function(err: ApiError, fd?: file.File) {
      if (err) {
        return cb(err);
      }
      cb = function(err: ApiError, arg?: file.File) {
        fd.close(function(err2: any) {
          if (err == null) {
            err = err2;
          }
          return oldCb(err, arg);
        });
      };
      fd.stat(function(err: ApiError, stat?: Stats) {
        if (err != null) {
          return cb(err);
        }
        // Allocate buffer.
        var buf = new Buffer(stat.size);
        fd.read(buf, 0, stat.size, 0, function(err) {
          if (err != null) {
            return cb(err);
          } else if (encoding === null) {
            return cb(err, buf);
          }
          try {
            cb(null, buf.toString(encoding));
          } catch (e) {
            cb(e);
          }
        });
      });
    });
  }
  public readFileSync(fname: string, encoding: string, flag: FileFlag): any {
    // Get file.
    var fd = this.openSync(fname, flag, 0x1a4);
    try {
      var stat = fd.statSync();
      // Allocate buffer.
      var buf = new Buffer(stat.size);
      fd.readSync(buf, 0, stat.size, 0);
      fd.closeSync();
      if (encoding === null) {
        return buf;
      }
      return buf.toString(encoding);
    } finally {
      fd.closeSync();
    }
  }
  public writeFile(fname: string, data: any, encoding: string, flag: FileFlag, mode: number, cb: (err: ApiError) => void): void {
    // Wrap cb in file closing code.
    var oldCb = cb;
    // Get file.
    this.open(fname, flag, 0x1a4, function(err: ApiError, fd?:file.File) {
      if (err != null) {
        return cb(err);
      }
      cb = function(err: ApiError) {
        fd.close(function(err2: any) {
          oldCb(err != null ? err : err2);
        });
      };

      try {
        if (typeof data === 'string') {
          data = new Buffer(data, encoding);
        }
      } catch (e) {
        return cb(e);
      }
      // Write into file.
      fd.write(data, 0, data.length, 0, cb);
    });
  }
  public writeFileSync(fname: string, data: any, encoding: string, flag: FileFlag, mode: number): void {
    // Get file.
    var fd = this.openSync(fname, flag, mode);
    try {
      if (typeof data === 'string') {
        data = new Buffer(data, encoding);
      }
      // Write into file.
      fd.writeSync(data, 0, data.length, 0);
    } finally {
      fd.closeSync();
    }
  }
  public appendFile(fname: string, data: any, encoding: string, flag: FileFlag, mode: number, cb: (err: ApiError) => void): void {
    // Wrap cb in file closing code.
    var oldCb = cb;
    this.open(fname, flag, mode, function(err: ApiError, fd?: file.File) {
      if (err != null) {
        return cb(err);
      }
      cb = function(err: ApiError) {
        fd.close(function(err2: any) {
          oldCb(err != null ? err : err2);
        });
      };
      if (typeof data === 'string') {
        data = new Buffer(data, encoding);
      }
      fd.write(data, 0, data.length, null, cb);
    });
  }
  public appendFileSync(fname: string, data: any, encoding: string, flag: FileFlag, mode: number): void {
    var fd = this.openSync(fname, flag, mode);
    try {
      if (typeof data === 'string') {
        data = new Buffer(data, encoding);
      }
      fd.writeSync(data, 0, data.length, null);
    } finally {
      fd.closeSync();
    }
  }
  public chmod(p: string, isLchmod: boolean, mode: number, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public chmodSync(p: string, isLchmod: boolean, mode: number) {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public chown(p: string, isLchown: boolean, uid: number, gid: number, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public chownSync(p: string, isLchown: boolean, uid: number, gid: number): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public utimes(p: string, atime: Date, mtime: Date, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public utimesSync(p: string, atime: Date, mtime: Date): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public link(srcpath: string, dstpath: string, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public linkSync(srcpath: string, dstpath: string): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public symlink(srcpath: string, dstpath: string, type: string, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public symlinkSync(srcpath: string, dstpath: string, type: string): void {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
  public readlink(p: string, cb: Function): void {
    cb(new ApiError(ErrorCode.ENOTSUP));
  }
  public readlinkSync(p: string): string {
    throw new ApiError(ErrorCode.ENOTSUP);
  }
}

/**
 * Implements the asynchronous API in terms of the synchronous API.
 * @class SynchronousFileSystem
 */
export class SynchronousFileSystem extends BaseFileSystem {
  public supportsSynch(): boolean {
    return true;
  }

  public rename(oldPath: string, newPath: string, cb: Function): void {
    try {
      this.renameSync(oldPath, newPath);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public stat(p: string, isLstat: boolean, cb: Function): void {
    try {
      cb(null, this.statSync(p, isLstat));
    } catch (e) {
      cb(e);
    }
  }

  public open(p: string, flags: FileFlag, mode: number, cb: Function): void {
    try {
      cb(null, this.openSync(p, flags, mode));
    } catch (e) {
      cb(e);
    }
  }

  public unlink(p: string, cb: Function): void {
    try {
      this.unlinkSync(p);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public rmdir(p: string, cb: Function): void {
    try {
      this.rmdirSync(p);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public mkdir(p: string, mode: number, cb: Function): void {
    try {
      this.mkdirSync(p, mode);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public readdir(p: string, cb: Function): void {
    try {
      cb(null, this.readdirSync(p));
    } catch (e) {
      cb(e);
    }
  }

  public chmod(p: string, isLchmod: boolean, mode: number, cb: Function): void {
    try {
      this.chmodSync(p, isLchmod, mode);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public chown(p: string, isLchown: boolean, uid: number, gid: number, cb: Function): void {
    try {
      this.chownSync(p, isLchown, uid, gid);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public utimes(p: string, atime: Date, mtime: Date, cb: Function): void {
    try {
      this.utimesSync(p, atime, mtime);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public link(srcpath: string, dstpath: string, cb: Function): void {
    try {
      this.linkSync(srcpath, dstpath);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public symlink(srcpath: string, dstpath: string, type: string, cb: Function): void {
    try {
      this.symlinkSync(srcpath, dstpath, type);
      cb();
    } catch (e) {
      cb(e);
    }
  }

  public readlink(p: string, cb: Function): void {
    try {
      cb(null, this.readlinkSync(p));
    } catch (e) {
      cb(e);
    }
  }
}
