///|
typealias @c.Nullable

///|
#external
type MySql

///|
/// Connect to MySQL with the given credentials. It sets the resulting connection 
/// character set to `utf8mb4`.
pub fn connect(
  host~ : String,
  port? : UInt,
  user~ : String,
  password~ : String,
  database~ : String,
  unix_socket? : String,
  client_flag? : ClientFlag = 0,
) -> MySql raise {
  let mysql = mysql_init(Nullable::none())
  guard mysql.to() is Some(mysql) else {
    fail("Failed to initialize MySQL connection")
  }
  let encode = @encoding.encode(encoding=UTF8, _)
  let host = encode(host)
  let user = encode(user)
  let password = encode(password)
  let database = encode(database)
  let unix_socket = match unix_socket {
    Some(socket) => Nullable::some(encode(socket))
    None => Nullable::none()
  }
  guard mysql_real_connect(
      mysql,
      host,
      user,
      password,
      database,
      port.unwrap_or(3306),
      unix_socket,
      client_flag.inner(),
    ).to()
    is Some(conn) else {
    mysql.fail("Failed to connect to MariaDB database")
  }
  if mysql_set_character_set(mysql, b"utf8mb4") != 0 {
    mysql.fail("Failed to set character set to 'utf8mb4'")
  }
  conn
}

///|
extern "c" fn mysql_init(mysql : Nullable[MySql]) -> Nullable[MySql] = "mysql_init"

///|
#borrow(host, user, passwd, db, unix_socket)
extern "c" fn mysql_real_connect(
  mysql : MySql,
  host : Bytes,
  user : Bytes,
  passwd : Bytes,
  db : Bytes,
  port : UInt,
  unix_socket : Nullable[Bytes],
  client_flag : UInt64,
) -> Nullable[MySql] = "mysql_real_connect"

///|
#borrow(cs)
extern "c" fn mysql_set_character_set(mysql : MySql, cs : Bytes) -> Int = "mysql_set_character_set"

///|
pub fn MySql::real_escape_string(self : Self, s : String) -> String {
  let from = @encoding.encode(encoding=UTF8, s)
  let to = Bytes::make(from.length() * 2 + 1, 0) // Allocate enough space for escaped string
  let escaped_len = mysql_real_escape_string(
    self,
    to,
    from,
    from.length().reinterpret_as_uint(),
  )
  let to = to[0:escaped_len]
  @encoding.decode_lossy(encoding=UTF8, to)
}

///|
#borrow(to, from)
extern "c" fn mysql_real_escape_string(
  mysql : MySql,
  to : Bytes,
  from : Bytes,
  length : UInt,
) -> Int = "mysql_real_escape_string"

///|
/// Executes the given query and returns the result.
pub fn MySql::execute(self : Self, query : String) -> QueryResult? raise {
  let bytes_query = @encoding.encode(encoding=UTF8, query)
  let len = bytes_query.length().reinterpret_as_uint()
  if mysql_real_query(self, bytes_query, len) != 0 {
    self.fail("Failed to execute query '\{query}'")
  }
  QueryResult::new(self)
}

///|
#borrow(query)
extern "c" fn mysql_real_query(
  mysql : MySql,
  query : Bytes,
  length : UInt,
) -> Int = "mysql_real_query"

///|
pub extern "c" fn MySql::close(self : MySql) = "mysql_close"

///|
pub fn MySql::affected_rows(self : Self) -> UInt {
  mysql_affected_rows(self)
}

///|
extern "c" fn mysql_affected_rows(mysql : MySql) -> UInt = "mysql_affected_rows"

///|
/// Raise a Failure with the current MySQL error information as returned
/// by the MySQL API(`mysql_error()` and `mysql_errno()`)
#inline
#callsite(autofill(loc))
pub fn[T] MySql::fail(
  self : MySql,
  err_msg : String,
  loc~ : SourceLoc,
) -> T raise {
  self.close()
  let error = @ffi.moonbit_string(mysql_error(self))
  raise Err((err_msg, mysql_errno(self), error, loc))
}

///|
extern "c" fn mysql_errno(mysql : MySql) -> UInt = "mysql_errno"

///|
extern "c" fn mysql_error(mysql : MySql) -> @ffi.CString = "mysql_error"