module DB
  # Methods to allow querying a database.
  # All methods accepts a `query : String` and a set arguments.
  #
  # Three kind of statements can be performed:
  #  1. `#exec` waits no record response from the database. An `ExecResult` is returned.
  #  2. `#scalar` reads a single value of the response. A union of possible values is returned.
  #  3. `#query` returns a `ResultSet` that allows iteration over the rows in the response and column information.
  #
  # Arguments can be passed by position or as an array.
  #
  # ```
  # db.query("SELECT name FROM ... WHERE age > ?", age)
  # db.query("SELECT name FROM ... WHERE age > ?", args: [age])
  # ```
  #
  # Convention of mapping how arguments are mapped to the query depends on each driver.
  #
  # Including `QueryMethods` requires a `build(query) : Statement` method that is not expected
  # to be called directly.
  module QueryMethods(Stmt)
    # :nodoc:
    abstract def build(query) : Stmt

    # Executes a *query* and returns a `ResultSet` with the results.
    # The `ResultSet` must be closed manually.
    #
    # ```
    # result = db.query "select name from contacts where id = ?", 10
    # begin
    #   if result.move_next
    #     id = result.read(Int32)
    #   end
    # ensure
    #   result.close
    # end
    # ```
    #
    # Note: to use a dynamic list length of arguments use `args:` keyword argument.
    #
    # ```
    # result = db.query "select name from contacts where id = ?", args: [10]
    # ```
    #
    def query(query, *args_, args : Array? = nil)
      build(query).query(*args_, args: args)
    end

    # Executes a *query* and yields a `ResultSet` with the results.
    # The `ResultSet` is closed automatically.
    #
    # ```
    # db.query("select name from contacts where age > ?", 18) do |rs|
    #   rs.each do
    #     name = rs.read(String)
    #   end
    # end
    # ```
    def query(query, *args_, args : Array? = nil)
      # CHECK build(query).query(*args, &block)
      rs = query(query, *args_, args: args)
      yield rs ensure rs.close
    end

    # Executes a *query* that expects a single row and yields a `ResultSet`
    # positioned at that first row.
    #
    # The given block must not invoke `move_next` on the yielded result set.
    #
    # Raises `DB::NoResultsError` if there were no rows.
    # Raises `DB::Error` if there were more than one row.
    #
    # ```
    # name = db.query_one "select name from contacts where id = ?", 18, &.read(String)
    # ```
    def query_one(query, *args_, args : Array? = nil, &block : ResultSet -> U) : U forall U
      query(query, *args_, args: args) do |rs|
        raise DB::NoResultsError.new("no results") unless rs.move_next

        value = yield rs
        raise DB::Error.new("more than one row") if rs.move_next
        return value
      end
    end

    # Executes a *query* that expects a single row and returns it
    # as a tuple of the given *types*.
    #
    # Raises `DB::NoResultsError` if there were no rows.
    # Raises `DB::Error` if there were more than one row.
    #
    # ```
    # db.query_one "select name, age from contacts where id = ?", 1, as: {String, Int32}
    # ```
    def query_one(query, *args_, args : Array? = nil, as types : Tuple)
      query_one(query, *args_, args: args) do |rs|
        rs.read(*types)
      end
    end

    # Executes a *query* that expects a single row and returns it
    # as a named tuple of the given *types* (the keys of the named tuple
    # are not necessarily the column names).
    #
    # Raises `DB::NoResultsError` if there were no rows.
    # Raises `DB::Error` if there were more than one row.
    #
    # ```
    # db.query_one "select name, age from contacts where id = ?", 1, as: {name: String, age: Int32}
    # ```
    def query_one(query, *args_, args : Array? = nil, as types : NamedTuple)
      query_one(query, *args_, args: args) do |rs|
        rs.read(**types)
      end
    end

    # Executes a *query* that expects a single row
    # and returns the first column's value as the given *type*.
    #
    # Raises `DB::NoResultsError` if there were no rows.
    # Raises `DB::Error` if there were more than one row.
    #
    # ```
    # db.query_one "select name from contacts where id = ?", 1, as: String
    # ```
    def query_one(query, *args_, args : Array? = nil, as type : Class)
      query_one(query, *args_, args: args) do |rs|
        rs.read(type)
      end
    end

    # Executes a *query* that expects at most a single row and yields a `ResultSet`
    # positioned at that first row.
    #
    # Returns `nil`, not invoking the block, if there were no rows.
    #
    # Raises `DB::Error` if there were more than one row
    # (this ends up invoking the block once).
    #
    # ```
    # name = db.query_one? "select name from contacts where id = ?", 18, &.read(String)
    # typeof(name) # => String | Nil
    # ```
    def query_one?(query, *args_, args : Array? = nil, &block : ResultSet -> U) : U? forall U
      query(query, *args_, args: args) do |rs|
        return nil unless rs.move_next

        value = yield rs
        raise DB::Error.new("more than one row") if rs.move_next
        return value
      end
    end

    # Executes a *query* that expects a single row and returns it
    # as a tuple of the given *types*.
    #
    # Returns `nil` if there were no rows.
    #
    # Raises `DB::Error` if there were more than one row.
    #
    # ```
    # result = db.query_one? "select name, age from contacts where id = ?", 1, as: {String, Int32}
    # typeof(result) # => Tuple(String, Int32) | Nil
    # ```
    def query_one?(query, *args_, args : Array? = nil, as types : Tuple)
      query_one?(query, *args_, args: args) do |rs|
        rs.read(*types)
      end
    end

    # Executes a *query* that expects a single row and returns it
    # as a named tuple of the given *types* (the keys of the named tuple
    # are not necessarily the column names).
    #
    # Returns `nil` if there were no rows.
    #
    # Raises `DB::Error` if there were more than one row.
    #
    # ```
    # result = db.query_one? "select name, age from contacts where id = ?", 1, as: {age: String, name: Int32}
    # typeof(result) # => NamedTuple(age: String, name: Int32) | Nil
    # ```
    def query_one?(query, *args_, args : Array? = nil, as types : NamedTuple)
      query_one?(query, *args_, args: args) do |rs|
        rs.read(**types)
      end
    end

    # Executes a *query* that expects a single row
    # and returns the first column's value as the given *type*.
    #
    # Returns `nil` if there were no rows.
    #
    # Raises `DB::Error` if there were more than one row.
    #
    # ```
    # name = db.query_one? "select name from contacts where id = ?", 1, as: String
    # typeof(name) # => String?
    # ```
    def query_one?(query, *args_, args : Array? = nil, as type : Class)
      query_one?(query, *args_, args: args) do |rs|
        rs.read(type)
      end
    end

    # Executes a *query* and yield a `ResultSet` positioned at the beginning
    # of each row, returning an array of the values of the blocks.
    #
    # ```
    # names = db.query_all "select name from contacts", &.read(String)
    # ```
    def query_all(query, *args_, args : Array? = nil, &block : ResultSet -> U) : Array(U) forall U
      ary = [] of U
      query_each(query, *args_, args: args) do |rs|
        ary.push(yield rs)
      end
      ary
    end

    # Executes a *query* and returns an array where each row is
    # read as a tuple of the given *types*.
    #
    # ```
    # contacts = db.query_all "select name, age from contacts", as: {String, Int32}
    # ```
    def query_all(query, *args_, args : Array? = nil, as types : Tuple)
      query_all(query, *args_, args: args) do |rs|
        rs.read(*types)
      end
    end

    # Executes a *query* and returns an array where each row is
    # read as a named tuple of the given *types* (the keys of the named tuple
    # are not necessarily the column names).
    #
    # ```
    # contacts = db.query_all "select name, age from contacts", as: {name: String, age: Int32}
    # ```
    def query_all(query, *args_, args : Array? = nil, as types : NamedTuple)
      query_all(query, *args_, args: args) do |rs|
        rs.read(**types)
      end
    end

    # Executes a *query* and returns an array where the
    # value of each row is read as the given *type*.
    #
    # ```
    # names = db.query_all "select name from contacts", as: String
    # ```
    def query_all(query, *args_, args : Array? = nil, as type : Class)
      query_all(query, *args_, args: args) do |rs|
        rs.read(type)
      end
    end

    # Executes a *query* and yields the `ResultSet` once per each row.
    # The `ResultSet` is closed automatically.
    #
    # ```
    # db.query_each "select name from contacts" do |rs|
    #   puts rs.read(String)
    # end
    # ```
    def query_each(query, *args_, args : Array? = nil)
      query(query, *args_, args: args) do |rs|
        rs.each do
          yield rs
        end
      end
    end

    # Performs the `query` and returns an `ExecResult`
    def exec(query, *args_, args : Array? = nil)
      build(query).exec(*args_, args: args)
    end

    # Performs the `query` and returns a single scalar value
    #
    # ```
    # puts db.scalar("SELECT MAX(name)").as(String) # => (a String)
    # ```
    def scalar(query, *args_, args : Array? = nil)
      build(query).scalar(*args_, args: args)
    end
  end
end
