#include <assert.h>
#include <errno.h>
#include <fs/vfs.h>
#include <mmu.h>
#include <poll.h>

vfs_inode_t *root_dir;
vfs_mounts_t mounts[10];
int num_mounts = 0;

vfs_fd_t *get_vfs_fd(int fd) {
  if (fd >= 100) {
    klog("get_vfs_fd(): Tried to get out of range fd", LOG_WARN);
    dump_backtrace(12);
    return NULL;
  }
  if (fd < 0) {
    klog("get_vfs_fd(): Tried to get out of range fd", LOG_WARN);
    dump_backtrace(12);
    return NULL;
  }
  return get_current_task()->file_descriptors[fd];
}

vfs_inode_t *vfs_create_inode(
    int inode_num, int type, uint8_t has_data, uint8_t can_write,
    uint8_t is_open, void *internal_object, uint64_t file_size,
    vfs_inode_t *(*open)(const char *path),
    int (*create_file)(const char *path, int mode),
    int (*read)(uint8_t *buffer, uint64_t offset, uint64_t len, vfs_fd_t *fd),
    int (*write)(uint8_t *buffer, uint64_t offset, uint64_t len, vfs_fd_t *fd),
    void (*close)(vfs_fd_t *fd),
    int (*create_directory)(const char *path, int mode),
    vfs_vm_object_t *(*get_vm_object)(uint64_t length, uint64_t offset,
                                      vfs_fd_t *fd),
    int (*truncate)(vfs_fd_t *fd, size_t length)) {
  vfs_inode_t *r = kmalloc(sizeof(inode_t));
  r->inode_num = inode_num;
  r->type = type;
  r->has_data = has_data;
  r->can_write = can_write;
  r->is_open = is_open;
  r->internal_object = internal_object;
  r->file_size = file_size;
  r->open = open;
  r->create_file = create_file;
  r->read = read;
  r->write = write;
  r->close = close;
  r->create_directory = create_directory;
  r->get_vm_object = get_vm_object;
  r->truncate = truncate;
  return r;
}

int vfs_create_fd(int flags, int mode, vfs_inode_t *inode, vfs_fd_t **fd) {
  process_t *p = (process_t *)get_current_task();
  int i;
  for (i = 0; i < 100; i++)
    if (!p->file_descriptors[i])
      break;
  if (p->file_descriptors[i])
    return -1;
  vfs_fd_t *r = kmalloc(sizeof(vfs_fd_t));
  r->flags = flags;
  r->mode = mode;
  r->inode = inode;
  r->reference_count = 1;
  p->file_descriptors[i] = r;
  if (fd)
    *fd = r;
  return i;
}

int vfs_create_file(const char *file) {
  vfs_mounts_t *file_mount = 0;
  int length = 0;
  for (int i = 0; i < num_mounts; i++) {
    int path_len = strlen(mounts[i].path);
    if (path_len <= length)
      continue;

    if (isequal_n(mounts[i].path, file, path_len)) {
      length = path_len;
      file_mount = &mounts[i];
    }
  }
  if (1 != length)
    file += length;

  if (!file_mount) {
    kprintf("vfs_internal_open could not find mounted path for file : %s\n",
            file);
    return 0;
  }
  //  ext2_create_file("/etc/oscreated", 0);
  assert(file_mount->local_root->create_file);
  kprintf("Creating a file\n");
  return file_mount->local_root->create_file(file, 0);
}

vfs_inode_t *vfs_internal_open(const char *file) {
  vfs_mounts_t *file_mount = 0;
  int length = 0;
  for (int i = 0; i < num_mounts; i++) {
    int path_len = strlen(mounts[i].path);
    if (path_len <= length)
      continue;

    if (isequal_n(mounts[i].path, file, path_len)) {
      length = path_len;
      file_mount = &mounts[i];
    }
  }
  if (1 != length)
    file += length;

  if (!file_mount) {
    kprintf("vfs_internal_open could not find mounted path for file : %s\n",
            file);
    return NULL;
  }

  vfs_inode_t *ret = file_mount->local_root->open(file);
  return ret;
}

char *vfs_clean_path(const char *path, char *resolved_path) {
  //  char *const clean = kmalloc(strlen(path) + 1);
  char *clean = resolved_path;
  int prev_slash = 0;
  char *ptr = clean;
  for (; *path; path++) {
    if (prev_slash && '/' == *path) {
      continue;
    }
    prev_slash = ('/' == *path);
    *ptr = *path;
    ptr++;
  }
  *ptr = '\0';
  return clean;
}

char *vfs_resolve_path(const char *file, char *resolved_path) {
  if ('/' == *file) {
    return vfs_clean_path(file, resolved_path);
  }
  const char *cwd = get_current_task()->current_working_directory;
  size_t l = strlen(cwd);
  assert(l > 0);
  assert('/' == cwd[l - 1]);
  //  char *r = kmalloc(l + strlen(file) + 1);
  char r[256];
  strcpy(r, cwd);
  strcat(r, file);
  char *final = vfs_clean_path(r, resolved_path);
  //  kfree(r);
  return final;
}

int vfs_mkdir(const char *path, int mode) {
  vfs_mounts_t *file_mount = 0;
  int length = 0;
  for (int i = 0; i < num_mounts; i++) {
    int path_len = strlen(mounts[i].path);
    if (path_len <= length)
      continue;

    if (isequal_n(mounts[i].path, path, path_len)) {
      length = path_len;
      file_mount = &mounts[i];
    }
  }
  if (1 != length)
    path += length;

  if (!file_mount) {
    kprintf("vfs_internal_open could not find mounted path for file : %s\n",
            path);
    return 0;
  }
  assert(file_mount->local_root->create_directory);
  // TODO: Error checking, don't just assume it is fine
  file_mount->local_root->create_directory(path, mode);
  return 0;
}

int vfs_open(const char *file, int flags, int mode) {
  char resolved_path[256] = {0};
  vfs_resolve_path(file, resolved_path);
  vfs_inode_t *inode = vfs_internal_open(resolved_path);
  if (0 == inode) {
    if (mode & O_CREAT) {
      if (vfs_create_file(resolved_path)) {
        klog("VFS: File created", LOG_NOTE);
        return vfs_open(file, flags, mode);
      }
      klog("VFS: Could not create file", LOG_WARN);
    }
    return -ENOENT;
  }
  if (inode->type == FS_TYPE_UNIX_SOCKET) {
    return uds_open(resolved_path);
  }

  return vfs_create_fd(flags, mode, inode, NULL);
}

int vfs_close(int fd) {
  vfs_fd_t *fd_ptr = get_vfs_fd(fd);
  if (NULL == fd_ptr) {
    return -1;
  }
  assert(0 < fd_ptr->reference_count);
  // Remove process reference
  fd_ptr->reference_count--;
  get_current_task()->file_descriptors[fd] = 0;
  // If no references left then free the contents
  if (0 == fd_ptr->reference_count) {
    if (fd_ptr->inode->close)
      fd_ptr->inode->close(fd_ptr);

    kfree(fd_ptr);
  }
  return 0;
}

int raw_vfs_pread(vfs_fd_t *vfs_fd, void *buf, uint64_t count,
                  uint64_t offset) {
  if (!(vfs_fd->flags & O_READ))
    return -EBADF;
  return vfs_fd->inode->read(buf, offset, count, vfs_fd);
}

int vfs_pread(int fd, void *buf, uint64_t count, uint64_t offset) {
  if (fd >= 100) {
    kprintf("EBADF : %x\n", fd);
    return -EBADF;
  }
  if (fd < 0) {
    dump_backtrace(12);
    kprintf("EBADF : %x\n", fd);
    return -EBADF;
  }
  vfs_fd_t *vfs_fd = get_current_task()->file_descriptors[fd];
  if (!vfs_fd)
    return -EBADF;
  int rc = raw_vfs_pread(vfs_fd, buf, count, offset);
  if (-EAGAIN == rc && count > 0) {
    if (!(vfs_fd->flags & O_NONBLOCK)) {
      struct pollfd fds;
      fds.fd = fd;
      fds.events = POLLIN;
      fds.revents = 0;
      poll(&fds, 1, 0);
      return vfs_pread(fd, buf, count, offset);
    }
  }
  return rc;
}

int raw_vfs_pwrite(vfs_fd_t *vfs_fd, void *buf, uint64_t count,
                   uint64_t offset) {
  assert(vfs_fd);
  assert(vfs_fd->inode);
  assert(vfs_fd->inode->write);
  return vfs_fd->inode->write(buf, offset, count, vfs_fd);
}

int vfs_pwrite(int fd, void *buf, uint64_t count, uint64_t offset) {
  vfs_fd_t *vfs_fd = get_vfs_fd(fd);
  if (!vfs_fd)
    return -EBADF;
  if (!(vfs_fd->flags & O_WRITE)) {
    return -EBADF;
  }
  return raw_vfs_pwrite(vfs_fd, buf, count, offset);
}

vfs_vm_object_t *vfs_get_vm_object(int fd, uint64_t length, uint64_t offset) {
  vfs_fd_t *vfs_fd = get_vfs_fd(fd);
  if (!vfs_fd)
    return NULL;
  vfs_vm_object_t *r = vfs_fd->inode->get_vm_object(length, offset, vfs_fd);
  return r;
}

int vfs_dup2(int org_fd, int new_fd) {
  get_current_task()->file_descriptors[new_fd] =
      get_current_task()->file_descriptors[org_fd];
  get_current_task()->file_descriptors[new_fd]->reference_count++;
  return 1;
}

int vfs_ftruncate(int fd, size_t length) {
  vfs_fd_t *fd_ptr = get_vfs_fd(fd);
  if (!fd_ptr)
    return -EBADF;
  if (!(fd_ptr->flags & O_READ))
    return -EINVAL;
  vfs_inode_t *inode = fd_ptr->inode;
  if (!inode)
    return -EINVAL;
  if (!inode->truncate)
    return -EINVAL;

  return inode->truncate(fd_ptr, length);
}

void vfs_mount(char *path, vfs_inode_t *local_root) {
  int len = strlen(path);
  mounts[num_mounts].path = kmalloc_eternal(len + 1);
  memcpy(mounts[num_mounts].path, path, len);
  mounts[num_mounts].path[len] = '\0';
  mounts[num_mounts].local_root = local_root;
  num_mounts++;
}