var Path = require('../path.js');
var normalize = Path.normalize;
var dirname = Path.dirname;
var basename = Path.basename;
var isAbsolutePath = Path.isAbsolute;
var shared = require('../shared.js');
var async = require('../../lib/async.js');

var Constants = require('../constants.js');
var NODE_TYPE_FILE = Constants.NODE_TYPE_FILE;
var NODE_TYPE_DIRECTORY = Constants.NODE_TYPE_DIRECTORY;
var NODE_TYPE_SYMBOLIC_LINK = Constants.NODE_TYPE_SYMBOLIC_LINK;
var NODE_TYPE_META = Constants.NODE_TYPE_META;

var FULL_READ_WRITE_EXEC_PERMISSIONS = Constants.FULL_READ_WRITE_EXEC_PERMISSIONS;

var ROOT_DIRECTORY_NAME = Constants.ROOT_DIRECTORY_NAME;
var SUPER_NODE_ID = Constants.SUPER_NODE_ID;
var SYMLOOP_MAX = Constants.SYMLOOP_MAX;

var O_READ = Constants.O_READ;
var O_WRITE = Constants.O_WRITE;
var O_CREATE = Constants.O_CREATE;
var O_EXCLUSIVE = Constants.O_EXCLUSIVE;
var O_APPEND = Constants.O_APPEND;
var O_FLAGS = Constants.O_FLAGS;

var XATTR_CREATE = Constants.XATTR_CREATE;
var XATTR_REPLACE = Constants.XATTR_REPLACE;
var FS_NOMTIME = Constants.FS_NOMTIME;
var FS_NOCTIME = Constants.FS_NOCTIME;

var Errors = require('../errors.js');
var DirectoryEntry = require('../directory-entry.js');
var openFiles = require('../open-files.js');
var OpenFileDescription = require('../open-file-description.js');
var SuperNode = require('../super-node.js');
var Node = require('../node.js');
var Dirent = require('../dirent.js');
var Stats = require('../stats.js');

/**
 * Update node times. Only passed times are modified (undefined times are ignored)
 * and filesystem flags are examined in order to override update logic.
 */
function update_node_times(context, path, node, times, callback) {
  // Honour mount flags for how we update times
  var flags = context.flags;
  if(flags.includes(FS_NOCTIME)) {
    delete times.ctime;
  }
  if(flags.includes(FS_NOMTIME)) {
    delete times.mtime;
  }

  // Only do the update if required (i.e., times are still present)
  var update = false;
  if(times.ctime) {
    node.ctime = times.ctime;
    // We don't do atime tracking for perf reasons, but do mirror ctime
    node.atime = times.ctime;
    update = true;
  }
  if(times.atime) {
    // The only time we explicitly pass atime is when utimes(), futimes() is called.
    // Override ctime mirror here if so
    node.atime = times.atime;
    update = true;
  }
  if(times.mtime) {
    node.mtime = times.mtime;
    update = true;
  }

  function complete(error) {
    // Queue this change so we can send watch events.
    // Unlike node.js, we send the full path vs. basename/dirname only.
    context.changes.push({ event: 'change', path: path });
    callback(error);
  }

  if(update) {
    context.putObject(node.id, node, complete);
  } else {
    complete();
  }
}

/**
 * make_node()
 */
// in: file or directory path
// out: new node representing file/directory
function make_node(context, path, type, callback) {
  if(type !== NODE_TYPE_DIRECTORY && type !== NODE_TYPE_FILE) {
    return callback(new Errors.EINVAL('type must be a directory or file', path));
  }

  path = normalize(path);

  var name = basename(path);
  var parentPath = dirname(path);
  var parentNode;
  var parentNodeData;
  var node;

  // Check if the parent node exists
  function create_node_in_parent(error, parentDirectoryNode) {
    if(error) {
      callback(error);
    } else if(parentDirectoryNode.type !== NODE_TYPE_DIRECTORY) {
      callback(new Errors.ENOTDIR('a component of the path prefix is not a directory', path));
    } else {
      parentNode = parentDirectoryNode;
      find_node(context, path, check_if_node_exists);
    }
  }

  // Check if the node to be created already exists
  function check_if_node_exists(error, result) {
    if(!error && result) {
      callback(new Errors.EEXIST('path name already exists', path));
    } else if(error && !(error instanceof Errors.ENOENT)) {
      callback(error);
    } else {
      context.getObject(parentNode.data, create_node);
    }
  }

  // Create the new node
  function create_node(error, result) {
    if(error) {
      callback(error);
    } else {
      parentNodeData = result;
      Node.create({
        guid: context.guid,
        type: type
      }, function(error, result) {
        if(error) {
          callback(error);
          return;
        }
        node = result;
        node.nlinks += 1;
        context.putObject(node.id, node, update_parent_node_data);
      });
    }
  }

  // Update parent node time
  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, parentPath, node, { mtime: now, ctime: now }, callback);
    }
  }

  // Update the parent nodes data
  function update_parent_node_data(error) {
    if(error) {
      callback(error);
    } else {
      parentNodeData[name] = new DirectoryEntry(node.id, type);
      context.putObject(parentNode.data, parentNodeData, update_time);
    }
  }

  // Find the parent node
  find_node(context, parentPath, create_node_in_parent);
}

/**
 * find_node
 */
// in: file or directory path
// out: node structure, or error
function find_node(context, path, callback) {
  path = normalize(path);
  if(!path) {
    return callback(new Errors.ENOENT('path is an empty string'));
  }
  var name = basename(path);
  var parentPath = dirname(path);
  var followedCount = 0;

  function read_root_directory_node(error, nodeData) {
    if(error) {
      return callback(error);
    }

    // Parse existing node as SuperNode
    const superNode = new SuperNode(nodeData);

    if(!superNode || superNode.type !== NODE_TYPE_META || !superNode.rnode) {
      callback(new Errors.EFILESYSTEMERROR());
    } else {
      context.getObject(superNode.rnode, check_root_directory_node);
    }
  }

  function check_root_directory_node(error, rootDirectoryNode) {
    if(error) {
      callback(error);
    } else if(!rootDirectoryNode) {
      callback(new Errors.ENOENT());
    } else {
      Node.create(rootDirectoryNode, callback);
    }
  }

  // in: parent directory node
  // out: parent directory data
  function read_parent_directory_data(error, parentDirectoryNode) {
    if(error) {
      callback(error);
    } else if(parentDirectoryNode.type !== NODE_TYPE_DIRECTORY || !parentDirectoryNode.data) {
      callback(new Errors.ENOTDIR('a component of the path prefix is not a directory', path));
    } else {
      context.getObject(parentDirectoryNode.data, get_node_from_parent_directory_data);
    }
  }

  // in: parent directory data
  // out: searched node
  function get_node_from_parent_directory_data(error, parentDirectoryData) {
    if(error) {
      callback(error);
    } else {
      if(!Object.prototype.hasOwnProperty.call(parentDirectoryData, name)) {
        callback(new Errors.ENOENT(null, path));
      } else {
        var nodeId = parentDirectoryData[name].id;
        context.getObject(nodeId, create_node);
      }
    }
  }

  function create_node(error, data) {
    if(error) {
      return callback(error);
    }
    Node.create(data, is_symbolic_link);
  }

  function is_symbolic_link(error, node) {
    if(error) {
      callback(error);
    } else {
      if(node.type === NODE_TYPE_SYMBOLIC_LINK) {
        followedCount++;
        if(followedCount > SYMLOOP_MAX){
          callback(new Errors.ELOOP(null, path));
        } else {
          follow_symbolic_link(node.data);
        }
      } else {
        callback(null, node);
      }
    }
  }

  function follow_symbolic_link(data) {
    data = normalize(data);
    parentPath = dirname(data);
    name = basename(data);
    if(ROOT_DIRECTORY_NAME === name) {
      context.getObject(SUPER_NODE_ID, read_root_directory_node);
    } else {
      find_node(context, parentPath, read_parent_directory_data);
    }
  }

  if(ROOT_DIRECTORY_NAME === name) {
    context.getObject(SUPER_NODE_ID, read_root_directory_node);
  } else {
    find_node(context, parentPath, read_parent_directory_data);
  }
}


/**
 * set extended attribute (refactor)
 */
function set_extended_attribute (context, path, node, name, value, flag, callback) {
  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      update_node_times(context, path, node, { ctime: Date.now() }, callback);
    }
  }

  var xattrs = node.xattrs;

  if (flag === XATTR_CREATE && Object.prototype.hasOwnProperty.call(xattrs, name)) {
    callback(new Errors.EEXIST('attribute already exists', path));
  }
  else if (flag === XATTR_REPLACE && !Object.prototype.hasOwnProperty.call(xattrs, name)) {
    callback(new Errors.ENOATTR(null, path));
  }
  else {
    xattrs[name] = value;
    context.putObject(node.id, node, update_time);
  }
}

/**
 * ensure_root_directory. Creates a root node if necessary.
 *
 * Note: this should only be invoked when formatting a new file system.
 * Multiple invocations of this by separate instances will still result
 * in only a single super node.
 */
function ensure_root_directory(context, callback) {
  var superNode;
  var directoryNode;
  var directoryData;

  function ensure_super_node(error, existingNode) {
    if(!error && existingNode) {
      // Another instance has beat us and already created the super node.
      callback();
    } else if(error && !(error instanceof Errors.ENOENT)) {
      callback(error);
    } else {
      SuperNode.create({guid: context.guid}, function(error, result) {
        if(error) {
          callback(error);
          return;
        }
        superNode = result;
        context.putObject(superNode.id, superNode, write_directory_node);
      });
    }
  }

  function write_directory_node(error) {
    if(error) {
      callback(error);
    } else {
      Node.create({
        guid: context.guid,
        id: superNode.rnode,
        type: NODE_TYPE_DIRECTORY
      }, function(error, result) {
        if(error) {
          callback(error);
          return;
        }
        directoryNode = result;
        directoryNode.nlinks += 1;
        context.putObject(directoryNode.id, directoryNode, write_directory_data);
      });
    }
  }

  function write_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      directoryData = {};
      context.putObject(directoryNode.data, directoryData, callback);
    }
  }

  context.getObject(SUPER_NODE_ID, ensure_super_node);
}

/**
 * make_directory
 */
function make_directory(context, path, callback) {
  path = normalize(path);
  var name = basename(path);
  var parentPath = dirname(path);

  var directoryNode;
  var directoryData;
  var parentDirectoryNode;
  var parentDirectoryData;

  function check_if_directory_exists(error, result) {
    if(!error && result) {
      callback(new Errors.EEXIST(null, path));
    } else if(error && !(error instanceof Errors.ENOENT)) {
      callback(error);
    } else {
      find_node(context, parentPath, read_parent_directory_data);
    }
  }

  function read_parent_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      parentDirectoryNode = result;
      context.getObject(parentDirectoryNode.data, write_directory_node);
    }
  }

  function write_directory_node(error, result) {
    if(error) {
      callback(error);
    } else {
      parentDirectoryData = result;
      Node.create({
        guid: context.guid,
        type: NODE_TYPE_DIRECTORY
      }, function(error, result) {
        if(error) {
          callback(error);
          return;
        }
        directoryNode = result;
        directoryNode.nlinks += 1;
        context.putObject(directoryNode.id, directoryNode, write_directory_data);
      });
    }
  }

  function write_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      directoryData = {};
      context.putObject(directoryNode.data, directoryData, update_parent_directory_data);
    }
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, parentPath, parentDirectoryNode, { mtime: now, ctime: now }, callback);
    }
  }

  function update_parent_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      parentDirectoryData[name] = new DirectoryEntry(directoryNode.id, NODE_TYPE_DIRECTORY);
      context.putObject(parentDirectoryNode.data, parentDirectoryData, update_time);
    }
  }

  find_node(context, path, check_if_directory_exists);
}

function access_file(context, path, mode, callback) {
  const { F_OK, R_OK, W_OK, X_OK, S_IXUSR, S_IXGRP, S_IXOTH } = Constants.fsConstants;

  path = normalize(path);
  find_node(context, path, function (err, node) {
    if (err) {
      return callback(err);
    }

    // If we have a node, F_OK is true.
    if(mode === F_OK) {
      return callback(null);
    }

    var st_mode = validateAndMaskMode(node.mode, callback);
    if(!st_mode) return;

    // For any other combo of F_OK, R_OK, W_OK, always allow. Filer user is a root user,
    // so existing files are always OK, readable, and writable
    if(mode & (R_OK | W_OK)) {
      return callback(null);
    }

    // For the case of X_OK, actually check if this file is executable
    if ((mode & X_OK) && (st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
      return callback(null);
    }

    // In any other case, the file isn't accessible
    callback(new Errors.EACCES('permission denied',path)) ;
  });
}

/**
 * remove_directory
 */
function remove_directory(context, path, callback) {
  path = normalize(path);
  var name = basename(path);
  var parentPath = dirname(path);

  var directoryNode;
  var directoryData;
  var parentDirectoryNode;
  var parentDirectoryData;

  function read_parent_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      parentDirectoryNode = result;
      context.getObject(parentDirectoryNode.data, check_if_node_exists);
    }
  }

  function check_if_node_exists(error, result) {
    if(error) {
      callback(error);
    } else if(ROOT_DIRECTORY_NAME === name) {
      callback(new Errors.EBUSY(null, path));
    } else if(!Object.prototype.hasOwnProperty.call(result, name)) {
      callback(new Errors.ENOENT(null, path));
    } else {
      parentDirectoryData = result;
      directoryNode = parentDirectoryData[name].id;
      context.getObject(directoryNode, check_if_node_is_directory);
    }
  }

  function check_if_node_is_directory(error, result) {
    if(error) {
      callback(error);
    } else if(result.type !== NODE_TYPE_DIRECTORY) {
      callback(new Errors.ENOTDIR(null, path));
    } else {
      directoryNode = result;
      context.getObject(directoryNode.data, check_if_directory_is_empty);
    }
  }

  function check_if_directory_is_empty(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryData = result;
      if(Object.keys(directoryData).length > 0) {
        callback(new Errors.ENOTEMPTY(null, path));
      } else {
        remove_directory_entry_from_parent_directory_node();
      }
    }
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, parentPath, parentDirectoryNode, { mtime: now, ctime: now }, remove_directory_node);
    }
  }

  function remove_directory_entry_from_parent_directory_node() {
    delete parentDirectoryData[name];
    context.putObject(parentDirectoryNode.data, parentDirectoryData, update_time);
  }

  function remove_directory_node(error) {
    if(error) {
      callback(error);
    } else {
      context.delete(directoryNode.id, remove_directory_data);
    }
  }

  function remove_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      context.delete(directoryNode.data, callback);
    }
  }

  find_node(context, parentPath, read_parent_directory_data);
}

function open_file(context, path, flags, mode, callback) {
  if (typeof mode === 'function'){
    callback = mode;
    mode = null;
  }
  path = normalize(path);
  var name = basename(path);
  var parentPath = dirname(path);

  var directoryNode;
  var directoryData;
  var directoryEntry;
  var fileNode;
  var fileData;

  var followedCount = 0;

  if(ROOT_DIRECTORY_NAME === name) {
    if(flags.includes(O_WRITE)) {
      callback(new Errors.EISDIR('the named file is a directory and O_WRITE is set', path));
    } else {
      find_node(context, path, set_file_node);
    }
  } else {
    find_node(context, parentPath, read_directory_data);
  }

  function read_directory_data(error, result) {
    if(error) {
      callback(error);
    } else if(result.type !== NODE_TYPE_DIRECTORY) {
      callback(new Errors.ENOENT(null, path));
    } else {
      directoryNode = result;
      context.getObject(directoryNode.data, check_if_file_exists);
    }
  }

  function check_if_file_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryData = result;
      if(Object.prototype.hasOwnProperty.call(directoryData, name)) {
        if(flags.includes(O_EXCLUSIVE)) {
          callback(new Errors.EEXIST('O_CREATE and O_EXCLUSIVE are set, and the named file exists', path));
        } else {
          directoryEntry = directoryData[name];
          if(directoryEntry.type === NODE_TYPE_DIRECTORY && flags.includes(O_WRITE)) {
            callback(new Errors.EISDIR('the named file is a directory and O_WRITE is set', path));
          } else {
            context.getObject(directoryEntry.id, check_if_symbolic_link);
          }
        }
      } else {
        if(!flags.includes(O_CREATE)) {
          callback(new Errors.ENOENT('O_CREATE is not set and the named file does not exist', path));
        } else {
          write_file_node();
        }
      }
    }
  }

  function check_if_symbolic_link(error, result) {
    if(error) {
      callback(error);
    } else {
      var node = result;
      if(node.type === NODE_TYPE_SYMBOLIC_LINK) {
        followedCount++;
        if(followedCount > SYMLOOP_MAX){
          callback(new Errors.ELOOP(null, path));
        } else {
          follow_symbolic_link(node.data);
        }
      } else {
        set_file_node(undefined, node);
      }
    }
  }

  function follow_symbolic_link(data) {
    data = normalize(data);
    parentPath = dirname(data);
    name = basename(data);
    if(ROOT_DIRECTORY_NAME === name) {
      if(flags.includes(O_WRITE)) {
        callback(new Errors.EISDIR('the named file is a directory and O_WRITE is set', path));
      } else {
        find_node(context, path, set_file_node);
      }
    }
    find_node(context, parentPath, read_directory_data);
  }

  function set_file_node(error, result) {
    if(error) {
      callback(error);
    } else {
      fileNode = result;
      callback(null, fileNode);
    }
  }

  function write_file_node() {
    Node.create({
      guid: context.guid,
      type: NODE_TYPE_FILE
    }, function(error, result) {
      if(error) {
        callback(error);
        return;
      }
      fileNode = result;
      fileNode.nlinks += 1;
      if(mode){
        fileNode.mode = mode;
      }
      context.putObject(fileNode.id, fileNode, write_file_data);
    });
  }

  function write_file_data(error) {
    if(error) {
      callback(error);
    } else {
      fileData = Buffer.alloc(0);
      context.putBuffer(fileNode.data, fileData, update_directory_data);
    }
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, parentPath, directoryNode, { mtime: now, ctime: now }, handle_update_result);
    }
  }

  function update_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      directoryData[name] = new DirectoryEntry(fileNode.id, NODE_TYPE_FILE);
      context.putObject(directoryNode.data, directoryData, update_time);
    }
  }

  function handle_update_result(error) {
    if(error) {
      callback(error);
    } else {
      callback(null, fileNode);
    }
  }
}

function replace_data(context, ofd, buffer, offset, length, callback) {
  var fileNode;

  function return_nbytes(error) {
    if(error) {
      callback(error);
    } else {
      callback(null, length);
    }
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, ofd.path, fileNode, { mtime: now, ctime: now }, return_nbytes);
    }
  }

  function update_file_node(error) {
    if(error) {
      callback(error);
    } else {
      context.putObject(fileNode.id, fileNode, update_time);
    }
  }

  function write_file_data(error, result) {
    if(error) {
      callback(error);
    } else {
      fileNode = result;

      var newData = Buffer.alloc(length);
      buffer.copy(newData, 0, offset, offset + length);
      ofd.position = length;

      fileNode.size = length;
      fileNode.version += 1;

      context.putBuffer(fileNode.data, newData, update_file_node);
    }
  }

  context.getObject(ofd.id, write_file_data);
}

function write_data(context, ofd, buffer, offset, length, position, callback) {
  var fileNode;
  var fileData;

  function return_nbytes(error) {
    if(error) {
      callback(error);
    } else {
      callback(null, length);
    }
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, ofd.path, fileNode, { mtime: now, ctime: now }, return_nbytes);
    }
  }

  function update_file_node(error) {
    if(error) {
      callback(error);
    } else {
      context.putObject(fileNode.id, fileNode, update_time);
    }
  }

  function update_file_data(error, result) {
    if(error) {
      callback(error);
    } else {
      fileData = result;
      if(!fileData) {
        return callback(new Errors.EIO('Expected Buffer'));
      }
      var _position = (!(undefined === position || null === position)) ? position : ofd.position;
      var newSize = Math.max(fileData.length, _position + length);
      var newData = Buffer.alloc(newSize);
      if(fileData) {
        fileData.copy(newData);
      }
      buffer.copy(newData, _position, offset, offset + length);
      if(undefined === position) {
        ofd.position += length;
      }

      fileNode.size = newSize;
      fileNode.version += 1;

      context.putBuffer(fileNode.data, newData, update_file_node);
    }
  }

  function read_file_data(error, result) {
    if(error) {
      callback(error);
    } else {
      fileNode = result;
      context.getBuffer(fileNode.data, update_file_data);
    }
  }

  context.getObject(ofd.id, read_file_data);
}

function read_data(context, ofd, buffer, offset, length, position, callback) {
  var fileNode;
  var fileData;

  function handle_file_data(error, result) {
    if(error) {
      callback(error);
    } else {
      fileData = result;
      if(!fileData) {
        return callback(new Errors.EIO('Expected Buffer'));
      }
      var _position = (!(undefined === position || null === position)) ? position : ofd.position;
      length = (_position + length > buffer.length) ? length - _position : length;
      fileData.copy(buffer, offset, _position, _position + length);
      if(undefined === position) {
        ofd.position += length;
      }
      callback(null, length);
    }
  }

  function read_file_data(error, result) {
    if(error) {
      callback(error);
    } else if(result.type === NODE_TYPE_DIRECTORY) {
      callback(new Errors.EISDIR('the named file is a directory', ofd.path));
    } else {
      fileNode = result;
      context.getBuffer(fileNode.data, handle_file_data);
    }
  }

  context.getObject(ofd.id, read_file_data);
}

function stat_file(context, path, callback) {
  path = normalize(path);
  find_node(context, path, callback);
}

function fstat_file(context, ofd, callback) {
  ofd.getNode(context, callback);
}

function lstat_file(context, path, callback) {
  path = normalize(path);
  var name = basename(path);
  var parentPath = dirname(path);

  var directoryNode;
  var directoryData;

  if(ROOT_DIRECTORY_NAME === name) {
    find_node(context, path, callback);
  } else {
    find_node(context, parentPath, read_directory_data);
  }

  function read_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryNode = result;
      context.getObject(directoryNode.data, check_if_file_exists);
    }
  }

  function create_node(error, data) {
    if(error) {
      return callback(error);
    }
    Node.create(data, callback);
  }

  function check_if_file_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryData = result;
      if(!Object.prototype.hasOwnProperty.call(directoryData, name)) {
        callback(new Errors.ENOENT('a component of the path does not name an existing file', path));
      } else {
        context.getObject(directoryData[name].id, create_node);
      }
    }
  }
}

function link_node(context, oldpath, newpath, callback) {
  oldpath = normalize(oldpath);
  var oldname = basename(oldpath);
  var oldParentPath = dirname(oldpath);

  newpath = normalize(newpath);
  var newname = basename(newpath);
  var newParentPath = dirname(newpath);
  var ctime = Date.now();

  var oldDirectoryNode;
  var oldDirectoryData;
  var newDirectoryNode;
  var newDirectoryData;
  var fileNodeID;
  var fileNode;

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      update_node_times(context, newpath, fileNode, { ctime: ctime }, callback);
    }
  }

  function update_file_node(error, result) {
    if(error) {
      callback(error);
    } else {
      fileNode = result;
      fileNode.nlinks += 1;
      context.putObject(fileNode.id, fileNode, update_time);
    }
  }

  function read_file_node(error) {
    if(error) {
      callback(error);
    } else {
      context.getObject(fileNodeID, update_file_node);
    }
  }

  function check_if_new_file_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      newDirectoryData = result;
      if(Object.prototype.hasOwnProperty.call(newDirectoryData, newname)) {
        callback(new Errors.EEXIST('newpath resolves to an existing file', newname));
      } else {
        newDirectoryData[newname] = oldDirectoryData[oldname];
        fileNodeID = newDirectoryData[newname].id;
        context.putObject(newDirectoryNode.data, newDirectoryData, read_file_node);
      }
    }
  }

  function read_new_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      newDirectoryNode = result;
      context.getObject(newDirectoryNode.data, check_if_new_file_exists);
    }
  }

  function check_if_old_file_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      oldDirectoryData = result;
      if(!Object.prototype.hasOwnProperty.call(oldDirectoryData, oldname)) {
        callback(new Errors.ENOENT('a component of either path prefix does not exist', oldname));
      } else if(oldDirectoryData[oldname].type === NODE_TYPE_DIRECTORY) {
        callback(new Errors.EPERM('oldpath refers to a directory'));
      } else {
        find_node(context, newParentPath, read_new_directory_data);
      }
    }
  }

  function read_old_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      oldDirectoryNode = result;
      context.getObject(oldDirectoryNode.data, check_if_old_file_exists);
    }
  }

  find_node(context, oldParentPath, read_old_directory_data);
}

function unlink_node(context, path, callback) {
  path = normalize(path);
  var name = basename(path);
  var parentPath = dirname(path);

  var directoryNode;
  var directoryData;
  var fileNode;

  function update_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      delete directoryData[name];
      context.putObject(directoryNode.data, directoryData, function(error) {
        if(error) {
          callback(error);
        } else {
          var now = Date.now();
          update_node_times(context, parentPath, directoryNode, { mtime: now, ctime: now }, callback);
        }
      });
    }
  }

  function delete_file_data(error) {
    if(error) {
      callback(error);
    } else {
      context.delete(fileNode.data, update_directory_data);
    }
  }

  function update_file_node(error, result) {
    if(error) {
      callback(error);
    } else {
      fileNode = result;
      fileNode.nlinks -= 1;
      if(fileNode.nlinks < 1) {
        context.delete(fileNode.id, delete_file_data);
      } else {
        context.putObject(fileNode.id, fileNode, function(error) {
          if(error) {
            callback(error);
          } else {
            update_node_times(context, path, fileNode, { ctime: Date.now() }, update_directory_data);
          }
        });
      }
    }
  }

  function check_if_node_is_directory(error, result) {
    if(error) {
      callback(error);
    } else if(result.type === NODE_TYPE_DIRECTORY) {
      callback(new Errors.EPERM('unlink not permitted on directories', name));
    } else {
      update_file_node(null, result);
    }
  }

  function check_if_file_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryData = result;
      if(!Object.prototype.hasOwnProperty.call(directoryData, name)) {
        callback(new Errors.ENOENT('a component of the path does not name an existing file', name));
      } else {
        context.getObject(directoryData[name].id, check_if_node_is_directory);
      }
    }
  }

  function read_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryNode = result;
      context.getObject(directoryNode.data, check_if_file_exists);
    }
  }

  find_node(context, parentPath, read_directory_data);
}

function read_directory(context, path, options, callback) {
  path = normalize(path);

  if (typeof options === 'function') {
    callback = options;
    options = {};
  }
  options = validate_directory_options(options);

  var directoryNode;
  var directoryData;

  function handle_directory_data(error, result) {
    if (error) {
      callback(error);
    } else {
      directoryData = result;
      var files = Object.keys(directoryData);

      if (options.encoding) {
        var fileBuffers = files.map(function (file) {
          return Buffer.from(file);
        });

        if (options.encoding === 'buffer') {
          files = fileBuffers;
        }
        else {
          files = fileBuffers.map(function (fileBuffer) {
            return fileBuffer.toString(options.encoding);
          });
        }
      }

      if (options.withFileTypes) {
        var dirEnts = [];

        // eslint-disable-next-line no-inner-declarations
        function to_dir_entry(file, callback) {
          const filename = Buffer.from(file, options.encoding).toString();
          const filepath = Path.join(path, filename);
          get_dir_entry(context, filepath, function(error, dirEnt) {
            if (error) {
              callback(error);
            }
            dirEnt.name = file;
            dirEnts.push(dirEnt);
            callback();
          });
        }

        async.eachSeries(files, to_dir_entry, function (error) {
          callback(error, dirEnts);
        });
      }

      else {
        callback(null, files);
      }
    }
  }

  function read_directory_data(error, result) {
    if (error) {
      callback(error);
    } else if (result.type !== NODE_TYPE_DIRECTORY) {
      callback(new Errors.ENOTDIR(null, path));
    } else {
      directoryNode = result;
      context.getObject(directoryNode.data, handle_directory_data);
    }
  }

  find_node(context, path, read_directory_data);
}

function get_dir_entry(context, path, callback) {
  function check_result(error, result) {
    if(error) {
      callback(error);
    } else {
      var stats = new Dirent(path, result, context.name);
      callback(null, stats);
    }
  }

  lstat_file(context, path, check_result);
}

function validate_directory_options(options, enc) {
  if (!options) {
    options = { encoding: enc };
  } else if (typeof options === 'function') {
    options = { encoding: enc };
  } else if (typeof options === 'string') {
    options = { encoding: options };
  }
  return options;
}

function make_symbolic_link(context, srcpath, dstpath, callback) {
  dstpath = normalize(dstpath);
  var name = basename(dstpath);
  var parentPath = dirname(dstpath);

  var directoryNode;
  var directoryData;
  var fileNode;

  if(ROOT_DIRECTORY_NAME === name) {
    callback(new Errors.EEXIST(null, name));
  } else {
    find_node(context, parentPath, read_directory_data);
  }

  function read_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryNode = result;
      context.getObject(directoryNode.data, check_if_file_exists);
    }
  }

  function check_if_file_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryData = result;
      if(Object.prototype.hasOwnProperty.call(directoryData, name)) {
        callback(new Errors.EEXIST(null, name));
      } else {
        write_file_node();
      }
    }
  }

  function write_file_node() {
    Node.create({
      guid: context.guid,
      type: NODE_TYPE_SYMBOLIC_LINK
    }, function(error, result) {
      if(error) {
        callback(error);
        return;
      }
      fileNode = result;
      fileNode.nlinks += 1;

      // If the srcpath isn't absolute, resolve it relative to the dstpath
      // but store both versions, since we'll use the relative one in readlink().
      if(!isAbsolutePath(srcpath)) {
        fileNode.symlink_relpath = srcpath;
        srcpath = Path.resolve(parentPath, srcpath);
      }

      fileNode.size = srcpath.length;
      fileNode.data = srcpath;

      context.putObject(fileNode.id, fileNode, update_directory_data);
    });
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, parentPath, directoryNode, { mtime: now, ctime: now }, callback);
    }
  }

  function update_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      directoryData[name] = new DirectoryEntry(fileNode.id, NODE_TYPE_SYMBOLIC_LINK);
      context.putObject(directoryNode.data, directoryData, update_time);
    }
  }
}

function read_link(context, path, callback) {
  path = normalize(path);
  var name = basename(path);
  var parentPath = dirname(path);

  var directoryNode;
  var directoryData;

  find_node(context, parentPath, read_directory_data);

  function read_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryNode = result;
      context.getObject(directoryNode.data, check_if_file_exists);
    }
  }

  function check_if_file_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      directoryData = result;
      if(!Object.prototype.hasOwnProperty.call(directoryData, name)) {
        callback(new Errors.ENOENT('a component of the path does not name an existing file', name));
      } else {
        context.getObject(directoryData[name].id, check_if_symbolic);
      }
    }
  }

  function check_if_symbolic(error, fileNode) {
    if(error) {
      callback(error);
    } else {
      if(fileNode.type !== NODE_TYPE_SYMBOLIC_LINK) {
        callback(new Errors.EINVAL('path not a symbolic link', path));
      } else {
        // If we were originally given a relative path, return that now vs. the
        // absolute path we've generated and use elsewhere internally.
        var target = fileNode.symlink_relpath ? fileNode.symlink_relpath : fileNode.data;
        callback(null, target);
      }
    }
  }
}

function truncate_file(context, path, length, callback) {
  path = normalize(path);

  var fileNode;

  function read_file_data (error, node) {
    if (error) {
      callback(error);
    } else if(node.type === NODE_TYPE_DIRECTORY ) {
      callback(new Errors.EISDIR(null, path));
    } else{
      fileNode = node;
      context.getBuffer(fileNode.data, truncate_file_data);
    }
  }

  function truncate_file_data(error, fileData) {
    if (error) {
      callback(error);
    } else {
      if(!fileData) {
        return callback(new Errors.EIO('Expected Buffer'));
      }
      var data = Buffer.alloc(length);
      if(fileData) {
        fileData.copy(data);
      }
      context.putBuffer(fileNode.data, data, update_file_node);
    }
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, path, fileNode, { mtime: now, ctime: now }, callback);
    }
  }

  function update_file_node (error) {
    if(error) {
      callback(error);
    } else {
      fileNode.size = length;
      fileNode.version += 1;
      context.putObject(fileNode.id, fileNode, update_time);
    }
  }

  if(length < 0) {
    callback(new Errors.EINVAL('length cannot be negative'));
  } else {
    find_node(context, path, read_file_data);
  }
}

function ftruncate_file(context, ofd, length, callback) {
  var fileNode;

  function read_file_data (error, node) {
    if (error) {
      callback(error);
    } else if(node.type === NODE_TYPE_DIRECTORY ) {
      callback(new Errors.EISDIR());
    } else{
      fileNode = node;
      context.getBuffer(fileNode.data, truncate_file_data);
    }
  }

  function truncate_file_data(error, fileData) {
    if (error) {
      callback(error);
    } else {
      var data;
      if(!fileData) {
        return callback(new Errors.EIO('Expected Buffer'));
      }
      if(fileData) {
        data = fileData.slice(0, length);
      } else {
        data = Buffer.alloc(length);
      }
      context.putBuffer(fileNode.data, data, update_file_node);
    }
  }

  function update_time(error) {
    if(error) {
      callback(error);
    } else {
      var now = Date.now();
      update_node_times(context, ofd.path, fileNode, { mtime: now, ctime: now }, callback);
    }
  }

  function update_file_node (error) {
    if(error) {
      callback(error);
    } else {
      fileNode.size = length;
      fileNode.version += 1;
      context.putObject(fileNode.id, fileNode, update_time);
    }
  }

  if(length < 0) {
    callback(new Errors.EINVAL('length cannot be negative'));
  } else {
    ofd.getNode(context, read_file_data);
  }
}

function utimes_file(context, path, atime, mtime, callback) {
  path = normalize(path);

  function update_times(error, node) {
    if (error) {
      callback(error);
    } else {
      update_node_times(context, path, node, { atime: atime, ctime: mtime, mtime: mtime }, callback);
    }
  }

  if (typeof atime !== 'number' || typeof mtime !== 'number') {
    callback(new Errors.EINVAL('atime and mtime must be number', path));
  }
  else if (atime < 0 || mtime < 0) {
    callback(new Errors.EINVAL('atime and mtime must be positive integers', path));
  }
  else {
    find_node(context, path, update_times);
  }
}

function futimes_file(context, ofd, atime, mtime, callback) {

  function update_times (error, node) {
    if (error) {
      callback(error);
    } else {
      update_node_times(context, ofd.path, node, { atime: atime, ctime: mtime, mtime: mtime }, callback);
    }
  }

  if (typeof atime !== 'number' || typeof mtime !== 'number') {
    callback(new Errors.EINVAL('atime and mtime must be a number'));
  }
  else if (atime < 0 || mtime < 0) {
    callback(new Errors.EINVAL('atime and mtime must be positive integers'));
  }
  else {
    ofd.getNode(context, update_times);
  }
}

function setxattr_file(context, path, name, value, flag, callback) {
  path = normalize(path);

  function setxattr(error, node) {
    if(error) {
      return callback(error);
    }
    set_extended_attribute(context, path, node, name, value, flag, callback);
  }

  if (typeof name !== 'string') {
    callback(new Errors.EINVAL('attribute name must be a string', path));
  }
  else if (!name) {
    callback(new Errors.EINVAL('attribute name cannot be an empty string', path));
  }
  else if (flag !== null &&
           flag !== XATTR_CREATE && flag !== XATTR_REPLACE) {
    callback(new Errors.EINVAL('invalid flag, must be null, XATTR_CREATE or XATTR_REPLACE', path));
  }
  else {
    find_node(context, path, setxattr);
  }
}

function fsetxattr_file (context, ofd, name, value, flag, callback) {
  function setxattr(error, node) {
    if(error) {
      return callback(error);
    }
    set_extended_attribute(context, ofd.path, node, name, value, flag, callback);
  }

  if (typeof name !== 'string') {
    callback(new Errors.EINVAL('attribute name must be a string'));
  }
  else if (!name) {
    callback(new Errors.EINVAL('attribute name cannot be an empty string'));
  }
  else if (flag !== null &&
           flag !== XATTR_CREATE && flag !== XATTR_REPLACE) {
    callback(new Errors.EINVAL('invalid flag, must be null, XATTR_CREATE or XATTR_REPLACE'));
  }
  else {
    ofd.getNode(context, setxattr);
  }
}

function getxattr_file (context, path, name, callback) {
  path = normalize(path);

  function get_xattr(error, node) {
    if(error) {
      return callback(error);
    }

    var xattrs = node.xattrs;

    if (!Object.prototype.hasOwnProperty.call(xattrs, name)) {
      callback(new Errors.ENOATTR(null, path));
    }
    else {
      callback(null, xattrs[name]);
    }
  }

  if (typeof name !== 'string') {
    callback(new Errors.EINVAL('attribute name must be a string', path));
  }
  else if (!name) {
    callback(new Errors.EINVAL('attribute name cannot be an empty string', path));
  }
  else {
    find_node(context, path, get_xattr);
  }
}

function fgetxattr_file (context, ofd, name, callback) {

  function get_xattr (error, node) {
    if (error) {
      return callback(error);
    }

    var xattrs = node.xattrs;

    if (!Object.prototype.hasOwnProperty.call(xattrs, name)) {
      callback(new Errors.ENOATTR());
    }
    else {
      callback(null, xattrs[name]);
    }
  }

  if (typeof name !== 'string') {
    callback(new Errors.EINVAL());
  }
  else if (!name) {
    callback(new Errors.EINVAL('attribute name cannot be an empty string'));
  }
  else {
    ofd.getNode(context, get_xattr);
  }
}

function removexattr_file (context, path, name, callback) {
  path = normalize(path);

  function remove_xattr (error, node) {
    if (error) {
      return callback(error);
    }

    function update_time(error) {
      if(error) {
        callback(error);
      } else {
        update_node_times(context, path, node, { ctime: Date.now() }, callback);
      }
    }

    var xattrs = node.xattrs;

    if (!Object.prototype.hasOwnProperty.call(xattrs, name)) {
      callback(new Errors.ENOATTR(null, path));
    }
    else {
      delete xattrs[name];
      context.putObject(node.id, node, update_time);
    }
  }

  if (typeof name !== 'string') {
    callback(new Errors.EINVAL('attribute name must be a string', path));
  }
  else if (!name) {
    callback(new Errors.EINVAL('attribute name cannot be an empty string', path));
  }
  else {
    find_node(context, path, remove_xattr);
  }
}

function fremovexattr_file (context, ofd, name, callback) {

  function remove_xattr (error, node) {
    if (error) {
      return callback(error);
    }

    function update_time(error) {
      if(error) {
        callback(error);
      } else {
        update_node_times(context, ofd.path, node, { ctime: Date.now() }, callback);
      }
    }

    var xattrs = node.xattrs;

    if (!Object.prototype.hasOwnProperty.call(xattrs, name)) {
      callback(new Errors.ENOATTR());
    }
    else {
      delete xattrs[name];
      context.putObject(node.id, node, update_time);
    }
  }

  if (typeof name !== 'string') {
    callback(new Errors.EINVAL('attribute name must be a string'));
  }
  else if (!name) {
    callback(new Errors.EINVAL('attribute name cannot be an empty string'));
  }
  else {
    ofd.getNode(context, remove_xattr);
  }
}

function validate_flags(flags) {
  return Object.prototype.hasOwnProperty.call(O_FLAGS, flags) ? O_FLAGS[flags] : null;
}

function validate_file_options(options, enc, fileMode){
  if(!options) {
    options = { encoding: enc, flag: fileMode };
  } else if(typeof options === 'function') {
    options = { encoding: enc, flag: fileMode };
  } else if(typeof options === 'string') {
    options = { encoding: options, flag: fileMode };
  }
  return options;
}

function open(context, path, flags, mode, callback) {
  if (arguments.length < 5 ){
    callback = arguments[arguments.length - 1];
    mode = 0o644;
  }
  else {
    mode = validateAndMaskMode(mode, FULL_READ_WRITE_EXEC_PERMISSIONS, callback);
  }

  function check_result(error, fileNode) {
    if(error) {
      callback(error);
    } else {
      var position;
      if(flags.includes(O_APPEND)) {
        position = fileNode.size;
      } else {
        position = 0;
      }
      var openFileDescription = new OpenFileDescription(path, fileNode.id, flags, position);
      var fd = openFiles.allocDescriptor(openFileDescription);
      callback(null, fd);
    }
  }

  flags = validate_flags(flags);
  if(!flags) {
    return callback(new Errors.EINVAL('flags is not valid'), path);
  }

  open_file(context, path, flags, mode, check_result);
}

function close(context, fd, callback) {
  if(!openFiles.getOpenFileDescription(fd)) {
    callback(new Errors.EBADF());
  } else {
    openFiles.releaseDescriptor(fd);
    callback(null);
  }
}

function mknod(context, path, type, callback) {
  make_node(context, path, type, callback);
}

function mkdir(context, path, mode, callback) {
  if (arguments.length < 4) {
    callback = mode;
    mode = FULL_READ_WRITE_EXEC_PERMISSIONS;
  } else {
    mode = validateAndMaskMode(mode, FULL_READ_WRITE_EXEC_PERMISSIONS, callback);
    if(!mode) return;
  }

  make_directory(context, path, callback);
}

function access(context, path, mode, callback) {
  if (typeof mode === 'function') {
    callback = mode;
    mode = Constants.fsConstants.F_OK;
  }

  mode = mode | Constants.fsConstants.F_OK;
  access_file(context, path, mode, callback);
}

function mkdtemp(context, prefix, options, callback) {
  callback = arguments[arguments.length - 1];
  if(!prefix) {
    return callback(new Error('filename prefix is required'));
  }

  let random = shared.randomChars(6);
  var path = prefix + '-' + random;

  make_directory(context, path, function(error) {
    callback(error, path);
  });
}

function rmdir(context, path, callback) {
  remove_directory(context, path, callback);
}

function stat(context, path, callback) {
  function check_result(error, result) {
    if(error) {
      callback(error);
    } else {
      var stats = new Stats(path, result, context.name);
      callback(null, stats);
    }
  }

  stat_file(context, path, check_result);
}

function fstat(context, fd, callback) {
  function check_result(error, result) {
    if(error) {
      callback(error);
    } else {
      var stats = new Stats(ofd.path, result, context.name);
      callback(null, stats);
    }
  }

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else {
    fstat_file(context, ofd, check_result);
  }
}

function link(context, oldpath, newpath, callback) {
  link_node(context, oldpath, newpath, callback);
}

function unlink(context, path, callback) {
  unlink_node(context, path, callback);
}

function read(context, fd, buffer, offset, length, position, callback) {
  // Follow how node.js does this
  function wrapped_cb(err, bytesRead) {
    // Retain a reference to buffer so that it can't be GC'ed too soon.
    callback(err, bytesRead || 0, buffer);
  }

  offset = (undefined === offset) ? 0 : offset;
  length = (undefined === length) ? buffer.length - offset : length;
  callback = arguments[arguments.length - 1];

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else if(!ofd.flags.includes(O_READ)) {
    callback(new Errors.EBADF('descriptor does not permit reading'));
  } else {
    read_data(context, ofd, buffer, offset, length, position, wrapped_cb);
  }
}

function fsync(context, fd, callback) {
  if(validateInteger(fd, callback) !== fd) return;
  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else {
    callback();
  }
}

function readFile(context, path, options, callback) {
  callback = arguments[arguments.length - 1];
  options = validate_file_options(options, null, 'r');

  var flags = validate_flags(options.flag || 'r');
  if(!flags) {
    return callback(new Errors.EINVAL('flags is not valid', path));
  }

  open_file(context, path, flags, function(err, fileNode) {
    if(err) {
      return callback(err);
    }
    var ofd = new OpenFileDescription(path, fileNode.id, flags, 0);
    var fd = openFiles.allocDescriptor(ofd);

    function cleanup() {
      openFiles.releaseDescriptor(fd);
    }

    fstat_file(context, ofd, function(err, fstatResult) {
      if(err) {
        cleanup();
        return callback(err);
      }

      var stats = new Stats(ofd.path, fstatResult, context.name);

      if(stats.isDirectory()) {
        cleanup();
        return callback(new Errors.EISDIR('illegal operation on directory', path));
      }

      var size = stats.size;
      var buffer = Buffer.alloc(size);

      read_data(context, ofd, buffer, 0, size, 0, function(err) {
        cleanup();

        if(err) {
          return callback(err);
        }

        var data;
        if(options.encoding === 'utf8') {
          data = buffer.toString('utf8');
        } else {
          data = buffer;
        }
        callback(null, data);
      });
    });
  });
}

function write(context, fd, buffer, offset, length, position, callback) {
  callback = arguments[arguments.length - 1];
  offset = (undefined === offset) ? 0 : offset;
  length = (undefined === length) ? buffer.length - offset : length;

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else if(!ofd.flags.includes(O_WRITE)) {
    callback(new Errors.EBADF('descriptor does not permit writing'));
  } else if(buffer.length - offset < length) {
    callback(new Errors.EIO('input buffer is too small'));
  } else {
    write_data(context, ofd, buffer, offset, length, position, callback);
  }
}

function writeFile(context, path, data, options, callback) {
  callback = arguments[arguments.length - 1];
  options = validate_file_options(options, 'utf8', 'w');

  var flags = validate_flags(options.flag || 'w');
  if(!flags) {
    return callback(new Errors.EINVAL('flags is not valid', path));
  }

  if(!Buffer.isBuffer(data)) {
    if(typeof data === 'number') {
      data = '' + data;
    }
    data = data || '';
    if(typeof data !== 'string') {
      data = Buffer.from(data.toString());
    }
    else {
      data = Buffer.from(data || '', options.encoding || 'utf8');
    }
  }

  open_file(context, path, flags, function(err, fileNode) {
    if(err) {
      return callback(err);
    }
    var ofd = new OpenFileDescription(path, fileNode.id, flags, 0);
    var fd = openFiles.allocDescriptor(ofd);

    replace_data(context, ofd, data, 0, data.length, function(err) {
      openFiles.releaseDescriptor(fd);

      if(err) {
        return callback(err);
      }
      callback(null);
    });
  });
}

function appendFile(context, path, data, options, callback) {
  callback = arguments[arguments.length - 1];
  options = validate_file_options(options, 'utf8', 'a');

  var flags = validate_flags(options.flag || 'a');
  if(!flags) {
    return callback(new Errors.EINVAL('flags is not valid', path));
  }

  data = data || '';
  if(typeof data === 'number') {
    data = '' + data;
  }
  if(typeof data === 'string' && options.encoding === 'utf8') {
    data = Buffer.from(data);
  }

  open_file(context, path, flags, function(err, fileNode) {
    if(err) {
      return callback(err);
    }
    var ofd = new OpenFileDescription(path, fileNode.id, flags, fileNode.size);
    var fd = openFiles.allocDescriptor(ofd);

    write_data(context, ofd, data, 0, data.length, ofd.position, function(err) {
      openFiles.releaseDescriptor(fd);

      if(err) {
        return callback(err);
      }
      callback(null);
    });
  });
}

function exists(context, path, callback) {
  function cb(err) {
    callback(err ? false : true);
  }
  stat(context, path, cb);
}

function validateInteger(value, callback) {
  if (typeof value !== 'number') {
    callback(new Errors.EINVAL('Expected integer', value));
    return;
  }

  return value;
}

// Based on https://github.com/nodejs/node/blob/c700cc42da9cf73af9fec2098520a6c0a631d901/lib/internal/validators.js#L21
var octalReg = /^[0-7]+$/;
function isUint32(value) {
  return value === (value >>> 0);
}
// Validator for mode_t (the S_* constants). Valid numbers or octal strings
// will be masked with 0o777 to be consistent with the behavior in POSIX APIs.
function validateAndMaskMode(value, def, callback) {
  if(typeof def === 'function') {
    callback = def;
    def = undefined;
  }

  if (isUint32(value)) {
    return value & FULL_READ_WRITE_EXEC_PERMISSIONS;
  }

  if (typeof value === 'number') {
    if (!Number.isInteger(value)) {
      callback(new Errors.EINVAL('mode not a valid an integer value', value));
      return false;
    } else {
      // 2 ** 32 === 4294967296
      callback(new Errors.EINVAL('mode not a valid an integer value', value));
      return false;
    }
  }

  if (typeof value === 'string') {
    if (!octalReg.test(value)) {
      callback(new Errors.EINVAL('mode not a valid octal string', value));
      return false;
    }
    var parsed = parseInt(value, 8);
    return parsed & FULL_READ_WRITE_EXEC_PERMISSIONS;
  }

  // TODO(BridgeAR): Only return `def` in case `value === null`
  if (def !== undefined) {
    return def;
  }

  callback(new Errors.EINVAL('mode not valid', value));
  return false;
}

function chmod_file(context, path, mode, callback) {
  path = normalize(path);

  function update_mode(error, node) {
    if (error) {
      callback(error);
    } else {
      node.mode = mode;
      update_node_times(context, path, node, { mtime: Date.now() }, callback);
    }
  }

  if (typeof mode !== 'number') {
    callback(new Errors.EINVAL('mode must be number', path));
  }
  else {
    find_node(context, path, update_mode);
  }
}

function fchmod_file(context, ofd, mode, callback) {
  function update_mode(error, node) {
    if (error) {
      callback(error);
    } else {
      node.mode = mode;
      update_node_times(context, ofd.path, node, { mtime: Date.now() }, callback);
    }
  }

  if (typeof mode !== 'number') {
    callback(new Errors.EINVAL('mode must be a number'));
  }
  else {
    ofd.getNode(context, update_mode);
  }
}

function chown_file(context, path, uid, gid, callback) {
  path = normalize(path);

  function update_owner(error, node) {
    if (error) {
      callback(error);
    } else {
      node.uid = uid;
      node.gid = gid;
      update_node_times(context, path, node, { mtime: Date.now() }, callback);
    }
  }

  find_node(context, path, update_owner);
}

function fchown_file(context, ofd, uid, gid, callback) {
  function update_owner(error, node) {
    if (error) {
      callback(error);
    } else {
      node.uid = uid;
      node.gid = gid;
      update_node_times(context, ofd.path, node, { mtime: Date.now() }, callback);
    }
  }

  ofd.getNode(context, update_owner);
}

function getxattr(context, path, name, callback) {
  getxattr_file(context, path, name, callback);
}

function fgetxattr(context, fd, name, callback) {
  var ofd = openFiles.getOpenFileDescription(fd);
  if (!ofd) {
    callback(new Errors.EBADF());
  }
  else {
    fgetxattr_file(context, ofd, name, callback);
  }
}

function setxattr(context, path, name, value, flag, callback) {
  if(typeof flag === 'function') {
    callback = flag;
    flag = null;
  }

  setxattr_file(context, path, name, value, flag, callback);
}

function fsetxattr(context, fd, name, value, flag, callback) {
  if(typeof flag === 'function') {
    callback = flag;
    flag = null;
  }

  var ofd = openFiles.getOpenFileDescription(fd);
  if (!ofd) {
    callback(new Errors.EBADF());
  }
  else if (!ofd.flags.includes(O_WRITE)) {
    callback(new Errors.EBADF('descriptor does not permit writing'));
  }
  else {
    fsetxattr_file(context, ofd, name, value, flag, callback);
  }
}

function removexattr(context, path, name, callback) {
  removexattr_file(context, path, name, callback);
}

function fremovexattr(context, fd, name, callback) {
  var ofd = openFiles.getOpenFileDescription(fd);
  if (!ofd) {
    callback(new Errors.EBADF());
  }
  else if (!ofd.flags.includes(O_WRITE)) {
    callback(new Errors.EBADF('descriptor does not permit writing'));
  }
  else {
    fremovexattr_file(context, ofd, name, callback);
  }
}

function lseek(context, fd, offset, whence, callback) {
  function update_descriptor_position(error, stats) {
    if(error) {
      callback(error);
    } else {
      if(stats.size + offset < 0) {
        callback(new Errors.EINVAL('resulting file offset would be negative'));
      } else {
        ofd.position = stats.size + offset;
        callback(null, ofd.position);
      }
    }
  }

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  }

  if('SET' === whence) {
    if(offset < 0) {
      callback(new Errors.EINVAL('resulting file offset would be negative'));
    } else {
      ofd.position = offset;
      callback(null, ofd.position);
    }
  } else if('CUR' === whence) {
    if(ofd.position + offset < 0) {
      callback(new Errors.EINVAL('resulting file offset would be negative'));
    } else {
      ofd.position += offset;
      callback(null, ofd.position);
    }
  } else if('END' === whence) {
    fstat_file(context, ofd, update_descriptor_position);
  } else {
    callback(new Errors.EINVAL('whence argument is not a proper value'));
  }
}

function readdir(context, path, options, callback) {
  read_directory(context, path, options, callback);
}

function toUnixTimestamp(time) {
  if (typeof time === 'number') {
    return time;
  }
  if (typeof time === 'object' && typeof time.getTime === 'function') {
    return time.getTime();
  }
}

function utimes(context, path, atime, mtime, callback) {
  var currentTime = Date.now();
  atime = (atime) ? toUnixTimestamp(atime) : toUnixTimestamp(currentTime);
  mtime = (mtime) ? toUnixTimestamp(mtime) : toUnixTimestamp(currentTime);

  utimes_file(context, path, atime, mtime, callback);
}

function futimes(context, fd, atime, mtime, callback) {
  var currentTime = Date.now();
  atime = (atime) ? toUnixTimestamp(atime) : toUnixTimestamp(currentTime);
  mtime = (mtime) ? toUnixTimestamp(mtime) : toUnixTimestamp(currentTime);

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else if(!ofd.flags.includes(O_WRITE)) {
    callback(new Errors.EBADF('descriptor does not permit writing'));
  } else {
    futimes_file(context, ofd, atime, mtime, callback);
  }
}

function chmod(context, path, mode, callback) {
  mode = validateAndMaskMode(mode, callback);
  if(!mode) return;

  chmod_file(context, path, mode, callback);
}

function fchmod(context, fd, mode, callback) {
  mode = validateAndMaskMode(mode, callback);
  if(!mode) return;

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else if(!ofd.flags.includes(O_WRITE)) {
    callback(new Errors.EBADF('descriptor does not permit writing'));
  } else {
    fchmod_file(context, ofd, mode, callback);
  }
}

function chown(context, path, uid, gid, callback) {
  if(!isUint32(uid)) {
    return callback(new Errors.EINVAL('uid must be a valid integer', uid));
  }
  if(!isUint32(gid)) {
    return callback(new Errors.EINVAL('gid must be a valid integer', gid));
  }

  chown_file(context, path, uid, gid, callback);
}

function fchown(context, fd, uid, gid, callback) {
  if(!isUint32(uid)) {
    return callback(new Errors.EINVAL('uid must be a valid integer', uid));
  }
  if(!isUint32(gid)) {
    return callback(new Errors.EINVAL('gid must be a valid integer', gid));
  }

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else if(!ofd.flags.includes(O_WRITE)) {
    callback(new Errors.EBADF('descriptor does not permit writing'));
  } else {
    fchown_file(context, ofd, uid, gid, callback);
  }
}

function rename(context, oldpath, newpath, callback) {
  oldpath = normalize(oldpath);
  newpath = normalize(newpath);

  var oldParentPath = Path.dirname(oldpath);
  var newParentPath = Path.dirname(newpath);
  var oldName = Path.basename(oldpath);
  var newName = Path.basename(newpath);
  var oldParentDirectory, oldParentData;
  var newParentDirectory, newParentData;
  var ctime = Date.now();
  var fileNode;

  function update_times(error, result) {
    if(error) {
      callback(error);
    } else {
      fileNode = result;
      update_node_times(context, newpath, fileNode, { ctime: ctime }, callback);
    }
  }

  function read_new_directory(error) {
    if(error) {
      callback(error);
    } else {
      context.getObject(newParentData[newName].id, update_times);
    }
  }

  function update_old_parent_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      if(oldParentDirectory.id === newParentDirectory.id) {
        oldParentData = newParentData;
      }
      delete oldParentData[oldName];
      context.putObject(oldParentDirectory.data, oldParentData, read_new_directory);
    }
  }

  function update_new_parent_directory_data(error) {
    if(error) {
      callback(error);
    } else {
      newParentData[newName] = oldParentData[oldName];
      context.putObject(newParentDirectory.data, newParentData, update_old_parent_directory_data);
    }
  }

  function check_if_new_directory_exists(error, result) {
    if(error) {
      callback(error);
    } else {
      newParentData = result;
      if(Object.prototype.hasOwnProperty.call(newParentData, newName)) {
        remove_directory(context, newpath, update_new_parent_directory_data);
      } else {
        update_new_parent_directory_data();
      }
    }
  }

  function read_new_parent_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      newParentDirectory = result;
      context.getObject(newParentDirectory.data, check_if_new_directory_exists);
    }
  }

  function get_new_parent_directory(error, result) {
    if(error) {
      callback(error);
    } else {
      oldParentData = result;
      find_node(context, newParentPath, read_new_parent_directory_data);
    }
  }

  function read_parent_directory_data(error, result) {
    if(error) {
      callback(error);
    } else {
      oldParentDirectory = result;
      context.getObject(result.data, get_new_parent_directory);
    }
  }

  function unlink_old_file(error) {
    if(error) {
      callback(error);
    } else {
      unlink_node(context, oldpath, callback);
    }
  }

  function check_node_type(error, node) {
    if(error) {
      callback(error);
    } else if(node.type === NODE_TYPE_DIRECTORY) {
      find_node(context, oldParentPath, read_parent_directory_data);
    } else {
      link_node(context, oldpath, newpath, unlink_old_file);
    }
  }

  find_node(context, oldpath, check_node_type);
}

function symlink(context, srcpath, dstpath, type, callback) {
  // NOTE: we support passing the `type` arg, but ignore it.
  callback = arguments[arguments.length - 1];
  make_symbolic_link(context, srcpath, dstpath, callback);
}

function readlink(context, path, callback) {
  read_link(context, path, callback);
}

function lstat(context, path, callback) {
  function check_result(error, result) {
    if(error) {
      callback(error);
    } else {
      var stats = new Stats(path, result, context.name);
      callback(null, stats);
    }
  }

  lstat_file(context, path, check_result);
}

function truncate(context, path, length, callback) {
  // NOTE: length is optional
  callback = arguments[arguments.length - 1];
  length = length || 0;

  if(validateInteger(length, callback) !== length) return;

  truncate_file(context, path, length, callback);
}

function ftruncate(context, fd, length, callback) {
  // NOTE: length is optional
  callback = arguments[arguments.length - 1];
  length = length || 0;

  var ofd = openFiles.getOpenFileDescription(fd);
  if(!ofd) {
    callback(new Errors.EBADF());
  } else if(!ofd.flags.includes(O_WRITE)) {
    callback(new Errors.EBADF('descriptor does not permit writing'));
  } else {
    if(validateInteger(length, callback) !== length) return;
    ftruncate_file(context, ofd, length, callback);
  }
}

module.exports = {
  appendFile,
  access,
  chown,
  chmod,
  close,
  // copyFile - https://github.com/filerjs/filer/issues/436
  ensureRootDirectory: ensure_root_directory,
  exists,
  fchown,
  fchmod,
  // fdatasync - https://github.com/filerjs/filer/issues/653
  fgetxattr,
  fremovexattr,
  fsetxattr,
  fstat,
  fsync,
  ftruncate,
  futimes,
  getxattr,
  // lchown - https://github.com/filerjs/filer/issues/620
  // lchmod - https://github.com/filerjs/filer/issues/619
  link,
  lseek,
  lstat,
  mkdir,
  mkdtemp,
  mknod,
  open,
  readdir,
  read,
  readFile,
  readlink,
  // realpath - https://github.com/filerjs/filer/issues/85
  removexattr,
  rename,
  rmdir,
  setxattr,
  stat,
  symlink,
  truncate,
  // unwatchFile - implemented in interface.js
  unlink,
  utimes,
  // watch - implemented in interface.js
  // watchFile - implemented in interface.js
  writeFile,
  write
};
