require 'criteria'

module Criteria
  class SQLTable < Table
    attr_reader   :table
    attr_accessor :_typemap, :_cols

    def initialize(table)
      @table          = table
      @_typemap       = nil
      @criterion_type = SQLCriterion
    end


    def to_s
      return @table
    end


    def insert(h, *args)
      unless h.is_a? Hash
        args.unshift h
        h=nil
      end

      ins  = "INSERT INTO #{table}"
      ins << (h ? _insert_hash(h) : _insert_arr(args))
    end #m:insert


    def _insert_hash(h)
      cols = []
      vals = []

      h.each {
        | k, v |
        cols << k
        vals << v
      }

      cols = cols.map { |c| c.to_s }.join(", ")
      vals = vals.map { |v| self.class.translate_item(v) }.join(", ")

      return " (#{cols}) VALUES (#{vals})"
    end #m:_insert_hash

    def _insert_arr(a)
      vals = a.map { |v| self.class.translate_item(v) }.join(", ")
      return " VALUES (#{vals})"
    end #m:_insert_arr


    protected :_insert_hash, :_insert_arr


    def _parse_row(cols, row)
      cols = @_cols if cols.empty?
      tm   = @_typemap
      i    = 0

      row.map! {
        | v |
        val  = _parse_val(v, tm[cols[i]]);
        i   += 1
        val
      }
      return row
    end #m:_parse_row

    def _parse_val(val, type)
      return val                    if type == String
      return val.to_i               if type == Integer
      return true                   if type == TrueClass
      return false                  if type == FalseClass
      return val.to_f               if type == Float
      return Time.at(val.to_i)      if type == Time
      return val
    end #m:_parse_val


    def SQLTable.create(table, *args)
      ct = "CREATE TABLE #{table}"
      
      vals = ""
      args.each {
        | a |
        vals << ", " if vals.size > 0

        if a.is_a? Array
          vals << a[0].to_s << " "
          if(a[2])
            vals << a[2]
          else
            vals << map_type(a[1])
          end
        else
          vals << a.to_s
        end
      }

      return "#{ct} (#{vals})"
    end #m:SQLTable.create


    def SQLTable.drop(table)
      return "DROP TABLE #{table}"
    end


    def _store_cols(args)
      tm   = Hash.new
      cols = Array.new
      args.each {
        | a |
        next unless a.is_a? Array
        tm[a[0]] = a[1] 
        cols    << a[0]
      }
      @_typemap = tm
      @_cols    = cols
    end #m:_store_cols


    def SQLTable.map_type(cls)
      return "text"        if cls == String
      return "int"         if cls == Integer
      return "float"       if cls == Float
      return "int"         if cls == Time
      return "int"         if cls == Boolean
    end #m:SQLTable.map_cls


    def SQLTable.translate_item(item)
      case item
      when  String;     "'#{item}'"   
      when  Regexp;     "'#{item}'"   
      when  Time;       item.to_i.to_s
      when  TrueClass;  "1"
      when  FalseClass; "0"
      when  NilClass;   "NULL"        
      else;             item.to_s
      end
    end #m:SQLTable.translate_item
  end #c:SQLTable


  class SQLFn < Blank
    attr_reader :name, :args
    def initialize(name, args)
      super()
      @name = name
      @args = args
    end #m:initialize

    def to_s
      return "#@name(#{@args.join(", ")})"
    end

    def SQLFn.method_missing(m, *args)
      return SQLFn.new(m.to_s.upcase, args)
    end #m:SQLFn

    def SQLFn.pk(*args)
      return SQLFn.new("PRIMARY KEY", args)
    end
  end #c:SQLFn


  class SQLCriterion < Criterion
    attr_accessor :limit, :offset, :order, :order_by

    protected
    attr_accessor :join_on, :joins, :join_type

    def initialize(*args)
      super
      @order     = nil
      @order_by  = nil

      @join_on   = nil
      @joins     = nil
      @join_type = nil
      
      @limit     = nil
      @offset    = nil
    end #m:initialize


    def _mkselect(s, *args)
      # SELECT and column clause
      s << "SELECT "
      s << args.map {
        |s|
        if s.is_a?(SQLTable); "#{s}.*"
        else                  s.to_s
        end
      }.join(", ")

      s << "*" unless args.size > 0
    end #m:_mkselect

    def _mkfrom(s, *args)
      # FROM clause
      s << " FROM "
      s << "#{@_parent.table}"    if(@_parent.table && args.size == 0)
      s << args.map { |i| i.is_a?(SQLCriterion) ? i._parent.table : i }.uniq.join(", ")
    end

    def _mkjoin(s, *args)
      return unless @join_type

      # JOIN clause
      s << " #@join_type "
      s << @joins.map { |j| j.to_s }.join(", ")

      if(@join_on.is_a? SQLCriterion)
        s << " ON "
        s << @join_on.to_s
      elsif(@join_on.is_a? Array)
        s << " USING("
        s << @join_on.join(", ")
        s << ")"
      end
    end #m:_mkjoin

    def _mkwhere(s, *args)
      # WHERE clause
      s << " WHERE #{self.to_s}"
    end #m_mkwhere

    def _mklimit(s, *args)
      # LIMIT and OFFSET clause
      if @limit
        s << " LIMIT"
        s << " #@offset, "   if @offset
        s << " #@limit"
      end
    end #m:_mklimit

    def _mkorder(s, *args)
      # ORDER clause
      if @order_by
        s << " ORDER BY #{@order_by.join(', ')}"
        s << " #@order"                             if order
      end
    end #m:_mkorder

    def _mkset(s, h)
      a = []
      h.each {
        | k, v |
        a << ("#{k} = " << @_parent.class.translate_item(v))
      }
      s << " SET #{a.join(", ")}"
    end #m:_mkset

    def self._defjoin(name, type, on = false, *args)
      dupstr = "q = self.dup\n"

      if on
        s  = "def #{name.to_s}(on, *args)\n"
        s << dupstr
        s << "q.join_on = on\n"
      else
        s  = "def #{name.to_s}(*args)\n"
        s << dupstr
      end

      s << <<-END
           q.joins     = args
           q.join_type = "#{type}"
           return q
        end
      END

      self.class_eval s
    end
    public

    def select(*args)
      s = ""
      _mkselect(s, *args)
      _mkfrom(s, *args)
      _mkjoin(s, *args)
      _mkwhere(s, *args) unless @field.is_a?(Symbol) && @field == :*
      _mklimit(s, *args)
      _mkorder(s, *args)
      return s
    end #m:select

    def delete
      s = "DELETE FROM #{@_parent.table}"
      _mkwhere(s) unless @field.is_a?(Symbol) && @field == :*
      return s
    end

    def update(h)
      s = "UPDATE #{@_parent.table}"
      _mkset(s, h)
      _mkwhere(s) unless @field.is_a?(Symbol) && @field == :*
      return s
    end

    _defjoin(:join,        "JOIN")
    _defjoin(:left_join,   "LEFT JOIN")
    _defjoin(:right_join,  "RIGHT JOIN")
    _defjoin(:full_join,   "FULL JOIN")
    _defjoin(:cross_join,  "CROSS JOIN")

    _defjoin(:inner_join,             "INNER JOIN",         true)
    _defjoin(:left_outer_join,        "LEFT OUTER JOIN",    true)
    _defjoin(:right_outer_join,       "RIGHT OUTER JOIN",   true)
    _defjoin(:full_outer_join,        "FULL OUTER JOIN",    true)

    def to_s
      if @chain.size < 1
        s  = ""
        s << "#{@_parent.table}."     if(@_parent.table && @field.is_a?(Symbol))
        s << @field.to_s
        return s
      end

      s  = "("
      s << "#{@_parent.table}."       if(@_parent.table && @field.is_a?(Symbol))
      s << "#{@field.to_s} #{_translate_fn @chain[0]}"
      @chain[1...@chain.size].each {
        | item |
        s << " "
        s << "#{@_parent.table}."     if(@_parent.table && item.is_a?(Symbol))
        s << @_parent.class.translate_item(item)
      }

      s << ")"
      return s
    end #m:to_s

    def _translate_fn(op)
      case op
      when :&;    "AND"
      when :|;    "OR"
      when :==;   "="
      when :nil?; "IS NULL"
      else;     op
      end
    end #m:_translate_fn
    protected :_translate_fn
  end #c:SQLCriterion
end #M:Criteria
