// Copyright 2021 The Emscripten Authors.  All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License.  Both these licenses can be
// found in the LICENSE file.

// This file defines the file object of the new file system.
// Current Status: Work in Progress.
// See https://github.com/emscripten-core/emscripten/issues/15041.

#pragma once

#include <assert.h>
#include <emscripten/html5.h>
#include <mutex>
#include <optional>
#include <sys/stat.h>
#include <vector>
#include <wasi/api.h>

namespace wasmfs {

// Note: The general locking strategy for all Files is to only hold 1 lock at a
// time to prevent deadlock. This methodology can be seen in getDirs().

class Backend;
// This represents an opaque pointer to a Backend. A user may use this to
// specify a backend in file operations.
using backend_t = Backend*;
const backend_t NullBackend = nullptr;

class File : public std::enable_shared_from_this<File> {
public:
  enum FileKind { DataFileKind = 0, DirectoryKind, SymlinkKind };

  template<class T> bool is() const {
    static_assert(std::is_base_of<File, T>::value,
                  "File is not a base of destination type T");
    return int(kind) == int(T::expectedKind);
  }

  template<class T> std::shared_ptr<T> dynCast() {
    static_assert(std::is_base_of<File, T>::value,
                  "File is not a base of destination type T");
    if (int(kind) == int(T::expectedKind)) {
      return std::static_pointer_cast<T>(shared_from_this());
    } else {
      return nullptr;
    }
  }

  template<class T> std::shared_ptr<T> cast() {
    static_assert(std::is_base_of<File, T>::value,
                  "File is not a base of destination type T");
    assert(int(kind) == int(T::expectedKind));
    return std::static_pointer_cast<T>(shared_from_this());
  }

  ino_t getIno() {
    // Set inode number to the file pointer. This gives a unique inode number.
    // TODO: For security it would be better to use an indirect mapping.
    // Ensure that the pointer will not overflow an ino_t.
    static_assert(sizeof(this) <= sizeof(ino_t));
    return (ino_t)this;
  }

  backend_t getBackend() { return backend; }

  class Handle;
  Handle locked();
  std::optional<Handle> maybeLocked();

protected:
  File(FileKind kind, mode_t mode, backend_t backend)
    : kind(kind), mode(mode), backend(backend) {}
  // A mutex is needed for multiple accesses to the same file.
  std::recursive_mutex mutex;

  virtual size_t getSize() = 0;

  mode_t mode = 0; // User and group mode bits for access permission.

  time_t ctime = 0; // Time when the file node was last modified.
  time_t mtime = 0; // Time when the file content was last modified.
  time_t atime = 0; // Time when the content was last accessed.

  FileKind kind;

  // Reference to parent of current file node. This can be used to
  // traverse up the directory tree. A weak_ptr ensures that the ref
  // count is not incremented. This also ensures that there are no cyclic
  // dependencies where the parent and child have shared_ptrs that reference
  // each other. This prevents the case in which an uncollectable cycle occurs.
  std::weak_ptr<File> parent;

  // This specifies which backend a file is associated with.
  backend_t backend;
};

class DataFile : public File {
  // TODO: Allow backends to override the version of read with multiple iovecs
  // to make it possible to implement pipes. See #16269.
  virtual __wasi_errno_t read(uint8_t* buf, size_t len, off_t offset) = 0;
  virtual __wasi_errno_t
  write(const uint8_t* buf, size_t len, off_t offset) = 0;

  // TODO: Design a proper API for flushing files.
  virtual void flush() = 0;

public:
  static constexpr FileKind expectedKind = File::DataFileKind;
  DataFile(mode_t mode, backend_t backend)
    : File(File::DataFileKind, mode | S_IFREG, backend) {}
  DataFile(mode_t mode, backend_t backend, mode_t fileType)
    : File(File::DataFileKind, mode | fileType, backend) {}
  virtual ~DataFile() = default;

  class Handle;
  Handle locked();
};

class Directory : public File {
public:
  struct Entry {
    std::string name;
    std::shared_ptr<File> file;
  };

private:
  // Return the file with the given name or null if there is none.
  virtual std::shared_ptr<File> getEntry(const std::string& name) = 0;
  // Remove the file with the given name, returning `true` on success or if the
  // entry has already been removed.
  virtual bool removeEntry(const std::string& name) = 0;
  // Insert the given file with the given name if there is not already an entry
  // with the same name. Returns the inserted file or the preexisting file or
  // null if the file could not be inserted and there was also no preexisting
  // file.
  virtual std::shared_ptr<File> insertEntry(const std::string& name,
                                            std::shared_ptr<File> file) = 0;
  // Return the name of the file if it is contained within this directory or an
  // empty string if it is not.
  virtual std::string getName(std::shared_ptr<File> file) = 0;
  // The number of entries in this directory.
  virtual size_t getNumEntries() = 0;
  // The list of entries in this directory.
  virtual std::vector<Directory::Entry> getEntries() = 0;

public:
  static constexpr FileKind expectedKind = File::DirectoryKind;
  Directory(mode_t mode, backend_t backend)
    : File(File::DirectoryKind, mode | S_IFDIR, backend) {}
  virtual ~Directory() = default;

  class Handle;
  Handle locked();
  std::optional<Handle> maybeLocked();

protected:
  // 4096 bytes is the size of a block in ext4.
  // This value was also copied from the JS file system.
  size_t getSize() override { return 4096; }
};

class Symlink : public File {
protected:
  // The target file that this symlink points to. This is constant as symlinks
  // cannot be modified to point to different things.
  const std::string target;

  size_t getSize() override;

public:
  static constexpr FileKind expectedKind = File::SymlinkKind;
  // Note that symlinks provide a mode of 0 to File. The mode of a symlink does
  // not matter, so that value will never be read (what matters is the mode of
  // the target).
  Symlink(std::string target, backend_t backend)
    : File(File::SymlinkKind, 0, backend), target(target) {}
  virtual ~Symlink() = default;

  // Constant, and therefore thread-safe, and can be done without locking.
  const std::string& getTarget() { return target; }
};

class File::Handle {
protected:
  // This mutex is needed when one needs to access access a previously locked
  // file in the same thread. For example, rename will need to traverse
  // 2 paths and access the same locked directory twice.
  // TODO: During benchmarking, test recursive vs normal mutex performance.
  std::unique_lock<std::recursive_mutex> lock;
  std::shared_ptr<File> file;

public:
  Handle(std::shared_ptr<File> file) : file(file), lock(file->mutex) {}
  Handle(std::shared_ptr<File> file, std::defer_lock_t)
    : file(file), lock(file->mutex, std::defer_lock) {}
  bool trylock() { return lock.try_lock(); }
  size_t getSize() { return file->getSize(); }
  mode_t& mode() { return file->mode; }
  time_t& ctime() { return file->ctime; }
  time_t& mtime() { return file->mtime; }
  time_t& atime() { return file->atime; }

  // Note: parent.lock() creates a new shared_ptr to the same Directory
  // specified by the parent weak_ptr.
  std::shared_ptr<File> getParent() { return file->parent.lock(); }
  void setParent(std::shared_ptr<File> parent) { file->parent = parent; }

  std::shared_ptr<File> unlocked() { return file; }
};

class DataFile::Handle : public File::Handle {
  std::shared_ptr<DataFile> getFile() { return file->cast<DataFile>(); }

public:
  Handle(std::shared_ptr<File> dataFile) : File::Handle(dataFile) {}
  Handle(Handle&&) = default;

  __wasi_errno_t read(uint8_t* buf, size_t len, off_t offset) {
    return getFile()->read(buf, len, offset);
  }
  __wasi_errno_t write(const uint8_t* buf, size_t len, off_t offset) {
    return getFile()->write(buf, len, offset);
  }

  // TODO: Design a proper API for flushing files.
  void flush() { getFile()->flush(); }

  // This function loads preloaded files from JS Memory into this DataFile.
  // TODO: Make this virtual so specific backends can specialize it for better
  // performance.
  void preloadFromJS(int index);
};

class Directory::Handle : public File::Handle {
  std::shared_ptr<Directory> getDir() { return file->cast<Directory>(); }

public:
  Handle(std::shared_ptr<File> directory) : File::Handle(directory) {}
  Handle(std::shared_ptr<File> directory, std::defer_lock_t)
    : File::Handle(directory, std::defer_lock) {}

  std::shared_ptr<File> getEntry(const std::string& name) {
    return getDir()->getEntry(name);
  }
  bool removeEntry(const std::string& name);
  std::shared_ptr<File> insertEntry(const std::string& name,
                                    std::shared_ptr<File> file);
  std::string getName(std::shared_ptr<File> file) {
    return getDir()->getName(file);
  }
  size_t getNumEntries() { return getDir()->getNumEntries(); }
  std::vector<Directory::Entry> getEntries() { return getDir()->getEntries(); }
};

inline File::Handle File::locked() { return Handle(shared_from_this()); }

inline std::optional<File::Handle> File::maybeLocked() {
  auto handle = Handle(shared_from_this(), std::defer_lock);
  if (handle.trylock()) {
    return Handle(shared_from_this());
  } else {
    return {};
  }
}

inline DataFile::Handle DataFile::locked() {
  return Handle(shared_from_this());
}

inline Directory::Handle Directory::locked() {
  return Handle(shared_from_this());
}

inline std::optional<Directory::Handle> Directory::maybeLocked() {
  auto handle = Handle(shared_from_this(), std::defer_lock);
  if (handle.trylock()) {
    return Handle(shared_from_this());
  } else {
    return {};
  }
}

} // namespace wasmfs
