require 'dbi'
require 'thread'

# FIXME
module DBI
  module SQL
    def SQL.query?(sql)
      return false unless /^\s*select\b/i =~ sql
      return false if /\s+for\s+update/im =~ sql
      true
    end
  end
end

begin
  require 'postgres' 
  class PGconn
    alias exec async_exec
  end
rescue LoadError
end

class DBIPool
  class DBIPoolError < RuntimeError; end
  class DBIPoolConnNotFound < DBIPoolError; end

  module DBIPoolConn
    private
    def conn
      dbi_pool_info[:conn]
    end

    def add_changed(it)
      dbi_pool_info[:changed].push(it)
    end

    def call_changed(commit)
      if commit
	dbi_pool_info[:changed].each do |it| it.commit end
      else
	dbi_pool_info[:changed].each do |it| it.rollback end
      end
    end

    def dbi_pool_info
      info = Thread.current[:DBIPool]
      raise(DBIPoolConnNotFound) unless info and info[:conn]
      info
    end
  end

  include DBIPoolConn

  @pool = nil
  def self.make_pool(sz, *args)
    @pool = self.new(sz, *args)
  end
  
  def self.pool
    @pool
  end
  
  def self.transaction(&b)
    @pool.transaction(&b)
  end

  def self.commit
    @pool.commit
  end

  def self.rollback
    @pool.rollback
  end

  def initialize(sz, *args)
    @args = args
    @db_name = @args[0]
    @size = sz
    @pool = Queue.new
    sz.times do
      @pool.push(nil)
    end
    @sz_mutex = Mutex.new
  end
  attr_reader :size, :db_name

  def size=(sz)
    @sz_mutex.synchronize do
      sz = 1 if sz <= 0
      diff = sz - @size
      if diff > 0
	diff.times do
	  @pool.push(nil) 
	  @size += 1
	end
      else
	diff.times do
	  conn = @pool.pop
	  conn.disconnect if conn
	  @size -= 1
	end
      end
    end
  end

  def transaction(more_conn=false)
    unless more_conn
      begin
	dbh = conn
	return yield(dbh)
      rescue DBIPoolConnNotFound
      end
    end

    abort = false
    result = nil
    dbh = new_conn
    dbh['AutoCommit'] = false rescue DBI::NotSupportedError
    stack = Thread.current[:DBIPool]
    begin
      Thread.current[:DBIPool] = {:conn => dbh, :changed => []}
      begin
	result = yield(dbh)
	call_changed(true)
      rescue Exception
	abort = true
	call_changed(false)
	dbh.rollback
	raise
      end
    ensure
      dbh.commit unless abort
      @pool.push dbh if dbh
      Thread.current[:DBIPool] = stack
    end
    result
  end

  def new_conn
    loop do
      dbh = @pool.pop || DBI.connect(*@args)
      return dbh if dbh.connected?
      @pool.push nil
    end
  end

  def commit
    conn.commit
  end

  def rollback
    conn.rollback
  end
  alias :abort :rollback

  public :conn
end
