require "../src/db"

class DummyDriver < DB::Driver
  class DummyConnectionBuilder < DB::ConnectionBuilder
    def initialize(@options : DB::Connection::Options)
    end

    def build : DB::Connection
      DummyConnection.new(@options)
    end
  end

  def connection_builder(uri : URI) : DB::ConnectionBuilder
    params = HTTP::Params.parse(uri.query || "")
    DummyConnectionBuilder.new(connection_options(params))
  end

  class DummyConnection < DB::Connection
    @@connections = [] of DummyConnection
    @@connections_count = Atomic(Int32).new(0)

    def initialize(options : DB::Connection::Options)
      super(options)
      Fiber.yield
      @@connections_count.add(1)
      @connected = true
      {% unless flag?(:preview_mt) %}
        # @@connections is only used in single-threaded mode in specs
        # for benchmarks we want to avoid the overhead of synchronizing this array
        @@connections << self
      {% end %}
    end

    def self.connections_count
      @@connections_count.get
    end

    def self.connections
      {% if flag?(:preview_mt) %}
        raise "DummyConnection.connections is only available in single-threaded mode"
      {% end %}
      @@connections
    end

    def self.clear_connections
      {% if flag?(:preview_mt) %}
        raise "DummyConnection.clear_connections is only available in single-threaded mode"
      {% end %}
      @@connections.clear
    end

    def build_prepared_statement(query) : DB::Statement
      assert_not_closed!

      DummyStatement.new(self, query, true)
    end

    def build_unprepared_statement(query) : DB::Statement
      assert_not_closed!

      DummyStatement.new(self, query, false)
    end

    def last_insert_id : Int64
      assert_not_closed!

      0
    end

    def check
      raise DB::ConnectionLost.new(self) unless @connected
    end

    def disconnect!
      @connected = false
    end

    def create_transaction
      assert_not_closed!

      DummyTransaction.new(self)
    end

    protected def do_close
      super
    end

    private def assert_not_closed!
      raise "Statement is closed" if closed?
    end
  end

  class DummyTransaction < DB::TopLevelTransaction
    getter committed = false
    getter rolledback = false

    def initialize(connection)
      super(connection)
    end

    def commit
      super
      @committed = true
    end

    def rollback
      super
      @rolledback = true
    end

    protected def create_save_point_transaction(parent, savepoint_name : String)
      DummySavePointTransaction.new(parent, savepoint_name)
    end
  end

  class DummySavePointTransaction < DB::SavePointTransaction
    getter committed = false
    getter rolledback = false

    def initialize(parent, savepoint_name)
      super(parent, savepoint_name)
    end

    def commit
      super
      @committed = true
    end

    def rollback
      super
      @rolledback = true
    end
  end

  class DummyStatement < DB::Statement
    @@statements_count = Atomic(Int32).new(0)
    @@statements_exec_count = Atomic(Int32).new(0)
    property params

    def initialize(connection, command : String, @prepared : Bool)
      @params = Hash(Int32 | String, DB::Any | Array(DB::Any)).new
      super(connection, command)
      @@statements_count.add(1)
      raise DB::Error.new(command) if command == "syntax error"
      raise DB::ConnectionLost.new(connection) if command == "raise ConnectionLost"
    end

    def self.statements_count
      @@statements_count.get
    end

    def self.statements_exec_count
      @@statements_exec_count.get
    end

    protected def perform_query(args : Enumerable) : DB::ResultSet
      assert_not_closed!

      @@statements_exec_count.add(1)

      Fiber.yield
      @connection.as(DummyConnection).check
      set_params args
      DummyResultSet.new self, command
    end

    protected def perform_exec(args : Enumerable) : DB::ExecResult
      assert_not_closed!

      @@statements_exec_count.add(1)

      @connection.as(DummyConnection).check
      set_params args
      raise DB::Error.new("forced exception due to query") if command == "raise"
      DB::ExecResult.new 0i64, 0_i64
    end

    private def set_params(args)
      @params.clear
      args.each_with_index do |arg, index|
        set_param(index, arg)
      end
    end

    private def set_param(index, value : DB::Any)
      @params[index] = value
    end

    private def set_param(index, value : Array)
      @params[index] = value.map(&.as(DB::Any))
    end

    private def set_param(index, value)
      raise "not implemented for #{value.class}"
    end

    def prepared?
      @prepared
    end

    protected def do_close
      super
    end

    private def assert_not_closed!
      raise "Statement is closed" if closed?
    end
  end

  class DummyResultSet < DB::ResultSet
    @top_values : Array(Array(String))
    @values : Array(String)?

    @@last_result_set : self?

    def initialize(statement, command)
      super(statement)
      Fiber.yield

      @top_values = command.split.map { |r| r.split(',') }.to_a
      @column_count = @top_values.size > 0 ? @top_values[0].size : 2

      @@last_result_set = self
    end

    protected def do_close
      super
    end

    def self.last_result_set
      @@last_result_set.not_nil!
    end

    def move_next : Bool
      @values = @top_values.shift?
      !!@values
    end

    def column_count : Int32
      @column_count
    end

    def column_name(index) : String
      "c#{index}"
    end

    def read
      n = @values.not_nil!.shift?
      raise "end of row" if n.is_a?(Nil)
      return nil if n == "NULL"

      if n == "?"
        return (@statement.as(DummyStatement)).params[0]
      end

      n.to_i64? || n
    end

    def next_column_index : Int32
      @column_count - @values.not_nil!.size
    end

    def read(t : String.class)
      read.to_s
    end

    def read(t : String?.class)
      read.try &.to_s
    end

    def read(t : Int32.class)
      read(String).to_i32
    end

    def read(t : Int32?.class)
      read(String?).try &.to_i32
    end

    def read(t : Int64.class)
      read(String).to_i64
    end

    def read(t : Int64?.class)
      read(String?).try &.to_i64
    end

    def read(t : Float32.class)
      read(String).to_f32
    end

    def read(t : Float64.class)
      read(String).to_f64
    end

    def read(t : Bytes.class)
      case value = read
      when String
        ary = value.bytes
        Slice.new(ary.to_unsafe, ary.size)
      when Bytes
        value
      else
        raise "#{value} is not convertible to Bytes"
      end
    end
  end
end

DB.register_driver "dummy", DummyDriver

class Witness
  getter count

  def initialize(@count = 1)
  end

  def check
    @count -= 1
  end
end

def with_witness(count = 1)
  w = Witness.new(count)
  yield w
  w.count.should eq(0), "The expected coverage was unmet"
end

def with_dummy(uri : String = "dummy://host?checkout_timeout=0.5")
  DummyDriver::DummyConnection.clear_connections

  DB.open uri do |db|
    yield db
  end
end

def with_dummy_connection(options = "")
  with_dummy("dummy://host?checkout_timeout=0.5&#{options}") do |db|
    db.using_connection do |cnn|
      yield cnn.as(DummyDriver::DummyConnection)
    end
  end
end
