require 'prb/dbiu'

module PRb
  MAX_INT8 = 2**63-1
  MIN_INT8 = (- MAX_INT8 - 1)
  
  module PRbO
    def _prb_ref
      @ref
    end

    def _prb_db_name
      @prb.db_name
    end

    def inspect
      "#<#{type} ref=#{@ref}>"
    end

    def transaction
      @prb.transaction do |dbh|
	_lock
	_setup_cache
	yield(dbh)
      end
    end

    private
    def _lock
      @prb.lock(@ref)
    end

    def _set(key, value)
      self.transaction do
	_prb_cache.delete(key)
	@prb.set_prop(@ref, key, value)
      end
    end

    def _get(key)
      @prb.transaction do
	_setup_cache
	if @cache.has_key?(key)
	  return @cache[key] 
	end
	@cache[key] = @prb.get_prop(@ref, key)
      end
    end

    def _delete(key)
      @prb.transaction do
	_prb_cache.delete(key)
	@prb.delete_prop(@ref, key)
      end
    end

    def _keys
      @prb.transaction do
	@prb.prop_keys(@ref)
      end
    end

    def _ary_keys
      self.transaction do
	@prb.ary_keys(@ref)
      end
    end

    def _all_keys
      self.transaction do
	@prb.all_keys(@ref)
      end
    end

    def _list_push(value)
      self.transaction do
	@prb.list_push(@ref, value)
      end
    end

    def _list_unshift(value)
      self.transaction do
	@prb.list_unshift(@ref, value)
      end
    end

    def _list_shift
      self.transaction do
	@prb.list_shift(@ref)
      end
    end

    def _list_pop
      self.transaction do
	@prb.list_pop(@ref)
      end
    end

    def _list_head
      @prb.transaction do
	@prb.list_head(@ref)
      end
    end

    def _list_tail
      @prb.transaction do
	@prb.list_tail(@ref)
      end
    end

    def _list_delete_value(value)
      @prb.transaction do
	@prb.list_delete_value(@ref, value)
      end
    end

    def _list_ary
      @prb.transaction do
	@prb.list_ary(@ref)
      end
    end

    def _list_length
      @prb.transaction do
	@prb.list_length(@ref)
      end
    end

    def _make_cache
      @prb.transaction do
	@cache = @prb.all_attr(@ref)
      end
    end
    
    def _setup_cache
      return @cache if @cache
      pool_info = Thread.current[:DBIPool]
      @cache = pool_info[[:PRb, @ref]] ||= {}
    end
    
    def _prb_cache
      _setup_cache
    end
  end

  class PRbObject
    include PRbO

    def self.knew(*args)
      if PRb === args[0]
	prb = args.shift
      else
	prb = PRb.primary
      end
      prb.transaction do
	it = prb.alloc(self)
	it.init(*args)
	it
      end
    end
    def init(*args); end
      
    def initialize(prb, ref)
      @prb = prb
      @ref = ref
    end

    def self.prb_attr(*args); 
      args.each do |sym|
	module_eval("def #{sym}; _get(:#{sym}); end")
	module_eval("def #{sym}=(v); _set(:#{sym}, v); end")
      end
    end

    def ==(other)
      return false unless type === other
      @ref == other._prb_ref && @prb.db_name == other._prb_db_name
    end
  end

  class PRbList < PRbObject
    def each(&block)
      _list_ary.each(&block) 
    end

    def push(v)
      _list_push(v)
    end

    def unshift(v)
      _list_unshift(v)
    end

    def pop 
      _list_pop
    end

    def shift
      _list_shift
    end

    def head
      _list_head
    end

    def tail
      _list_tail
    end

    def to_a
      _list_ary
    end

    def length
      _list_length
    end
    alias size length
  end

  class PRbAttr < PRbObject
    def keys
      _keys
    end

    def [](key)
      key = key.to_s.intern unless Symbol === key
      _get(key)
    end

    def []=(key, value)
      key = key.to_s.intern unless Symbol === key
      _set(key, value)
    end

  end

  class PRbRoot < PRbAttr
    def add(obj)
      raise("Bad DB Name") unless _prb_db_name == obj._prb_db_name
      key = obj._prb_ref
      _set(key, obj)
      obj
    end

    def delete(obj_or_key)
      key = if PRbObject === obj_or_key
	      raise("Bad DB Name") unless _prb_db_name == obj_or_key._prb_db_name
	      obj_or_key._prb_ref
	    else
	      obj_or_key.to_i
	    end
      _delete(key)
      self
    end

    def fetch(key)
      _get(key)
    end

    def all_entry
      _list_ary
    end
  end

  # FIXME
  class PRbAlist < PRbObject; end

  module Prim
    def primitive?(obj)
      case obj
      when Symbol, String, Float, Bignum, Time
	true
      end
      false
    end

    def prim_dump(obj)
      case obj
      when Symbol
	obj.id2name
      when String
	obj
      when Float
	sprintf("%.16g", obj)
      when Bignum
	obj.to_s
      when Time
	obj.to_i.to_s
      else
	nil
      end
    end

    def prim_load(klass, value)
      case klass
      when 'Symbol'
	value.intern
      when 'String'
	value
      when 'Float'
	value.to_f
      when 'Bignum'
	value.to_i
      when 'Time'
	Time.at(value.to_i)
      else
	nil
      end
    end
  end

  class Seq
    include DBIPool::DBIPoolConn
    def initialize(pool, name)
      @pool = pool
      @name = name
    end

    def next_value
      @pool.transaction(true) do
	conn.do("update seq set value=value+incr where name=?", @name)
	conn.select_one("select value from seq where name=?", @name)[0]
      end
    end
    
    def create(start, incr)
      start = start - incr
      @pool.transaction(true) do
	conn.do("insert into seq values (?, ?, ?)", @name, start, incr)
      end
    end
  end

  class SymbolTable
    include DBIPool::DBIPoolConn
    def initialize(pool)
      @pool = pool
      @cache = {}
      @rev_cache = {}
      @symbol_id = 6
      add_symbol(:Symbol, @symbol_id)
      @symseq = Seq.new(pool, 'symbol_seq')
      load_symbol_cache rescue DBI::ProgrammingError
    end
    attr_reader :table
    
    def to_s
      @table
    end
    
    def symbol(sym)
      return @cache[sym] if @cache.include?(sym)
      new_id = alloc_symbol(sym)
      add_symbol(sym, new_id)
      new_id
    end

    def [](num)
      return @rev_cache[num] if @rev_cache.include?(num)
      @pool.transaction(true) do |dbh|
	sql = "select value from symbol where id=#{num}"
	str = dbh.select_one(sql)[0]
	return nil unless str
	add_symbol(str.intern, num)
      end
      return @rev_cache[num]
    end

    def create
      @symseq.create(@symbol_id, 4)

      new_id = @symseq.next_value
      conn.do("insert into symbol values (?, ?)", new_id, 'Symbol')
      add_symbol(:Symbol, new_id)

      new_id = @symseq.next_value
      root_klass = PRbRoot.to_s
      conn.do("insert into symbol values (?, ?)", new_id, root_klass)
      add_symbol(root_klass.intern, new_id)
    end
    
    private
    def load_symbol_cache(limit=64)
      @pool.transaction(true) do |dbh|
	sql = "select id, value from symbol order by id limit ?"
	dbh.select_all(sql, limit) do |row|
	  add_symbol(row[1].intern, row[0])
	end
      end
    end

    def add_symbol(sym, num)
      @cache[sym] = num
      @rev_cache[num] = sym
    end

    def alloc_symbol(sym)
      remain = 5
      str = sym.to_s

      begin
	row = conn.select_one("select id from symbol where value=?", str)
	return row[0] if row

	@pool.transaction(true) do |dbh|
	  row = conn.select_one("select id from symbol where value=?", str)
	  unless row
	    new_id = @symseq.next_value
	    dbh.do("insert into symbol values (?, ?)", new_id, str)
	    row = conn.select_one("select id from symbol where value=?", str)
	  end
	  row ? row[0] : nil
	end
      rescue DBI::ProgrammingError
	if remain > 0
	  remain = remain - 1
	  retry
	end
      end
    end
  end

  class Store
    include DBIPool::DBIPoolConn
    include Prim

    @prb = {}
    @primary = nil
    def self.[](name)
      @prb[name]
    end

    def self.[]=(name, prb)
      Thread.exclusive do
	@prb[name] = prb unless @prb.include?(name)
	@primary = prb unless @primary
      end
    end
    
    def self.primary
      @primary
    end
    
    def initialize(pool)
      @pool = pool
      @symcache = {}
      
      @symtab = SymbolTable.new(pool)
      @prbseq = Seq.new(pool, 'object_seq')
      
      @root_id = 8
      @root = PRbRoot.new(self, @root_id)
      
      @db_name = @pool.db_name
      type[@db_name] = self
    end
    attr_reader :root, :root_id, :db_name
    
    def transaction(&block)
      @pool.transaction(&block)
    end
    
    def create_table
      sql = <<EOQ
create table seq (name text, value int8, incr int8, primary key (name));
EOQ
      conn.do(sql)

      conn.do("create table symbol (id int8, value text, primary key (value))")

      sql = <<EOQ
create table object (id int8, 
                     klass int8, value text, 
                     primary key (id));
EOQ
      conn.do(sql)

      sql = <<EOQ
create table alist (id int8 , 
                    key int8, value int8, 
                    primary key (id, key));
EOQ
      conn.do(sql)

      conn.do("select * into o_gc from object")
      conn.do("select * into a_gc from alist")
    end

    def setup_table
      transaction do
	create_table
      end

      transaction do
	@symtab.create
	@prbseq.create(@root_id, 4)
      end

      transaction do
	alloc_obj(PRbRoot, nil)
      end
    end
    
    def set_prop(ref, key, value)
      map = {}
      kref = ref_walk(key, map)
      vref = ref_walk(value, map)
      lock(ref)
      conn.do("delete from alist where id=? and key=?", ref, kref)
      alloc_alist(ref, kref, vref)
      kref
    end
    
    def get_prop(ref, key)
      kref = ref_walk(key, {})
      sql = <<EOQ
select a.value, b.klass, b.value from 
    (alist as a left join object as b on a.value=b.id) 
    where a.id=? and a.key=?;
EOQ
      r = conn.select_one(sql, ref, kref)
      return nil if r.nil?
      prop_to_obj(*r)
    end
    
    def delete_prop(ref, key)
      kref = ref_walk(key, {})
      conn.do("delete from alist where id=? and key=?", ref, kref)
    end

    def prop_keys(ref)
      sql = <<EOQ
select b.value from alist as a, symbol as b where a.key=b.id and a.id=?;
EOQ
      result = []
      conn.select_all(sql, ref) { |r| result.push(r[0].intern) }
      result
    end

    def ary_keys(ref)
      sql = <<EOQ
select (key-1)/2 from alist where key % int8(2) = 1 and id=?;
EOQ
      result = []
      conn.select_all(sql, ref) { |r| result.push(r[0]) }
      result
    end
    
    def list_push(ref, value)
      vref = ref_walk(value, {})
      sql = <<EOQ
insert into alist values 
  (?, 
   (select coalesce(max(key),-1)+2 from alist 
    where key%int8(2) in (-1,1) and id = ?), 
   ?)
EOQ
      conn.do(sql, ref, ref, vref)
    end

    def list_unshift(ref, value)
      vref = ref_walk(value, {})
      sql = <<EOQ
insert into alist values 
  (?, 
   (select coalesce(min(key),1)-2 from alist 
    where key%int8(2) in (-1,1) and id = ?), 
   ?)
EOQ
      conn.do(sql, ref, ref, vref)
    end
   
    def list_adjust(ref)
      sql = <<EOQ
select value from alist where id=? and (key-1)%int8(2) =0 order by key
EOQ
      ary = []
      conn.select_all(sql, ref) { |r| ary.push(r[0]) }

      sql = <<EOQ
delete from alist where id=? and (key-1)%int8(2) = 0
EOQ
      conn.do(sql, ref)

      ary.each_with_index do |v, idx|
	conn.do("insert into alist values (?, ?, ?)", ref, idx*2+1, v)
      end
    end
  
    def list_key_range(ref)
      r = conn.select_one("select min(key), max(key) from alist where id=?", ref)
      return nil, nil if r.nil? || r[0].nil?
      return r[0], r[1]
    end

    def list_head(ref)
      min_k, max_k = list_key_range(ref)
      return nil if min_k.nil?
      
      sql = "select a.value, b.klass, b.value from 
             (alist as a left join object as b on a.value=b.id) 
             where a.id=? and a.key=?;"
      r = conn.select_one(sql, ref, min_k)
      return nil if r.nil?
      prop_to_obj(*r)
    end
    
    def list_tail(ref)
      min_k, max_k = list_key_range(ref)
      return nil if max_k.nil?
      
      sql = "select a.value, b.klass, b.value from 
    (alist as a left join object as b on a.value=b.id) 
    where a.id=? and a.key=?;"
      r = conn.select_one(sql, ref, max_k)
      return nil if r.nil?
      prop_to_obj(*r)
    end

    def list_pop(ref)
      min_k, max_k = list_key_range(ref)
      return nil if max_k.nil?

      sql = "select a.value, b.klass, b.value from 
    (alist as a left join object as b on a.value=b.id) 
    where a.id=? and a.key=?;"
      r = conn.select_one(sql, ref, max_k)
      return nil if r.nil?
      conn.do("delete from alist where id=? and key=?", ref, max_k)
      
      list_adjust(ref) if (min_k < MIN_INT8+4)
      prop_to_obj(*r)
    end

    def list_shift(ref)
      min_k, max_k = list_key_range(ref)
      return nil if min_k.nil?
      
      sql = "select a.value, b.klass, b.value from 
    (alist as a left join object as b on a.value=b.id) 
    where a.id=? and a.key=?;"
      r = conn.select_one(sql, ref, min_k)
      return nil if r.nil?
      conn.do("delete from alist where id=? and key=?", ref, min_k)
      
      list_adjust(ref) if (max_k > MAX_INT8-4)
      prop_to_obj(*r)
    end
    
    def list_ary_x(ref)
      sql = <<EOQ
select a.value, b.klass, b.value from 
    (alist as a left join object as b on a.value=b.id) 
    where a.id=? and (a.key-1)%int8(2) = 0 order by a.key;
EOQ
      result = []
      conn.select_all(sql, ref) { |r| result.push(prop_to_obj(*r)) if r && r[0] }
      result
    end

    def list_ary(ref)
      sql = <<EOQ
select a.value, b.klass, b.value, a.key from 
    (alist as a left join object as b on a.value=b.id) 
    where a.id=? order by a.key;
EOQ
      result = []
      conn.select_all(sql, ref) do |r|
	if r && r[0] && ((r[3]-1) % 2) == 0
	  result.push(prop_to_obj(r[0], r[1], r[2]))
	end
      end
      result
    end
    
    def list_length(ref)
      sql = <<EOQ
select count(a.value) from 
    (alist as a left join object as b on a.value=b.id) 
    where a.id=? and (a.key-1)%int8(2) = 0;
EOQ
      conn.select_one(sql, ref)[0]
    end

    def all_attr(ref)
      sql = <<EOQ
select 
  a.key as k_id, b.klass as k_klass, b.value as k_value, 
  a.value as v_id, d.klass as v_klass, d.value as v_value 
  from 
  ((alist as a left join object as b on a.key=b.id) 
    left join object as d on a.value=d.id) where a.id=?;
EOQ
      result = {}
      conn.select_all(sql, ref) do |r|
	key = prop_to_obj(*(r[0..2]))
	value = prop_to_obj(*(r[3..5]))
	result[key] = value
      end
      result
    end
	     

    def list_delete_value(ref, value)
      vref = x_obj2ref(value)
      return if vref.nil?
      conn.do("delete from alist where id=? and value=? and (key-1)%int8(2)= 0",
	      ref, vref)
    end

    def lock(ref)
      conn.do("select id from object where id=? for update", ref)
    end
    
    def regist(obj)
      ref_walk(obj, {})
    end
    
    def alloc(klass)
      klass.new(self, alloc_obj(klass, nil))
    end

    def gc
      transaction do
	conn.do("select * from object for update")
	conn.do("select * from alist for update")

	conn.do("delete from a_gc")
	conn.do("delete from o_gc")
	
	conn.do("insert into o_gc select * from object where id=?", @root_id)
	conn.do("insert into a_gc select * from alist where id=?", @root_id)
	
	loop do
	  sql =<<EOQ
from object as o, a_gc 
   where o.id not in (select o_gc.id from o_gc) 
    and (a_gc.key = o.id or a_gc.value = o.id);
EOQ
	  count, = conn.select_one("select count(*) #{sql}")
	  break if count == 0
	  
	  conn.do("insert into o_gc select distinct o.* #{sql}")
	  sql = <<EOQ
insert into a_gc select a.* from alist as a, o_gc as o
  where a.id not in (select id from a_gc) and o.id=a.id
EOQ
	  conn.do(sql)
	end

	conn.do("delete from alist")
	conn.do("delete from object")
	
	conn.do("insert into object select * from o_gc")
	conn.do("insert into alist select * from a_gc")
      end
    end

    private
    def alloc_obj(klass, prim_value)
      if klass == Array
	klass = PRbList
      elsif klass == Hash
	klass = PRbAlist
      end
      ref = @prbseq.next_value
      kid = @symtab.symbol(klass.to_s.intern)
      conn.do("insert into object values (?, ?, ?)", ref, kid, prim_value)
      ref
    end
    
    def alloc_alist(ref, kref, vref)
      conn.do("insert into alist values (?, ?, ?)", ref, kref, vref)
    end
    
    def prop_to_obj(ref, klass, value)
      return true if ref == true.id
      return false if ref == false.id
      return nil if ref == nil.id
      return @symtab[ref] if (ref - 2) % 4 == 0
      return (ref - 1) / 2 if (ref - 1) % 2 == 0
      
      klass = @symtab[klass].to_s
      case klass
      when "String"
	return value
      when "PRb::PRbList"
	return PRbList.new(self, ref)
      when "PRb::PRbAlist"
	return PRbAlist.new(self, ref)
      when /.+/
	v = prim_load(klass, value)
	return v if v
	k = eval(klass)
	return k.new(self, ref)
      end
    end
    
    def x_obj2ref(obj)
      case obj
      when true, false, nil, Fixnum
	obj.id
      when Symbol
	@symtab.symbol(obj)
      when PRbO
	obj._prb_ref
      else
	nil
      end
    end
    
    def ref_walk(obj, map)
      ref = x_obj2ref(obj)
      return ref unless ref.nil?
      return map[obj.id] if map.include? obj.id
      
      ref = alloc_obj(obj.type, prim_dump(obj))
      map[obj.id] = ref
      
      case obj
      when Array
	obj.each_with_index do |v, i|
	  kref = ref_walk(i, map)
	  vref = ref_walk(v, map)
	  alloc_alist(ref, kref, vref)
	end
      when Hash
	obj.each do |k, v|
	  kref = ref_walk(k, map)
	  vref = ref_walk(v, map)
	  alloc_alist(ref, kref, vref)
	end
      end
      return ref
    end
  end

  def primary
    Store.primary
  end
  module_function :primary
end
