// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Handle to an opened file
pub struct File {
  /// `text` is a builtin UTF-8 view of the file's content.
  /// It can be used for direct UTF-8 based text IO over the file.
  text : @io.Utf8TextView
  priv io : @event_loop.IoHandle
  priv reader_ctrl : @io.BinaryReaderController
  priv writer_ctrl : @io.BinaryWriterController
}

///|
/// Get the underlying file descriptor/Windows HANDLE of a file
pub fn File::fd(self : File) -> @fd_util.Fd {
  self.io.fd()
}

///|
/// Close an opened file handle, release associated resource
pub fn File::close(self : File) -> Unit {
  self.text.close()
  self.io.close()
}

///|
/// Access to request for when opening a file
pub(all) enum Mode {
  ReadOnly = 0
  WriteOnly
  ReadWrite
}

///|
/// Determine how data is synchronized after writing:
///
/// - `NoSync` means no synchronization is performed.
///
/// - `Data` means the data part and relevant metadata such as file size
///   will be synchronized immediately after a `write` call,
///   but other metadata, such as last access time, will not.
///   See man page of `fdatasync(2)` for more details.
///
/// - `Full` means all data and metdata will be synchronized
///   immediately after writing.
///   See man page of `fsync(2)` for more details.
pub(all) enum SyncMode {
  NoSync
  Data
  Full
}

///|
/// Specify how to handle file creation in `@fs.open()`.
///
/// - `OpenExisting`: open an existing file, fail if the file does not exist
/// - `TruncateExisting`: open an existing file and truncate it, fail if the file does not exist
/// - `OpenOrCreate`: if the file exists, open it without truncation. Otherwise create a new file
/// - `CreateOrTruncate`: if the file exists, open it and truncate it. Otherwise create a new file
/// - `CreateNew`: create a new file, fail if the file already exists
pub(all) enum CreateMode {
  /// open an existing file, fail if the file does not exist
  OpenExisting = 0
  /// open an existing file and truncate it, fail if the file does not exist
  TruncateExisting = 1
  /// if the file exists, open it without truncation. Otherwise create a new file
  OpenOrCreate = 2
  /// if the file exists, open it and truncate it. Otherwise create a new file
  CreateOrTruncate = 3
  /// create a new file, fail if the file already exists
  CreateNew = 4
}

///|
fn CreateMode::to_int(self : CreateMode) -> Int32 = "%identity"

///|
/// Open a file. `filename` will be encoded using UTF8.
///
/// - `mode` determines what operations (read/write) are permitted for the file.
///
/// - `sync` determines how data will be synchronized to disk after writing,
///   see `SyncMode` for more details. The default is `NoSync`
///
/// - if `append` is `true` (`false` by default), the file will be opened in append mode.
///   Under append mode, sequential write (via the `@io.BinaryWriter` interface)
///   will always write to the end of the file, even if the file is modified externally.
///   The semantic of reading from the file is unaffected by `append`.
///   Random access write (`write_at`) is forbidden for file opened in append mode.
///
/// - `create_mode` specify how to handle file creation, truncation, etc.
///   See the type `CreateMode` for more details.
///   The default value is `OpenExisting`.
///
/// - `permission` specifies the user permission of the new file,
///   if a new file is created by `@fs.open`.
///   The permission is represented as an integer in UNIX permission style.
///   For example, `0o640` means:
///
///   - the owner of the file can read and write the file (`6`)
///   - users in the owner group of the file can read the file (`4`)
///   - other users can do nothing to the file
///
///   The default value is `0o644` (anyone can read, only owner can write, not executable).
///   `permission` is currently ignored on Windows.
pub async fn open(
  filename : StringView,
  mode~ : Mode,
  sync~ : SyncMode = NoSync,
  append~ : Bool = false,
  create_mode~ : CreateMode = OpenExisting,
  permission~ : Int32 = 0o644,
) -> File {
  let access = match mode {
    ReadOnly => 0
    WriteOnly => 1
    ReadWrite => 2
  }
  let sync = match sync {
    NoSync => 0
    Data => 1
    Full => 2
  }
  let (io, _) = @event_loop.open(
    filename,
    access,
    create=create_mode.to_int(),
    append~,
    sync~,
    mode=permission,
    context="@fs.open()",
  )
  let file = {
    text: @io.Utf8TextView::new_uninitialized(),
    io,
    reader_ctrl: BinaryReaderController(),
    writer_ctrl: BinaryWriterController(),
  }
  file.text.set_inner(file)
  file
}

///|
/// Create a new file at `pathname`. `pathname` will be encoded using UTF8.
///
/// - If the file already exists and `allow_existing` is `true` (`true` by default),
///   the existing file will be truncated and opened.
///   If the file already exists and `allow_existing` is `fale`, this function will fail.
///
/// - User permission of the new file would be set to `permission`,
///   which is an integer in UNIX style. For example, `0o640` means:
///     - the owner of the file can read and write the file (`6`)
///     - users in the owner group of the file can read the file (`4`)
///     - other users can do nothing to the file
///   The default value is `0o644` (anyone can read, only owner can write, not executable).
///   `permission` is currently ignored on Windows.
///
/// - `sync` determines how data will be synchronized to disk after writing,
///   see `SyncMode` for more details. The default is `NoSync`
pub async fn create(
  filename : StringView,
  allow_existing~ : Bool = true,
  permission~ : Int32 = 0o644,
  sync~ : SyncMode = NoSync,
) -> File {
  let create_mode = if allow_existing { CreateOrTruncate } else { CreateNew }
  open(filename, mode=WriteOnly, sync~, create_mode~, permission~)
}

///|
/// When reading a file via the `@io.BinaryReader` interface,
/// the content of the file will be read as a byte stream.
/// The position of the read stream in the file is
/// independent of the write stream (via the `@io.BinaryWriter` interface)
pub impl @io.BinaryReader for File

///|
impl @io.BinaryReaderImpl for File with fn raw_read_impl(
  self,
  buf,
  offset~,
  max_len~,
) {
  self.io.read(
    buf,
    offset~,
    len=max_len,
    context="impl @io.BinaryReader for @fs.File",
  )
}

///|
impl @io.BinaryReaderImpl for File with fn get_controller(self) {
  self.reader_ctrl
}

///|
pub impl @io.BinaryReader for File with fn read_all(self) {
  guard self.io.kind() is Regular else { @io.read_all_binary(self) }
  let size = (self.size() - self.io.read_offset) catch {
    _ => return @io.read_all_binary(self)
  }
  guard size > 0 && size < @int.MAX_VALUE.to_int64() else {
    @io.read_all_binary(self)
  }
  let size_hint = size.to_int()
  let buf = FixedArray::make(size_hint, b'\x00')
  let n = self.read_into(buf, offset=0, max_len=size_hint)
  if n is 0 {
    return b""
  }
  // The only reliable way to detect EOF is `read` returning `0`,
  // even for regular files. So do a simple EOF probe here.
  let eof_probe = FixedArray::make(1, b'\x00')
  if self.read_into(eof_probe, offset=0, max_len=1) is 0 {
    // happy path, the file size is accurate
    return buf.unsafe_reinterpret_as_bytes()[:n].to_owned()
  }
  @io.read_all_binary(
    self,
    prefix_list=[
      buf.unsafe_reinterpret_as_bytes()[:n],
      eof_probe.unsafe_reinterpret_as_bytes(),
    ],
    chunk_size=1 << 16, // 64k
  )
}

///|
pub extend File with @io.BinaryReader::{
  read_into,
  read,
  read_at_least,
  peek,
  peek_at_least,
  read_exactly,
  drop_exactly,
  peek_exactly,
  read_all,
  drop_all,
  read_until,
  peek_until,
}

///|
/// Read from a file at the offset given by `position`.
/// Only seekable files (e.g. regular files and block devices) support this operation,
/// calling `read_at` on unsupported file, such as pipe or socket, result in error.
///
/// This function does not change the cursor for reading/writing the file
/// as a stream (i.e. using `@io.BinaryReader` and `@io.BinaryWriter` APIs),
/// so it is safe to perform multiple `read_at` on the same file simutaneously.
///
/// Up to `len` bytes of data will be read into `buf[offset:]`.
/// The number of bytes actually read would be returned.
/// The number of read bytes will be smaller than `len` only when EOF is reached.
pub async fn File::read_at(
  self : File,
  buf : FixedArray[Byte],
  position~ : Int64,
  offset~ : Int32 = 0,
  len~ : Int32 = buf.length() - offset,
) -> Int32 {
  self.io.read_at(buf, offset~, len~, position~, context="@fs.File::read_at()")
}

///|
/// Read exactly `len` bytes from a file, starting at offset `position`,
/// and return requested content.
/// If there is no enough data before EOF, an error will be raised.
///
/// Only seekable files (e.g. regular files and block devices) support this operation,
/// calling `read_at_exactly` on unsupported file, such as pipe or socket, result in error.
///
/// This function does not change the cursor for reading/writing the file
/// as a stream (i.e. using `@io.BinaryReader` and `@io.BinaryWriter` APIs),
/// so it is safe to perform multiple `read_at` on the same file simutaneously.
pub async fn File::read_exactly_at(
  self : File,
  len : Int32,
  position~ : Int64,
) -> Bytes {
  let buf = FixedArray::make(len, b'\x00')
  let n = self.io.read_at(
    buf,
    offset=0,
    len~,
    position~,
    context="@fs.File::read_exactly_at()",
  )
  if n < len {
    raise @io.ReaderClosed
  }
  buf.unsafe_reinterpret_as_bytes()
}

///|
/// If the file is opened with `append=true`,
/// writing to the file via the `@io.BinaryWriter` interface
/// always append content at the end of the file.
///
/// If the file is opened with `append=false` (the default),
/// content written to the file via the `@io.BinaryWriter` interface
/// will be written as a byte stream, starting at offset 0.
/// The position of the write stream in the file is
/// independent of the read stream (via the `@io.BinaryReader` interface)
pub impl @io.BinaryWriter for File

///|
impl @io.BinaryWriterImpl for File with fn raw_write_impl(
  self,
  buf,
  offset~,
  len~,
) {
  self.io.write(
    buf,
    offset~,
    len~,
    context="impl @io.BinaryWriter for @fs.File",
  )
}

///|
impl @io.BinaryWriterImpl for File with fn get_controller(self) {
  self.writer_ctrl
}

///|
pub extend File with @io.BinaryWriter::{write, write_reader}

///|
/// Write to a file at the offset given by `position`.
/// Only seekable files (e.g. regular files and block devices) support this operation,
/// calling `write_at` on unsupported file, such as pipe or socket, result in error.
///
/// This function does not change the cursor for reading/writing the file
/// as a stream (i.e. using `@io.BinaryReader` and `@io.BinaryWriter` APIs),
/// so it is safe to perform multiple `write_at` on the same file simutaneously,
/// as long as the regions they write do not overlap.
///
/// `write_at` would only return after all data is written.
pub async fn File::write_at(
  self : File,
  buf : BytesView,
  position~ : Int64,
) -> Unit {
  self.io.write_at(
    buf.data(),
    offset=buf.start_offset(),
    len=buf.length(),
    position~,
    context="@fs.File::write_at()",
  )
}

///|
/// Flush all in-memory data of an opened file to disk.
/// If `only_data` is `true` (`false` by default),
/// some metadata such as timestamp may not be flushed.
/// Buf file content and important metadata such as file size are always flushed.
pub async fn File::sync(self : File, only_data~ : Bool = false) -> Unit {
  self.io.fsync(only_data~, context="@fs.File::sync()")
}

///|
/// The kind of lock that can be acquired on a file.
/// `Exclusive` lock must be unique,
/// while multiple `Shared` lock can coexist.
/// `Exclusive` lock and `Shared` lock are mutually exclusive.
pub(all) enum Lock {
  Shared
  Exclusive
}

///|
/// Lock a file, block until the lock is successfully acquired.
///
/// The lock here is advisory, so normal read/write operations
/// on the file are not affected by the lock,
/// only another attempt to lock the same file may conflict with the lock here.
///
/// The underlying syscall used to implement the lock is undefined.
/// So the lock should only be used for synchronization between multiple process
/// written using `moonbitlang/async`.
pub async fn File::lock(self : File, lock : Lock) -> Unit {
  self.io.lock(exclusive=lock is Exclusive, context="@fs.File::lock()")
}

///|
#cfg(target="native")
extern "C" fn try_lock_ffi(fd : @fd_util.Fd, exclusive~ : Bool) -> Int32 = "moonbit_rt_try_lock_file"

///|
#cfg(target="wasm")
#unsafe_skip_stub_check
fn try_lock_ffi(fd : @fd_util.Fd, exclusive~ : Bool) -> Int32 = "moonbitlang/async" "fs/try_lock_file"

///|
/// Try to lock a file.
/// If the lock is successfully acquired, `true` is returned.
/// If the lock cannot be acquired immediately
/// (because an incompatible lock is held by another process),
/// `false` is returned immediately.
///
/// The lock here is advisory, so normal read/write operations
/// on the file are not affected by the lock,
/// only another attempt to lock the same file may conflict with the lock here.
///
/// The underlying syscall used to implement the lock is undefined.
/// So the lock should only be used for synchronization between multiple process
/// written using `moonbitlang/async`.
pub fn File::try_lock(self : File, lock : Lock) -> Bool raise {
  let ret = try_lock_ffi(self.fd(), exclusive=lock is Exclusive)
  if ret >= 0 {
    true
  } else if @errno_util.get_errno() == @errno_util.lock_violation {
    false
  } else {
    @os_error.check_errno("@fs.File::try_lock()")
    false
  }
}

///|
#cfg(target="native")
extern "C" fn unlock_ffi(fd : @fd_util.Fd) -> Int32 = "moonbit_rt_unlock_file"

///|
#cfg(target="wasm")
#unsafe_skip_stub_check
fn unlock_ffi(fd : @fd_util.Fd) -> Int32 = "moonbitlang/async" "fs/unlock_file"

///|
/// Unlock a file that was previously locked by this process.
pub fn File::unlock(self : File) -> Unit {
  if unlock_ffi(self.fd()) < 0 {
    // swallow the error so that we can use `unlock` in `defer`.
    // TODO: remove the `try!` after we support raising error in `defer`.
    try! @os_error.check_errno("@fs.File::unlock()")
  }
}

///|
/// Remove the file located at `path`.
/// Unless `allow_missing=true` (`false` by default),
/// an error will be raised if the target file does not exist.
///
/// This function is not cancellable.
pub async fn remove(
  path : StringView,
  allow_missing~ : Bool = false,
) -> Unit nocancel {
  @event_loop.remove(path, context="@fs.remove()") catch {
    @os_error.OsError(NotFound, ..) if allow_missing => ()
    err => raise err
  }
}

///|
/// Rename (move) the file located at `old_path` to `new_path`.
///
/// If `replace` is `true` (`true` by default),
/// and a file already exist on `new_path`, that file will be replaced.
/// In this case, open handles to the existing file remain valid,
/// and still point to the old file.
pub async fn rename(
  old_path : StringView,
  new_path : StringView,
  replace~ : Bool = true,
) -> Unit {
  @event_loop.rename(old_path, new_path, replace~, context="@fs.rename()")
}

///|
/// Get the size of the file. This method will not change position in the file.
/// Can only be applied to a regular file.
pub async fn File::size(self : File) -> Int64 {
  @event_loop.file_size(self.io.fd(), context="@fs.File::size()")
}

///|
/// Get the size of the file at the specific path.
/// Can only be applied to a regular file.
pub async fn file_size(path : StringView) -> Int64 {
  @event_loop.file_size_of_path(path, context="@fs.file_size()")
}

///|
/// Convert a file to directory.
/// If the file is not a directory, an error will be raised.
///
/// The ownership of the file will be transferred to `as_dir`.
/// The input file will be automatically closed when the
/// result directory object is closed.
/// If `as_dir` fails, the input file will be closed automatically.
pub fn File::as_dir(self : File) -> Directory raise {
  guard self.io.kind() is Directory else {
    self.close()
    raise @os_error.from_errno(
      @errno_util.not_directory,
      context="@fs.File::as_dir()",
    )
  }
  self.text.close()
  Directory::from_io_handle(self.io)
}

///|
/// Get the kind of a file
pub fn File::kind(self : File) -> FileKind {
  FileKind::from_fd_util_file_kind(self.io.kind())
}

///|
/// Get the last access time of a file.
pub async fn File::atime(self : File) -> Timestamp {
  let (sec, nsec) = @event_loop.atime(self.io.fd(), context="@fs.File::atime()")
  { sec, nsec, }
}

///|
/// Get the last modification time of a file.
pub async fn File::mtime(self : File) -> Timestamp {
  let (sec, nsec) = @event_loop.mtime(self.io.fd(), context="@fs.File::mtime()")
  { sec, nsec, }
}

///|
/// Get the last status change time of a file.
pub async fn File::ctime(self : File) -> Timestamp {
  let (sec, nsec) = @event_loop.ctime(self.io.fd(), context="@fs.File::ctime()")
  { sec, nsec, }
}

///|
/// Read the content of the file located at `path` in binary.
/// If `sync_timestamp` is `true` (`false` by default),
/// `read_binary` will block until timestamp change is written to the file system.
pub async fn read_binary(
  path : StringView,
  sync_timestamp~ : Bool = false,
) -> Bytes {
  let file = open(path, mode=ReadOnly)
  defer file.close()
  let result = file.read_all()
  if sync_timestamp {
    file.io.fsync(
      only_data=false,
      context="@fs.read_binary(sync_timestamp=true)",
    )
  }
  result
}

///|
/// Read the text content of the UTF-8 encoded file located at `path`.
/// If `sync_timestamp` is `true` (`false` by default),
/// `read_text` will block until timestamp change is written to the file system.
pub async fn read_text(
  path : StringView,
  sync_timestamp~ : Bool = false,
) -> String {
  let file = open(path, mode=ReadOnly)
  defer file.close()
  let result = file.text.read_all()
  if sync_timestamp {
    file.io.fsync(only_data=false, context="@fs.read_text(sync_timestamp=true)")
  }
  result
}

///|
/// Write binary data to a file located at `path`.
/// The meaning of `sync`, `append`, `create_mode` and `permission`,
/// is the same as `open`, except that `create_mode` is `TruncateExisting` by default.
/// See `open` for more details.
pub async fn write_binary(
  path : StringView,
  content : BytesView,
  create_mode~ : CreateMode = CreateOrTruncate,
  permission~ : Int32 = 0o644,
  sync~ : SyncMode = Data,
  append~ : Bool = false,
) -> Unit {
  let file = open(
    path,
    mode=WriteOnly,
    sync~,
    append~,
    create_mode~,
    permission~,
  )
  defer file.close()
  file.write(content)
}

///|
/// Write UTF-8 encoded text data to a file located at `path`.
/// The meaning of `sync`, `append`, `create_mode` and `permission`,
/// is the same as `open`, except that `create_mode` is `TruncateExisting` by default.
/// See `open` for more details.
pub async fn write_text(
  path : StringView,
  content : StringView,
  create_mode~ : CreateMode = CreateOrTruncate,
  permission~ : Int32 = 0o644,
  sync~ : SyncMode = Data,
  append~ : Bool = false,
) -> Unit {
  let file = open(
    path,
    mode=WriteOnly,
    sync~,
    append~,
    create_mode~,
    permission~,
  )
  defer file.close()
  file.text.write(content)
}