// Copyright 2025 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.

///|
#cfg(target="native")
pub extern "C" fn get_errno() -> Int = "moonbitlang_async_get_errno"

///|
#cfg(target="wasm")
#unsafe_skip_stub_check
pub fn get_errno() -> Int = "moonbitlang/async" "os_error/get_errno"

///|
fn errno_is_nonblocking_io_error(errno : Int) -> Bool {
  match @env_util.platform {
    Linux =>
      errno == linux_EAGAIN ||
      errno == linux_EINPROGRESS ||
      errno == linux_EWOULDBLOCK
    MacOS =>
      errno == macos_EAGAIN ||
      errno == macos_EINPROGRESS ||
      errno == macos_EWOULDBLOCK
    Windows =>
      errno == windows_ERROR_IO_INCOMPLETE || errno == windows_ERROR_IO_PENDING
  }
}

///|
pub fn is_nonblocking_io_error() -> Bool {
  errno_is_nonblocking_io_error(get_errno())
}

///|
#cfg(target="native")
extern "C" fn strerror(errno : Int) -> @c_buffer.Buffer = "moonbitlang_async_errno_to_string"

///|
#unsafe_skip_stub_check
#cfg(target="wasm")
fn strerror(errno : Int) -> @c_buffer.Buffer = "moonbitlang/async" "os_error/errno_to_string"

///|
#cfg(target="native")
extern "C" fn free_errno_str(str : @c_buffer.Buffer) = "moonbitlang_async_free_errno_str"

///|
#unsafe_skip_stub_check
#cfg(target="wasm")
#owned(str)
fn free_errno_str(str : @c_buffer.Buffer) -> Unit = "moonbitlang/async" "os_error/free_errno_str"

///|
#internal(internal, "for internal use only")
pub fn errno_to_string(errno : Int) -> String {
  let c_str = strerror(errno)
  defer free_errno_str(c_str)
  @os_string.decode(c_str).trim_end(chars="\r\n").to_owned()
}

///|
pub(all) suberror OSError {
  OSError(Int, context~ : String)
} derive(ToJson)

///|
pub impl Show for OSError with fn output(self, logger) -> Unit {
  let OSError(errno, context~) = self
  let msg = "\{context}: \{errno_to_string(errno)}"
  logger <+ "OSError(\{Repr(msg)})"
}

///|
pub impl Debug for OSError with fn to_repr(self) {
  let OSError(errno, context~) = self
  Repr::opaque_(
    "@os_error.OSError",
    Repr::string("\{context}: \{errno_to_string(errno)}"),
  )
}

///|
pub fn OSError::is_nonblocking_io_error(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  errno_is_nonblocking_io_error(errno)
}

///|
pub fn OSError::is_EINTR(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  match @env_util.platform {
    Linux => errno == linux_EINTR
    MacOS => errno == macos_EINTR
    Windows => false
  }
}

///|
pub extend OSError with Show::{to_string}

///|
/// Detect whether the error is the `ENOENT` (no such file or directory) error.
pub fn OSError::is_ENOENT(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  match @env_util.platform {
    Linux => errno == linux_ENOENT
    MacOS => errno == macos_ENOENT
    Windows =>
      errno == windows_ERROR_FILE_NOT_FOUND ||
      errno == windows_ERROR_PATH_NOT_FOUND
  }
}

///|
/// Detect whether the error is the `EEXIST` (already exists) error.
pub fn OSError::is_EEXIST(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  match @env_util.platform {
    Linux => errno == linux_EEXIST
    MacOS => errno == macos_EEXIST
    Windows =>
      errno == windows_ERROR_FILE_EXISTS ||
      errno == windows_ERROR_ALREADY_EXISTS
  }
}

///|
/// Detect whether the error is the `EACCES` (permission denied) error.
pub fn OSError::is_EACCES(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  match @env_util.platform {
    Linux => errno == linux_EACCES
    MacOS => errno == macos_EACCES
    Windows => errno == windows_ERROR_ACCESS_DENIED
  }
}

///|
/// Detect whether the error is the `ECONNREFUSED` (connection refused) error.
pub fn OSError::is_ECONNREFUSED(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  match @env_util.platform {
    Linux => errno == linux_ECONNREFUSED
    MacOS => errno == macos_ECONNREFUSED
    Windows => errno == windows_ERROR_CONNECTION_REFUSED
  }
}

///|
/// Detect whether the error is the Windows `ERROR_NOTIFY_ENUM_DIR` error.
pub fn OSError::is_ERROR_NOTIFY_ENUM_DIR(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  match @env_util.platform {
    Linux | MacOS => false
    Windows => errno == windows_ERROR_NOTIFY_ENUM_DIR
  }
}

///|
pub fn check_errno(context : String) -> Unit raise OSError {
  let err_code = get_errno()
  if err_code != 0 {
    raise OSError(err_code, context~)
  }
}

///|
pub let errno_ENOTDIR : Int = match @env_util.platform {
  Linux => linux_ENOTDIR
  MacOS => macos_ENOTDIR
  Windows => windows_ERROR_DIRECTORY
}

///|
pub fn OSError::is_ENOTDIR(err : OSError) -> Bool {
  let OSError(errno, ..) = err
  errno == errno_ENOTDIR
}

///|
pub let errno_ENOTSUP : Int = match @env_util.platform {
  Linux => linux_ENOTSUP
  MacOS => macos_ENOTSUP
  Windows => windows_ERROR_NOT_SUPPORTED
}