// The engine: a connection string names the backend, and the backend names the
// dialect a session renders its statements for. moonorm owns no drivers, so the
// engine parses the URL and hands the dialect to a session; the caller supplies the
// driver the scheme names (moonsqlite, moonpostgres, …).

///|
/// Why a connection string could not be read.
pub(all) suberror DsnError {
  DsnError(String)
}

///|
/// The human-readable form of a `DsnError`.
pub impl Show for DsnError with fn output(self : DsnError, logger : &Logger) -> Unit {
  let DsnError(m) = self
  logger.write_string("DsnError: " + m)
}

///|
pub extend DsnError with Show::{to_string, output}

///|
/// A parsed connection string. The scheme picks the driver and the `dialect`; the
/// rest is what a driver needs to connect. A `+driver` suffix (SQLAlchemy's
/// `postgresql+asyncpg://`) overrides `driver` while leaving the scheme's dialect.
pub(all) struct Dsn {
  // The URL scheme as written: "sqlite", "postgresql", "postgres" or "mysql".
  scheme : String
  // The driver the scheme (or its `+suffix`) names.
  driver : String
  dialect : @query.Dialect
  user : String?
  password : String?
  host : String?
  port : Int32?
  // The database name (PostgreSQL/MySQL) or file path (SQLite). `None` is an
  // in-memory SQLite database.
  database : String?
  // Query-string parameters, last value wins.
  params : Map[String, String]
}

///|
/// Parse `dsn` into a `Dsn`. It accepts the SQLAlchemy URL forms:
///
/// - `sqlite://`, `sqlite:///:memory:`, `sqlite:///file.db`, `sqlite:////abs/file.db`
/// - `postgresql://user:pass@host:5432/db?sslmode=require` (and `postgres://`)
/// - `mysql://user:pass@host:3306/db` (and `mysql+pymysql://`)
///
/// A `+driver` suffix overrides the driver name without changing the dialect.
/// An absent or unknown scheme raises `DsnError`.
pub fn parse_dsn(dsn : String) -> Dsn raise DsnError {
  let raw = dsn.trim(chars=" ").to_owned()
  if raw == "" {
    raise DsnError("empty connection string")
  }
  let (scheme_part, rest) = match index_of(raw, "://") {
    Some(i) => (slice(raw, 0, i), slice(raw, i + 3, raw.length()))
    None =>
      // SQLite also accepts the shorter `sqlite:` form (`sqlite::memory:`,
      // `sqlite:file.db`).
      if raw.has_prefix("sqlite:") {
        ("sqlite", slice(raw, "sqlite:".length(), raw.length()))
      } else {
        raise DsnError("connection string has no scheme: " + raw)
      }
  }
  let (scheme, driver) = match index_of(scheme_part, "+") {
    Some(i) =>
      (
        slice(scheme_part, 0, i),
        slice(scheme_part, i + 1, scheme_part.length()),
      )
    None => (scheme_part, scheme_part)
  }
  let dialect = match scheme {
    "sqlite" => @query.Sqlite
    "postgres" | "postgresql" => @query.Postgres
    "mysql" => @query.Mysql
    _ => raise DsnError("unknown scheme: " + scheme)
  }
  if scheme == "sqlite" {
    parse_sqlite(rest, scheme, driver, dialect)
  } else {
    parse_network(rest, scheme, driver, dialect)
  }
}

///|
/// A SQLite URL: the remainder is a file path (or empty / `:memory:` for an
/// in-memory database). SQLAlchemy's slash rule is kept: one leading `/` marks a
/// relative path (`sqlite:///file.db`), two an absolute one (`sqlite:////file.db`).
fn parse_sqlite(
  rest : String,
  scheme : String,
  driver : String,
  dialect : @query.Dialect,
) -> Dsn {
  let (path_part, query_part) = split_query(rest)
  let path = if path_part.has_prefix("/") {
    slice(path_part, 1, path_part.length())
  } else {
    path_part
  }
  let database = if path == "" || path == ":memory:" {
    None
  } else {
    Some(path)
  }
  {
    scheme,
    driver,
    dialect,
    user: None,
    password: None,
    host: None,
    port: None,
    database,
    params: parse_params(query_part),
  }
}

///|
/// A PostgreSQL or MySQL URL: `[userinfo@]host[:port][/database][?params]`.
fn parse_network(
  rest : String,
  scheme : String,
  driver : String,
  dialect : @query.Dialect,
) -> Dsn {
  let (authority, after) = match index_of(rest, "/") {
    Some(i) => (slice(rest, 0, i), slice(rest, i + 1, rest.length()))
    None => (rest, "")
  }
  let (database_part, query_part) = split_query(after)
  let (userinfo, hostport) = match index_of(authority, "@") {
    Some(i) =>
      (
        Some(slice(authority, 0, i)),
        slice(authority, i + 1, authority.length()),
      )
    None => (None, authority)
  }
  let (user, password) = match userinfo {
    Some(ui) =>
      match index_of(ui, ":") {
        Some(i) => (Some(slice(ui, 0, i)), Some(slice(ui, i + 1, ui.length())))
        None => (Some(ui), None)
      }
    None => (None, None)
  }
  let (host, port) = parse_hostport(hostport)
  let database = if database_part == "" { None } else { Some(database_part) }
  {
    scheme,
    driver,
    dialect,
    user,
    password,
    host,
    port,
    database,
    params: parse_params(query_part),
  }
}

///|
/// Split `s` at its first `?` into the part before and the query string after.
fn split_query(s : String) -> (String, String) {
  match index_of(s, "?") {
    Some(i) => (slice(s, 0, i), slice(s, i + 1, s.length()))
    None => (s, "")
  }
}

///|
/// `host[:port]`, with IPv6 bracketed as `[addr]` or `[addr]:port`.
fn parse_hostport(hp : String) -> (String?, Int32?) {
  if hp == "" {
    return (None, None)
  }
  if hp.has_prefix("[") {
    match index_of(hp, "]") {
      Some(j) => {
        let host = slice(hp, 1, j)
        let tail = slice(hp, j + 1, hp.length())
        if tail.has_prefix(":") {
          (Some(host), parse_port(slice(tail, 1, tail.length())))
        } else {
          (Some(host), None)
        }
      }
      None => (Some(hp), None)
    }
  } else {
    match index_of(hp, ":") {
      Some(i) =>
        (Some(slice(hp, 0, i)), parse_port(slice(hp, i + 1, hp.length())))
      None => (Some(hp), None)
    }
  }
}

///|
/// A decimal port, or `None` when `s` is not all digits.
fn parse_port(s : String) -> Int32? {
  if s.length() == 0 {
    return None
  }
  let mut n = 0
  let mut i = 0
  while i < s.length() {
    let c = s[i].to_int()
    if c < 48 || c > 57 {
      return None
    }
    n = n * 10 + (c - 48)
    i = i + 1
  }
  Some(n)
}

///|
/// The `k=v&k2=v2` query string as a map, last value winning. A bare key maps to
/// the empty string.
fn parse_params(query : String) -> Map[String, String] {
  let params : Map[String, String] = Map([])
  if query == "" {
    return params
  }
  for part in query.split("&") {
    let pair = part.to_owned()
    if pair != "" {
      match index_of(pair, "=") {
        Some(i) =>
          params.set(slice(pair, 0, i), slice(pair, i + 1, pair.length()))
        None => params.set(pair, "")
      }
    }
  }
  params
}

///|
/// The byte offset of `needle` in `s`, or `None`. An empty needle matches at 0.
fn index_of(s : String, needle : String) -> Int32? {
  let n = s.length()
  let m = needle.length()
  if m == 0 {
    return Some(0)
  }
  let mut i = 0
  while i + m <= n {
    let mut j = 0
    let mut ok = true
    while j < m {
      if s[i + j] != needle[j] {
        ok = false
        break
      }
      j = j + 1
    }
    if ok {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
/// The substring `s[start..end]`, empty when the range is empty.
fn slice(s : String, start : Int32, end : Int32) -> String {
  if start >= end {
    ""
  } else {
    s.exact_view(start~, end~).to_owned()
  }
}

///|
/// A backend selected by its connection string: the parsed `Dsn` plus the dialect
/// its sessions render for. `session` binds one to a driver the caller supplies.
pub struct Engine {
  dsn : Dsn
}

///|
/// Parse `dsn` into an `Engine`. The URL's scheme picks the driver and the dialect.
pub fn Engine::new(dsn : String) -> Engine raise DsnError {
  { dsn: parse_dsn(dsn), }
}

///|
/// An `Engine` over an already parsed `Dsn`.
pub fn Engine::from_dsn(dsn : Dsn) -> Engine {
  { dsn, }
}

///|
/// The parsed connection string.
pub fn Engine::dsn(self : Engine) -> Dsn {
  self.dsn
}

///|
/// The dialect the URL's scheme selects.
pub fn Engine::dialect(self : Engine) -> @query.Dialect {
  self.dsn.dialect
}

///|
/// The driver the URL names (the scheme, or its `+driver` override).
pub fn Engine::driver(self : Engine) -> String {
  self.dsn.driver
}

///|
/// Wrap `driver` in a session that renders for this engine's dialect. moonorm owns
/// no driver contract, so the concrete backend is the caller's to supply.
pub fn Engine::session(
  self : Engine,
  driver : &@moondb.Driver,
) -> @session.Session {
  @session.Session::new(driver, dialect=self.dsn.dialect)
}