# vim:sw=4:sts=4:tw=0

class Domain
    def can_assign(other)
	if other.kind_of?(Class) && other < Relation
	    me = self
	    other.instance_eval {
		@columns[compare_column].domain == me
	    }
	else
	    can_assign_absolute(other)
	end
    end
    def ==(other)
	other.kind_of?(self.class) 
    end
    class SQL < self
	class Varchar < Domain
	    def initialize(length)
		@length = length
	    end
	    def sql
		"VARCHAR(#{@length})"
	    end
	    def can_assign_absolute(object)
		object.kind_of?(::String) && object.length <= @length
	    end
	    def ==(other)
		super && other.length == length
	    end
	end
	class Integer < Domain
	    def sql() "INTEGER" end
	    def can_assign_absolute(object)
		object.kind_of?(::Integer)
	    end
	end
	class Text < Domain
	    def sql() "TEXT" end
	    def can_assign_absolute(object)
		object.kind_of?(::String)
	    end
	end
	def self.new(sqltype)
	    case sqltype
	    when /^VARCHAR\s*(\(\d+\))?\s*$/
		Varchar.new((sqltype =~ /^VARCHAR\s*\((\d+)\)\s*$/) ? $1.to_i : 255)
	    when "INTEGER"
		Integer.new
	    when "TEXT"
		Text.new
	    else
		raise RuntimeError, "Unknown SQL domain #{sqltype.inspect}"
	    end
	end
    end
end

class Relation
    class FIXMEError < RuntimeError
    end
    class Column
	attr_reader :name, :domain
	def initialize(name, domain)
	    @name = name
	    @domain = domain
	end
    end
    class Compare
	class True < self
	    def to_sql() "true" end
	end
	class False < self
	    def to_sql() "false" end
	end
    end
    # Initializer for subclasses.
    @columns = {}
    @tablename = self.name.gsub(/.*::/, "")
    @virtual = true
    @constraints = Compare::True.new
    @assignments = {}
    # Primary key
    @pkey = nil
    def self.inherited(child)
	columns = @columns
	constraints = @constraints
	assignments = @assignments
	child.instance_eval {
	    @columns = columns.dup
	    @tablename = child.name.gsub(/.*::/, "")
	    @virtual = false
	    @constraints = constraints.dup
	    @assignments = assignments.dup
	    @order_by = []
	    # Last column we were "seen as"
	    @lastcolumn = nil
	}
    end
    # NOTE
    # Patterns for optimizing and and or:
    #  Pullup OR:
    #   (and x (or y z1 zz1) (or y z2 zz2))  -> (or y (and (or z1 zz1) (or z2 zz2)))
    #   (or x (or y z1 zz1))   -> (or x y z1 zz1)
    #  Pullup AND:
    #   (or x (and y z1 zz1) (and y z2 zz2)) -> (and y (or (and z1 zz1) (and z2 zz2))
    #   (and x (and y z1 zz1)) -> (and x y z1 zz1)
    class Compare
	def to_sql
	    raise RuntimeError, "You need to implement to_sql in the child class!"
	end
	class OperatorBase < self
	    attr_reader :column, :other
	    def initialize(column, other)
		@column = column
		@other = other
	    end
	    def to_sql
		"#{self.column}#{self.operator}#{Relation._sql_value(self.other)}"
	    end
	    def <=>(b)
		[:class, :column, :other].each { |msg|
		    cmp = (self.send msg) <=> (b.send msg)
		    return self.__id__ <=> b.__id__ unless cmp
		    return cmp if cmp != 0
		}
		0
	    end
	    def ==(b)
		b.class == self.class && (self <=> b) == 0
	    end
	    def eql?(b)
	        self == b
	    end
	    def hash
		column.hash + other.hash
	    end
	end
	OPERATORS = [["Equal",    "=", "=="],
		     ["LessThan", "<"],
		     ["LessEqual", "<="],
		     ["Greater", ">"],
		     ["GreaterEqual", ">="],
		     #["NotEqual", "<>", "!="]
		     ]
	OPERATORS.each { |name, operator|
	     module_eval "class #{name} < OperatorBase;    def operator() \"#{operator}\" end end"
	 }
	 class LogicalBase < self
	     def initialize(a, b)
		 raise RuntimeError, "nil paramete 1r" unless a
		 raise RuntimeError, "nil paramete 2r" unless b
		 if a.kind_of?(self.class) && b.kind_of?(self.class)
		     raise unless a.instance_eval { @operators }
		     raise unless b.instance_eval { @operators }
		     @operators = a.instance_eval { @operators } | b.instance_eval { @operators }
		 elsif (a.kind_of? self.class)
		     @operators = a.instance_eval { @operators }.dup.push(b).uniq
		 else
		     @operators = [a, b].uniq
		 end
	     end
	     def operator
		 raise RuntiemError, "Not appropriate"
	     end
	     # :nodoc:
	     # The shortciruit is the Compare class that result in this op
	     #   shortcircuiting (false for and, true for or)
	     # The noop operation is the operation that can be eliminated from
	     #   this op - false for or, true for and.
	     def logical_to_sql(shortcircuit, noop, opname)
		 return shortcircuit.to_sql if @operators.find { |v| v.kind_of?(shortcircuit) }
		 operators = @operators.find_all { |v| not v.kind_of?(noop) }
		 case operators.length
		 when 0
		     return noop.to_sql
		 when 1
		     return operators[0].to_sql
		 else
		     "(" << operators.collect { |v| v.to_sql }.join(" #{opname} ") << ")"
		 end
	     end
	 end
	 class And < LogicalBase
	     def to_sql
		 logical_to_sql(Compare::False, Compare::True, "AND")
	     end
	 end
	 class Or < LogicalBase
	     def to_sql
		 logical_to_sql(Compare::True, Compare::False, "OR")
	     end
	 end
    end
    def self.<=>(other)
	if other.compare_column == @lastcolumn && other.instance_eval { @constraints } == @constraints && other.instance_eval { @columns } 
	    return 0
	else
	    return self.__id__ <=> other.__id__
	end
    end
    # Just easier to read
    def self.inspect
	retval = "#<Class Relation #{@tablename}"
	retval << ":#{@lastcolumn}" if @lastcolumn
	retval << ">"
	retval
    end
    #
    # Define a single column for a table
    #
    def self.column(name, type)
	name = name.to_s
	raise RuntimeError, "Illegal characters in name #{name.inspect}" unless name =~ /^[a-z_][a-z0-9_]*$/i
	@columns[name] = Column.new(name, Domain::SQL.new(type))
    end
    # Override the name of a table.  The default is that this is taken from the
    # name of the class.
    def self.tablename=(name)
	@tablename = name
    end
    #
    # Generate the SQL for this table
    #
    def self.sql
	# Skip virtual tables
	return "" if @virtual
	retval = "CREATE TABLE #{@tablename} (\n"
	retval << @columns.collect { |name, c| "    #{c.name} #{c.domain.sql}"
	}.join(",\n")
	retval << "\n);\n"
	return retval
    end
    #
    # Mark this as not being a table, but just an intermediate class that
    # should not be inserted into the database and where queries won't work.
    # (Or at least not for the time being - might be cool to make them
    # work...)
    # FIXME Should be tested
    #
    def self.virtual
	relation_init
	@virtual = true
    end
    #
    # Set up a primary key
    # FIXME Setting should be implemented
    #
    def self.pkey(*args)
	@pkey
    end
    #
    # Reference another table.
    # This brings over the column(s) corresponding to the primary key, if they
    # do not exist already (which is an error)
    #
    def self.reference(relation, symbol=nil)
	# FIXME The finish! et al should really be delayed until we start using the database.
	relation.finish!
	columns = relation.pkey
	if symbol
	    # FIXME Should have a test for this
	    raise RuntimeError, "Cannot do symbol-based reference to compound-keyed table" unless columns.length == 1
	    @columns[symbol.to_s] = relation.instance_eval { @columns[columns[0]] }
	else
	    columns.each { |column|
		# FIXME Should have a test for this
		raise RuntimeError, "Duplicate column through reference: #{column}" \
		    if @columns[column]
	    }
	    columns.each { |column|
		@columns[column] = relation.instance_eval { @columns[column] }
	    }
	end
	nil
    end
    #
    # Add some SQL that is to be used for initialization.
    # Primarily helpful for "helper tables" (which work sort of as domain
    # definers)
    # FIXME Should be implemented (or the concept should be replaced)
    #
    def self.init_sql(sql, *bindings)
    end
    def self._assert_column(column)
	raise RuntimeError, "Unknown column #{column}" unless @columns.has_key?(column.to_s)
    end
    #
    # Select a subset of the data we should have (or all of it)
    #
    def self.select(args = {})
	finish!
	clone.instance_eval {
	    @constraints = @constraints.dup
	    args.each { |key, value|
		_assert_column(key)
		# FIXME Should use domains
		raise RuntimeError, "Unknown type #{value.class.name}" unless value.kind_of?(Numeric) || value.kind_of?(String) || (value.kind_of?(Class) && value < Relation)
		_add_constraint(Compare::Equal.new(key, value))
	    }
	    self
	}
    end
    # Generate an SQL string, quoted as appropriate
    def self._sqlquote(text)
	"'" + text.to_s.gsub(/(['\\\\])/) { "\\#{$1}" } + "'"
    end
    def self._sql_value(value)
	case value
	when Numeric
	    value
	when Class
	    # Has to be a Relation due to restrictions elsewhere
	    value.compare_column
	when String
	    _sqlquote(value)
	else
	    raise RuntimeError, "Unknown compare class: #{value.class}"
	end
    end
    # Get the selector string for this selector.
    def self._sql_where
	constraint = @constraints.to_sql
	constraint = (constraint =~ /^\((.*)\)$/) ? $1 : constraint
	if constraint == "" || constraint == "true"
	    ""
	else
	    " WHERE " << constraint
	end
    end
    def self._sql_delsel_guts
	" FROM #{@tablename}" << _sql_where
    end
    #
    # Generate SQL for the delete! method
    #
    def self._sql_delete!
	"DELETE" << _sql_delsel_guts
    end
    #
    # Generate SQL for a selector.
    #
    def self._select_sql
	"SELECT *" << _sql_delsel_guts << _order_by_sql
    end
    #
    # "Complete" the definition of a table:
    #	* Create any auto-created columns
    #
    def self.finish!
	# FIXME Should be protected against being called several times.
	cname = "#{@tablename.downcase}_id"
	@pkey = [cname]
	column(cname, "INTEGER")
    end
    def self.order_by_internal(order, columns)
	finish!
	clone.instance_eval {
	    @order_by = @order_by.dup
	    columns.each { |column|
		column = column.to_s
		raise RuntimeError, "Duplicate column in order_by clauses" if @order_by.find { |c| c == column }
		_assert_column(column)
		@order_by.push(column + order)
	    }
	    self
	}
    end
    #
    # Set up ordering for query results
    #
    def self.order_by(*columns)
	order_by_internal("", columns)
    end
    #
    # Set up ordering for query results - reverse
    #
    def self.order_by_desc(*columns)
	order_by_internal(" DESC", columns)
    end
    #
    # Grab SQL for "ORDER BY"
    #
    def self._order_by_sql
	if @order_by.length != 0
	    " ORDER BY " << @order_by.join(",")
	else
	    ""
	end
    end
    #
    # Set up a "left join" against another table.
    #
    # In a left join, the row from this table (let's call it Lefty, and the
    # rows Lefties) will always be there, and the row from the other table
    # (let's call it Bambini) will either be there or nil.  If there are
    # multiple Bambinis that reference a single Lefty, a random Bambini will be
    # picked.
    #
    def self.leftjoin(other)
	# FIXME More code here
    end
    #
    # Catchup for stuff that we do not directly handle, e.g. column references
    #
    def self.method_missing(method, *args)
	method = method.to_s
	if (method =~ /(.*)=$/)
	    method = $1
	    super unless @columns[method]
	    selfdescr = "#{@tablename}##{method}=: "
	    raise RuntimeError, "#{selfdescr}Bad assignment (domain violation): #{args[0].inspect}" \
		unless @columns[method].domain.can_assign(args[0])
	    case args[0]
	    when Numeric, String
	    when Class
		raise RuntimeError, "#{selfdescr}Unknown assignment: Class #{args[0].name} (not < Relation)" \
		    unless args[0] < Relation
		raise RuntimeError, "#{selfdescr}Cross-table assignment: Attempt to set to #{args[0].name}#{args[0].compare_column}" \
		    unless @tablename == args[0].instance_eval { @tablename }
	    else
		raise RuntimeError, "#{selfdescr}Unknown assignment: #{args[0].class.name}"
	    end
	    @assignments[method] = args[0]
	else
	    super unless @columns[method]
	    clone.instance_eval {
		@lastcolumn = method
		self
	    }
	end
    end
    # :nodoc: Add a constraint for a particular column
    def self._add_constraint(constraint)
	@constraints = Compare::And.new(@constraints, constraint)
    end
    #
    # Build the various comparators (but where did != go?)
    #
    Compare::OPERATORS.each { |name, sqlop, rubyop|
	rubyop ||= sqlop
	module_eval <<EOM
    def self.#{rubyop}(other)
	super unless @lastcolumn
	clone.instance_eval {
	    @constraints = @constraints.dup
	    _add_constraint(Compare::#{name}.new(@lastcolumn, other))
	    # FIXME Not clearing lastcolumn SHOULD result in a test (in
	    # test_column) failing, but somehow doesn't.
	    # @lastcolumn = nil
	    self
	}
    end
EOM
    }
    #
    # Check if a particular column is available
    #
    def self.has_column?(column)
	finish!
	!!@columns[column.to_s]
    end
    #
    # Get the presently active comparison column name
    #
    def self.compare_column
	@lastcolumn
    end
    #
    # Generate SQL for updates
    #
    def self._update_sql
	finish!
	retval = "UPDATE #{@tablename} SET "
	@assignments.each { |key, value|
	    retval << "#{key}=#{_sql_value(value)}"
	}
	retval << _sql_where
	retval
    end
    #
    # Get hash of columns - name => domain
    #
    def self.columns
	@columns.dup
    end
    #
    # Create a sub-relation with fewer columns
    #
    def self.project(*projected)
	clone.instance_eval {
	    new_columns = {}
	    projected.each { |column|
		column = column.to_s
		if @columns[column]
		    new_columns[column] = @columns[column]
		else
		    raise RuntimeError, "Unknown column #{column} in project"
		end
	    }
	    @columns = new_columns
	    self
	}
    end
    #
    # Do a natural join (all columns named the same are tested for equality)
    #
    def self.*(relation)
	if relation.instance_eval { @columns } == @columns
	    clone.instance_eval {
		@constraints = Compare::And.new(@constraints, relation.instance_eval { @constraints })
		self
	    }
	else
	    raise FIXMEError, "Cannot natural join anything that is different yet - sorry!"
	end
    end
    #
    # Do a union (relation schema needs to be the smae)
    #
    def self.+(relation)
	if relation.instance_eval { @columns } == @columns
	    clone.instance_eval {
		@constraints = Compare::Or.new(@constraints, relation.instance_eval { @constraints })
		self
	    }
	else
	    raise RuntimeError, "Cannot do a union against a relation with a different schema"
	end
    end
end
