#include <hilbert/kernel/storage/fs/tarfs.hpp>

//in tarfs_instance, node_id_t and directory_iter_t refer to the number
//of bytes into the block device that the info sector is located.

namespace hilbert::kernel::storage::fs {

  #define BD_TO_FS(expr) \
    { \
      bd_result _result = expr; \
      if (_result == bd_result::out_of_bounds) \
        return fs_result::fs_corrupt; \
      if (_result == bd_result::device_error) \
        return fs_result::device_error; \
    }

  #define FS_TO_FS(expr) \
    { \
      fs_result _result = expr; \
      if (_result != fs_result::success) \
        return _result; \
    }

  tarfs_instance::tarfs_instance(block_device *bd) : bd(bd) {}

  fs_result tarfs_instance::next_node(node_id_t node, node_id_t &out) {

    uint64_t bytes;
    FS_TO_FS(read_num(node + 124, 12, bytes))
    out = node + ((bytes - 1) / 512 + 2) * 512;

    uint8_t sector[512];
    BD_TO_FS(bd->read_bytes(node, 512, sector))
    for (unsigned i = 0; i < 512; ++i)
      if (sector[i] != 0)
        return fs_result::success;

    return fs_result::does_not_exist;

  }

  fs_result tarfs_instance::read_full_name(
    node_id_t node, utility::string &out
  ) {

    out.count = 0;
    out.verify_buffer_len(155);
    BD_TO_FS(bd->read_bytes(node + 345, 155, out.buffer))

    while (out.count < 155 && out.buffer[out.count] != '\0')
      ++out.count;

    unsigned new_max = out.count + 100;
    out.verify_buffer_len(new_max);
    BD_TO_FS(bd->read_bytes(node, 100, out.buffer + out.count))

    while (out.count < 255 && out.buffer[out.count] != '\0')
      ++out.count;

    return fs_result::success;

  }

  //len <= 12.
  fs_result tarfs_instance::read_num(
    uint64_t offset, unsigned len, uint64_t &out
  ) {

    char buf[12];
    BD_TO_FS(bd->read_bytes(offset, len, buf))

    out = 0;
    for (unsigned i = 0; i < len && buf[i] != '\0'; ++i) {
      if (buf[i] < '0' || buf[i] > '7')
        return fs_result::fs_corrupt;
      out = out * 8 + buf[i] - '0';
    }

    return fs_result::success;

  }

  fs_result tarfs_instance::first_child_starting_at(
    node_id_t parent, node_id_t start, node_id_t &out
  ) {

    utility::string parent_full_name;
    FS_TO_FS(read_full_name(parent, parent_full_name))

    utility::string child_full_name;
    out = start;

    while (true) {

      FS_TO_FS(read_full_name(out, child_full_name))

      if (child_full_name.count > parent_full_name.count &&
          child_full_name.starts_with(parent_full_name)
      ) {
        if (child_full_name.buffer[child_full_name.count - 1] == '/')
          --child_full_name.count;
        for (unsigned i = parent_full_name.count;
             i < child_full_name.count; ++i)
          if (child_full_name.buffer[i] == '/')
            goto next;
        return fs_result::success;
      }

    next:
      FS_TO_FS(next_node(out, out))

    }

  }

  fs_result tarfs_instance::get_dir_entry(node_id_t node, dir_entry &entry) {

    utility::string full_name;
    read_full_name(node, full_name);

    if (full_name.count == 2)
      entry.name.count = 0;

    else {

      if (full_name.buffer[full_name.count - 1] == '/')
        --full_name.count;

      unsigned last_slash =
        utility::find_last(full_name.buffer, full_name.count, '/');
      entry.name.count = full_name.count - last_slash - 1;
      entry.name.verify_buffer_len(entry.name.count);

      for (unsigned i = 0; i < entry.name.count; ++i)
        entry.name.buffer[i] = full_name.buffer[last_slash + 1 + i];

    }

    entry.node = node;

    char ch;
    BD_TO_FS(bd->read_bytes(node + 156, 1, &ch));
    switch (ch) {
      case '0':
        entry.type = file_type::regular_file;
        break;
      case '2':
        entry.type = file_type::symlink;
        break;
      case '5':
        entry.type = file_type::directory;
        break;
      default:
        return fs_result::fs_corrupt;
    }

    if (entry.type == file_type::regular_file) {
      uint64_t length;
      FS_TO_FS(read_num(node + 124, 12, length))
      entry.length = length;
    }

    else if (entry.type == file_type::symlink) {
      utility::string target;
      target.verify_buffer_len(100);
      BD_TO_FS(bd->read_bytes(node + 157, 100, target.buffer))
      while (target.count < 100 && target.buffer[target.count] != '\0')
        ++target.count;
      entry.target = utility::move(target);
    }

    return fs_result::success;

  }

  fs_result tarfs_instance::get_root_node(node_id_t &out) {

    utility::string full_name;
    node_id_t on = 0;

    while (true) {

      FS_TO_FS(read_full_name(on, full_name))
      if (full_name.count == 2) {
        out = on;
        return fs_result::success;
      }
      fs_result result = next_node(on, on);
      if (result == fs_result::does_not_exist)
        return fs_result::fs_corrupt;
      FS_TO_FS(result)

    }

  }

  fs_result tarfs_instance::get_first_child(
    node_id_t node, dir_entry &out, directory_iter_t &iter_out
  ) {

    node_id_t child;
    FS_TO_FS(first_child_starting_at(node, 0, child))

    dir_entry entry;
    FS_TO_FS(get_dir_entry(child, entry))
    out = utility::move(entry);
    iter_out = (directory_iter_t)child;
    return fs_result::success;

  }

  fs_result tarfs_instance::get_next_child(
    node_id_t node, dir_entry &out, directory_iter_t &iter
  ) {

    node_id_t start;
    FS_TO_FS(next_node((node_id_t)iter, start))

    node_id_t child;
    FS_TO_FS(first_child_starting_at(node, start, child))

    dir_entry entry;
    FS_TO_FS(get_dir_entry(child, entry))
    out = utility::move(entry);
    iter = (directory_iter_t)child;
    return fs_result::success;

  }

  fs_result tarfs_instance::read_bytes_from_file(
    node_id_t node, uint64_t start, uint64_t count, void *into
  ) {
    BD_TO_FS(bd->read_bytes(node + 512 + start, count, into))
    return fs_result::success;
  }


}
