require 'dbi'
require 'thread'
require 'date'
require 'parsedate'

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

$PRB_PIG = ENV['PRB_PIG']

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

  module DBIPoolConn
    private
    def conn
      sleep 0.05 if $PRB_PIG
      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

module DBIHome
  class Home
    include DBIPool::DBIPoolConn
    
    def initialize(name)
      @name = name
    end
    attr_reader :name

    def regist(hash)
      key = []
      val = []

      hash.each do |k, v|
	key.push k
	val.push conn.quote(v)
      end

      key = key.join(', ')
      val = val.join(', ')

      conn.execute("insert into #{@name} (#{key}) values (#{val});")
    end

    def extent(where=nil)
      sql = "select * from #{@name}" 
      sql += " where #{where}" if where
      conn.execute(sql) do |sth|
	while h = sth.fetch_hash
	  yield(h)
	end
      end
    end

    def fetch(pk)
      extent(make_where(pk)) do |h|
	return h
      end
      nil
    end

    def update(hash, pk)
      ary = []
      hash.each do |k, v|
	ary.push("#{k}=#{conn.quote(v)}")
      end
      sets = ary.join(', ')

      conn.execute("update #{@name} set #{sets} where #{make_where(pk)};")
    end

    def make_where(hash)
      ary = []
      hash.each do |k, v|
	ary.push("#{k}=#{conn.quote(v)}")
      end
      ary.join(', ')
    end
  end

  class Entity
    include DBIPool::DBIPoolConn

    def initialize(home, pk = {})
      @home = home
      @pk = pk
    end
    attr_reader :home, :pk
    
    def ==(other)
      self.type == other.type && @home == other.home && @pk == other.pk
    end
    alias eql? ==

    def hash
      [@home, @pk.to_s].hash
    end

    def [](k)
      cache[k]
    end

    def []=(k, v)
      cache[k] = v
      (cache[:dirty] ||= []).push(k)
      add_changed(self)
    end
    
    def value
      cache
    end

    def commit
      hash = {}
      cache[:dirty].uniq.each do |k|
	hash[k] = cache[k]
      end
      @home.update(hash, @pk)
    end

    def rollback
    end

    private
    def cache
      dbi_pool_info[self] ||= @home.fetch(@pk)
    end
  end
end
