#include <hilbert/kernel/vfile.hpp>

//TODO: handle symlink loops nicely in vfile::get_child,
//      vfile::get_children, and lookup_path.

namespace hilbert::kernel::vfile {

  void canon_path::parent() {
    if (segments.count != 0)
      --segments.count;
    else if (!absolute)
      ++parent_count;
  }

  void canon_path::rel(const canon_path &r) {
    if (r.absolute) {
      segments.count = 0;
      absolute = true;
      parent_count = 0;
    }
    for (unsigned i = 0; i < r.parent_count; ++i)
      parent();
    for (unsigned i = 0; i < r.segments.count; ++i)
      segments.add_end(r.segments.buffer[i]);
  }

  void canonize_path(const utility::string &name, canon_path &out) {

    out.absolute = false;
    out.parent_count = 0;
    out.segments.count = 0;

    const char *str = name.buffer;
    unsigned len = name.count;

    if (len == 0)
      return;

    if (len == 1 && str[0] == '/') {
      out.absolute = true;
      return;
    }

    if (str[0] == '/') {
      out.absolute = true;
      ++str;
      --len;
    }

    while (len != 0) {

      unsigned segment_len = utility::find(str, len, '/');
      unsigned to_skip = segment_len == len ? segment_len : segment_len + 1;

      if (segment_len == 0)
        ;

      else if (segment_len == 1 && str[0] == '.')
        ;

      else if (segment_len == 2 && str[0] == '.' && str[1] == '.')
        out.parent();

      else {
        utility::string segment(str, segment_len);
        out.segments.add_end(utility::move(segment));
      }

      str += to_skip;
      len -= to_skip;

    }

  }

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

  storage::fs_result vfile::follow_symlinks(vfile &out) const {

    if (dir_entry.type != storage::file_type::symlink) {
      out = *this;
      return storage::fs_result::success;
    }

    canon_path target_path;
    canonize_path(dir_entry.target, target_path);
    canon_path full_path = path;
    full_path.parent();
    full_path.rel(target_path);

    vfile next;
    RET_NOT_SUC(look_up_path(full_path, next, false))

    next.path = path;
    return next.follow_symlinks(out);

  }

  storage::fs_result vfile::get_child(
    vfile &out, const utility::string &name
  ) const {

    storage::dir_entry entry;
    storage::directory_iter_t iter;

    RET_NOT_SUC(bd->mounted_as->get_first_child(dir_entry.node, entry, iter))

    while (true) {

      if (entry.name == name) {

        vfile vf;
        vf.bd = bd;
        vf.dir_entry = utility::move(entry);
        vf.path = path;
        vf.path.segments.add_end(name);
        out = utility::move(vf);
        return storage::fs_result::success;

      }

      RET_NOT_SUC(bd->mounted_as->get_next_child(dir_entry.node, entry, iter))

    }

  }

  storage::fs_result vfile::get_children(utility::vector<vfile> &out) const {

    storage::dir_entry entry;
    storage::directory_iter_t iter;

    storage::fs_result result =
      bd->mounted_as->get_first_child(dir_entry.node, entry, iter);
    if (result == storage::fs_result::does_not_exist)
      return storage::fs_result::success;
    else if (result != storage::fs_result::success)
      return result;

    while (true) {

      vfile vf;
      vf.bd = bd;
      vf.path = path;
      vf.path.segments.add_end(entry.name);
      vf.dir_entry = utility::move(entry);
      out.add_end(utility::move(vf));

      result = bd->mounted_as->get_next_child(dir_entry.node, entry, iter);
      if (result == storage::fs_result::does_not_exist)
        return storage::fs_result::success;
      else if (result != storage::fs_result::success)
        return result;

    }

  }

  storage::fs_result vfile::read_file(
    uint64_t start, uint64_t length, void *into
  ) const {
    return bd->mounted_as->read_bytes_from_file(
      dir_entry.node, start, length, into);
  }

  //TODO: see comment at top of vfile.hpp.
  static const vfile *root;

  void set_root(const vfile &root) {
    kernel::vfile::root = new vfile(root);
  }

  storage::fs_result look_up_path(
    const canon_path &path, vfile &out, bool follow_final_symlink
  ) {

    //assume path is absolute.

    out = *root;
    for (unsigned i = 0; i < path.segments.count; ++i) {

      vfile result;
      RET_NOT_SUC(out.follow_symlinks(result))
      out = utility::move(result);

      RET_NOT_SUC(out.get_child(result, path.segments.buffer[i]))
      out = utility::move(result);

    }

    if (follow_final_symlink) {
      vfile result;
      RET_NOT_SUC(out.follow_symlinks(result))
      out = utility::move(result);
    }

    return storage::fs_result::success;

  }

}