defmodule Sqlite.DbConnection.Protocol do
  @moduledoc false

  alias Sqlite.DbConnection.Query
  use DBConnection

  defstruct [db: nil, path: nil, checked_out?: false]

  @type state :: %__MODULE__{db: pid, path: String.t, checked_out?: boolean}

  @spec connect(Keyword.t) :: {:ok, state}
  def connect(opts) do
    db_path = Keyword.fetch!(opts, :database)
    db_timeout = Keyword.get(opts, :db_timeout, 5000)

    {:ok, db} = Sqlitex.Server.start_link(db_path, db_timeout: db_timeout)
    :ok = Sqlitex.Server.exec(db, "PRAGMA foreign_keys = ON")
    {:ok, [[foreign_keys: 1]]} = Sqlitex.Server.query(db, "PRAGMA foreign_keys")

    {:ok, %__MODULE__{db: db, path: db_path, checked_out?: false}}
  end

  @spec disconnect(Exception.t, state) :: :ok
  def disconnect(_exc, %__MODULE__{db: db} = _state) when db != nil do
    GenServer.stop(db)
    :ok
  end
  def disconnect(_exception, _state), do: :ok

  @spec checkout(state) :: {:ok, state}
  def checkout(%{checked_out?: false} = s) do
    {:ok, %{s | checked_out?: true}}
  end

  @spec checkin(state) :: {:ok, state}
  def checkin(%{checked_out?: true} = s) do
    {:ok, %{s | checked_out?: false}}
  end

  @spec handle_prepare(Sqlite.DbConnection.Query.t, Keyword.t, state) ::
    {:ok, Sqlite.DbConnection.Query.t, state} |
    {:error, ArgumentError.t, state}
  def handle_prepare(%Query{statement: statement, prepared: nil} = query, _opts,
                     %__MODULE__{checked_out?: true, db: db} = s)
  do
    binary_stmt = :erlang.iolist_to_binary(statement)
    case Sqlitex.Server.prepare(db, binary_stmt) do
      {:ok, prepared_info} ->
        updated_query = %{query | prepared: refined_info(prepared_info)}
        {:ok, updated_query, s}
      {:error, {_sqlite_errcode, _message}} = err ->
        sqlite_error(err, s)
    end
  end
  def handle_prepare(query, _opts, s) do
    query_error(s, "query #{inspect query} has already been prepared")
  end

  @spec handle_execute(Sqlite.DbConnection.Query.t, list, Keyword.t, state) ::
    {:ok, Sqlite.DbConnection.Result.t, state} |
    {:error, ArgumentError.t, state} |
    {:error, Sqlite.DbConnection.Error.t, state}
  def handle_execute(%Query{} = query, params, opts, s) do
    handle_execute(query, params, :sync, opts, s)
  end

  @spec handle_close(Sqlite.DbConnection.Query.t, Keyword.t, state) ::
    {:ok, Sqlite.DbConnection.Result.t, state} |
    {:error, ArgumentError.t, state} |
    {:error, Sqlite.DbConnection.Error.t, state}
  def handle_close(_query, _opts, s) do
    # no-op: esqlite doesn't expose statement close.
    # Instead it relies on statements getting garbage collected.
    res = %Sqlite.DbConnection.Result{command: :close}
    {:ok, res, s}
  end

  @spec handle_begin(Keyword.t, state) ::
    {:ok, Sqlite.DbConnection.Result.t, state}
  def handle_begin(opts, s) do
    sql = case Keyword.get(opts, :mode, :transaction) do
      :transaction -> "BEGIN"
      :savepoint   -> "SAVEPOINT sqlite_ecto_savepoint"
    end
    handle_transaction(sql, [timeout: Keyword.get(opts, :timeout, 5000)], s)
  end

  @spec handle_commit(Keyword.t, state) ::
    {:ok, Sqlite.DbConnection.Result.t, state}
  def handle_commit(opts, s) do
    sql = case Keyword.get(opts, :mode, :transaction) do
      :transaction -> "COMMIT"
      :savepoint   -> "RELEASE SAVEPOINT sqlite_ecto_savepoint"
    end
    handle_transaction(sql, [timeout: Keyword.get(opts, :timeout, 5000)], s)
  end

  @spec handle_rollback(Keyword.t, state) ::
    {:ok, Sqlite.DbConnection.Result.t, state}
  def handle_rollback(opts, s) do
    sql = case Keyword.get(opts, :mode, :transaction) do
      :transaction -> "ROLLBACK"
      :savepoint   -> "ROLLBACK TO SAVEPOINT sqlite_ecto_savepoint"
    end
    handle_transaction(sql, [timeout: Keyword.get(opts, :timeout, 5000)], s)
  end

  defp refined_info(prepared_info) do
    types =
      prepared_info.types
      |> Enum.map(&maybe_atom_to_lc_string/1)
      |> Enum.to_list

    prepared_info
    |> Map.delete(:columns)
    |> Map.put(:column_names, atoms_to_strings(prepared_info.columns))
    |> Map.put(:types, types)
  end

  defp atoms_to_strings(nil), do: nil
  defp atoms_to_strings(list), do: Enum.map(list, &maybe_atom_to_string/1)

  defp maybe_atom_to_string(nil), do: nil
  defp maybe_atom_to_string(item), do: to_string(item)

  defp maybe_atom_to_lc_string(nil), do: nil
  defp maybe_atom_to_lc_string(item), do: item |> to_string |> String.downcase

  defp handle_execute(%Query{statement: sql}, params, _sync, opts, s) do
    # Note that we rely on Sqlitex.Server to cache the prepared statement,
    # so we can simply refer to the original SQL statement here.
    case run_stmt(sql, params, opts, s) do
      {:ok, result} ->
        {:ok, result, s}
      other ->
        other
    end
  end

  defp query_error(s, msg) do
    {:error, ArgumentError.exception(msg), s}
  end

  defp sqlite_error({:error, {sqlite_errcode, message}}, s) do
    {:error, %Sqlite.DbConnection.Error{sqlite: %{code: sqlite_errcode},
                                        message: to_string(message)}, s}
  end

  defp run_stmt(query, params, opts, s) do
    query_opts = [
      timeout: Keyword.get(opts, :timeout, 5000),
      decode: :manual,
      types: true,
      bind: params
    ]

    command = command_from_sql(query)
    case query_rows(s.db, to_string(query), query_opts) do
      {:ok, %{rows: raw_rows, columns: raw_column_names}} ->
        {rows, num_rows, column_names} = case {raw_rows, raw_column_names} do
          {_, []} -> {nil, get_changes_count(s.db, command), nil}
          _ -> {raw_rows, length(raw_rows), raw_column_names}
        end
        {:ok, %Sqlite.DbConnection.Result{rows: rows,
                                          num_rows: num_rows,
                                          columns: atoms_to_strings(column_names),
                                          command: command}}
      {:error, :wrong_type} -> {:error, %ArgumentError{message: "Wrong type"}, s}
      {:error, {_sqlite_errcode, _message}} = err ->
        sqlite_error(err, s)
      {:error, %Sqlite.DbConnection.Error{} = err} ->
        {:error, err, s}
      {:error, :args_wrong_length} ->
        {:error,
         %ArgumentError{message: "parameters must match number of placeholders in query"},
         s}
    end
  end

  defp get_changes_count(db, command)
    when command in [:insert, :update, :delete]
  do
    {:ok, %{rows: [[changes_count]]}} = Sqlitex.Server.query_rows(db, "SELECT changes()")
    changes_count
  end
  defp get_changes_count(_db, _command), do: 1

  defp command_from_sql(sql) do
    sql
    |> :erlang.iolist_to_binary
    |> String.downcase
    |> String.split(" ", parts: 3)
    |> command_from_words
  end

  defp command_from_words([verb, subject, _])
    when verb == "alter" or verb == "create" or verb == "drop"
  do
    String.to_atom("#{verb}_#{subject}")
  end

  defp command_from_words(words) when is_list(words) do
    String.to_atom(List.first(words))
  end

  defp handle_transaction(stmt, opts, s) do
    {:ok, _rows} = query_rows(s.db, stmt, Keyword.merge(opts, [into: :raw_list]))
    command = command_from_sql(stmt)
    result = %Sqlite.DbConnection.Result{rows: nil,
                                         num_rows: nil,
                                         columns: nil,
                                         command: command}
    {:ok, result, s}
  end

  defp query_rows(db, stmt, opts) do
    try do
      Sqlitex.Server.query_rows(db, stmt, opts)
    catch
      :exit, {:timeout, _gen_server_call} ->
        {:error, %Sqlite.DbConnection.Error{message: "Timeout"}}
      :exit, ex ->
        {:error, %Sqlite.DbConnection.Error{message: inspect(ex)}}
    end
  end
end
