///|
/// A failure produced while loading, resolving, or closing a native library.
pub(all) enum DynlibError {
  InvalidLibraryPath
  InvalidSymbolName
  LoadFailed
  SymbolNotFound(String)
  CloseFailed
  Closed
} derive(Eq, Debug)

///|
/// An explicitly owned native dynamic-library handle.
///
/// Call close when the library is no longer needed. Closing is idempotent.
pub struct Library {
  priv mut handle : UInt64
}

///|
/// A resolved symbol tied to the library that produced it.
///
/// The address cannot be obtained after the source library is closed.
pub struct Symbol {
  priv library : Library
  priv address : UInt64
}

///|
fn has_nul(value : String) -> Bool {
  value.contains_code_unit(0)
}

///|
fn encode_utf8(value : String) -> Bytes {
  @utf8.encode(value)
}

///|
/// Loads a dynamic library by absolute path or platform loader name.
///
/// Paths are encoded as UTF-8. On Windows they are converted to UTF-16 before
/// calling the operating system loader.
pub fn load(path : String) -> Result[Library, DynlibError] {
  if path.is_empty() || has_nul(path) {
    return Err(InvalidLibraryPath)
  }
  let handle = native_load(encode_utf8(path))
  if handle == 0UL {
    Err(LoadFailed)
  } else {
    Ok({ handle, })
  }
}

///|
/// Returns whether this library remains open.
pub fn Library::is_open(self : Library) -> Bool {
  self.handle != 0UL
}

///|
/// Resolves a native symbol without invoking it.
///
/// Dynamic function calls are deliberately outside this package's API because
/// calling conventions and signatures must be modeled by the consuming FFI.
pub fn Library::resolve(
  self : Library,
  name : String,
) -> Result[Symbol, DynlibError] {
  if !self.is_open() {
    return Err(Closed)
  }
  if name.is_empty() || has_nul(name) {
    return Err(InvalidSymbolName)
  }
  let address = native_resolve(self.handle, encode_utf8(name))
  if address == 0UL {
    Err(SymbolNotFound(name))
  } else {
    Ok({ library: self, address })
  }
}

///|
/// Returns a raw symbol address while its source library remains open.
///
/// The caller must not retain or invoke the address after Library::close.
pub fn Symbol::address(self : Symbol) -> Result[UInt64, DynlibError] {
  if self.library.is_open() {
    Ok(self.address)
  } else {
    Err(Closed)
  }
}

///|
/// Unloads the library. Calling close again returns success without touching
/// the operating system handle.
pub fn Library::close(self : Library) -> Result[Unit, DynlibError] {
  if !self.is_open() {
    return Ok(())
  }
  if native_close(self.handle) {
    self.handle = 0UL
    Ok(())
  } else {
    Err(CloseFailed)
  }
}