// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
//
// SPDX-License-Identifier: MIT

import {
  dircookie,
  errno,
  fd,
  fd_readdir,
  fdflags,
  filestat,
  lookupflags,
  oflags,
  path_filestat_get,
  path_open,
  rights,
} from "@assemblyscript/wasi-shim/assembly/bindings/wasi_snapshot_preview1";
import { Descriptor } from "as-wasi/assembly";
import {
  _copyFile,
  _deleteFile,
  _moveFile,
  _replaceContent,
  _writeContent,
} from "./imports";
import { errorOrNull } from "./wasm";

export const FsBaseWorktree = "worktree";
export const FsBaseWebcontent = "webcontent";
export const FsBaseCache = "cache";

export class FS {
  private fs: FileSystem = new FileSystem(3);
  private base: string;

  constructor(base: string) {
    this.base = base;
  }

  readDir(path: string): string[] | null {
    const dirs = this.fs.readdir(this.base + "/" + path);
    if (dirs == null) {
      return [];
    }
    return dirs;
  }

  readAll(path: string): string | null {
    const fd = this.fs.open(this.base + "/" + path);
    if (fd == null) {
      return null;
    }
    const data = fd.readString();
    if (data == null) {
      return null;
    }
    fd.close();
    return data;
  }

  readLine(path: string): File | null {
    const fd = this.fs.open(this.base + "/" + path);
    if (fd == null) {
      return null;
    }
    return new File(fd);
  }

  exists(path: string): bool {
    return this.fs.exists(this.base + "/" + path);
  }

  writeContent(path: string, content: string): Error | null {
    return errorOrNull(_writeContent(this.base, path, content));
  }

  replaceContent(
    path: string,
    oldContent: string,
    content: string,
  ): Error | null {
    return errorOrNull(_replaceContent(this.base, path, oldContent, content));
  }

  copyFile(fromPath: string, toPath: string): Error | null {
    return errorOrNull(_copyFile(this.base, fromPath, this.base, toPath));
  }

  copyFileToFs(fromPath: string, toFs: FS, toPath: string): Error | null {
    return errorOrNull(_copyFile(this.base, fromPath, toFs.base, toPath));
  }

  deleteFile(filename: string): Error | null {
    return errorOrNull(_deleteFile(this.base, filename));
  }

  moveFile(fromPath: string, toPath: string): Error | null {
    return errorOrNull(_moveFile(this.base, fromPath, this.base, toPath));
  }

  moveFileToFs(fromPath: string, toFs: FS, toPath: string): Error | null {
    return errorOrNull(_moveFile(this.base, fromPath, toFs.base, toPath));
  }
}

class File {
  constructor(private fd: Descriptor) {}

  readLine(): string | null {
    return this.fd.readLine();
  }

  close(): void {
    this.fd.close();
  }
}

/**
 * ######################################
 * COPIED FROM AS-WASI
 * ######################################
 */

/**
 * A class to access a filesystem
 */
class FileSystem {
  constructor(private dirfd: u32) {}

  /**
   * Open a path
   * @path path
   * @flags r, r+, w, wx, w+ or xw+
   * @returns a descriptor
   */
  open(path: string, flags: string = "r"): Descriptor | null {
    let dirfd = this.dirfdForPath(path);
    let fd_lookup_flags = lookupflags.SYMLINK_FOLLOW;
    let fd_oflags: u16 = 0;
    let fd_rights: u64 = 0;
    if (flags == "r") {
      fd_rights =
        rights.FD_READ |
        rights.FD_SEEK |
        rights.FD_TELL |
        rights.FD_FILESTAT_GET |
        rights.FD_READDIR;
    } else if (flags == "r+") {
      fd_rights =
        rights.FD_WRITE |
        rights.FD_READ |
        rights.FD_SEEK |
        rights.FD_TELL |
        rights.FD_FILESTAT_GET |
        rights.PATH_CREATE_FILE;
    } else if (flags == "w") {
      fd_oflags = oflags.CREAT | oflags.TRUNC;
      fd_rights =
        rights.FD_WRITE |
        rights.FD_SEEK |
        rights.FD_TELL |
        rights.FD_FILESTAT_GET |
        rights.PATH_CREATE_FILE;
    } else if (flags == "wx") {
      fd_oflags = oflags.CREAT | oflags.TRUNC | oflags.EXCL;
      fd_rights =
        rights.FD_WRITE |
        rights.FD_SEEK |
        rights.FD_TELL |
        rights.FD_FILESTAT_GET |
        rights.PATH_CREATE_FILE;
    } else if (flags == "w+") {
      fd_oflags = oflags.CREAT | oflags.TRUNC;
      fd_rights =
        rights.FD_WRITE |
        rights.FD_READ |
        rights.FD_SEEK |
        rights.FD_TELL |
        rights.FD_FILESTAT_GET |
        rights.PATH_CREATE_FILE;
    } else if (flags == "xw+") {
      fd_oflags = oflags.CREAT | oflags.TRUNC | oflags.EXCL;
      fd_rights =
        rights.FD_WRITE |
        rights.FD_READ |
        rights.FD_SEEK |
        rights.FD_TELL |
        rights.FD_FILESTAT_GET |
        rights.PATH_CREATE_FILE;
    } else {
      return null;
    }
    let fd_rights_inherited = fd_rights;
    let fd_flags: fdflags = 0;
    let path_utf8_buf = String.UTF8.encode(path);
    let path_utf8_len: usize = path_utf8_buf.byteLength;
    let path_utf8 = changetype<usize>(path_utf8_buf);
    let fd_buf = memory.data(8);
    let res = path_open(
      dirfd as fd,
      fd_lookup_flags,
      path_utf8,
      path_utf8_len,
      fd_oflags,
      fd_rights,
      fd_rights_inherited,
      fd_flags,
      fd_buf,
    );
    if (res !== errno.SUCCESS) {
      return null;
    }
    let fd = load<u32>(fd_buf);
    return new Descriptor(fd);
  }

  /**
   * Check if a file exists at a given path
   * @path path
   * @returns `true` on success, `false` on failure
   */
  exists(path: string): bool {
    let dirfd = this.dirfdForPath(path);
    let path_utf8_buf = String.UTF8.encode(path);
    let path_utf8_len: usize = path_utf8_buf.byteLength;
    let path_utf8 = changetype<usize>(path_utf8_buf);
    let fd_lookup_flags = lookupflags.SYMLINK_FOLLOW;
    let st_buf = changetype<usize>(new ArrayBuffer(64));
    let res = path_filestat_get(
      dirfd,
      fd_lookup_flags,
      path_utf8,
      path_utf8_len,
      changetype<filestat>(st_buf),
    );
    return res === errno.SUCCESS;
  }

  /**
   * Get the content of a directory
   * @param path the directory path
   * @returns An array of file names
   */
  readdir(path: string): Array<string> | null {
    let fd = this.open(path, "r");
    if (fd === null) {
      return null;
    }
    let out = new Array<string>();
    let buf_size = 4096;
    // @ts-ignore
    let buf = heap.alloc(buf_size);
    // @ts-ignore
    let buf_used_p = memory.data(8);
    let buf_used = 0;
    for (;;) {
      if (
        fd_readdir(fd.rawfd, buf, buf_size, 0 as dircookie, buf_used_p) !==
        errno.SUCCESS
      ) {
        fd.close();
      }
      buf_used = load<u32>(buf_used_p);
      if (buf_used < buf_size) {
        break;
      }
      buf_size <<= 1;
      // @ts-ignore
      buf = heap.realloc(buf, buf_size);
    }
    let offset = 0;
    while (offset < buf_used) {
      offset += 16;
      let name_len = load<u32>(buf + offset);
      offset += 8;
      if (offset + name_len > buf_used) {
        return null;
      }
      let name = String.UTF8.decodeUnsafe(buf + offset, name_len);
      out.push(name);
      offset += name_len;
    }
    // @ts-ignore
    heap.free(buf);
    fd.close();

    return out;
  }

  dirfdForPath(path: string): fd {
    return this.dirfd;
  }
}
