
module HH::Sequel
  class Schema
    COMMA_SEPARATOR = ', '.freeze
    COLUMN_DEF = '%s %s'.freeze
    UNIQUE = ' UNIQUE'.freeze
    NOT_NULL = ' NOT NULL'.freeze
    DEFAULT = ' DEFAULT %s'.freeze
    PRIMARY_KEY = ' PRIMARY KEY'.freeze
    REFERENCES = ' REFERENCES %s'.freeze
    ON_DELETE = ' ON DELETE %s'.freeze
    
    RESTRICT = 'RESTRICT'.freeze
    CASCADE = 'CASCADE'.freeze
    NO_ACTION = 'NO ACTION'.freeze
    SET_NULL = 'SET NULL'.freeze
    SET_DEFAULT = 'SET DEFAULT'.freeze
    
    TYPES = Hash.new {|h, k| k}
    TYPES[:double] = 'double precision'
    
    def self.on_delete_action(action)
      case action
      when restrict then RESTRICT
      when cascade then CASCADE
      when set_null then SET_NULL
      when set_default then SET_DEFAULT
      else NO_ACTION
      end
    end
    
    def self.column_definition(column)
      c = COLUMN_DEF % [column[:name], TYPES[column[:type]]]
      c << UNIQUE if column[:unique]
      c << NOT_NULL if column[:null] == false
      c << DEFAULT % SQLite::Database.quote(column[:default]) if column.include?(:default)
      c << PRIMARY_KEY if column[:primary_key]
      c << REFERENCES % column[:table] if column[:table]
      c << ON_DELETE % on_delete_action(column[:on_delete]) if 
        column[:on_delete]
      c
    end
  
    def self.create_table_column_list(columns)
      columns.map {|c| column_definition(c)}.join(COMMA_SEPARATOR)
    end
    
    CREATE_INDEX = 'CREATE INDEX %s ON %s (%s);'.freeze
    CREATE_UNIQUE_INDEX = 'CREATE UNIQUE INDEX %s ON %s (%s);'.freeze
    INDEX_NAME = '%s_%s_index'.freeze
    UNDERSCORE = '_'.freeze
    
    def self.index_definition(table_name, index)
      fields = index[:columns].join(COMMA_SEPARATOR)
      index_name = index[:name] || INDEX_NAME %
        [table_name, index[:columns].join(UNDERSCORE)]
      (index[:unique] ? CREATE_UNIQUE_INDEX : CREATE_INDEX) %
        [index_name, table_name, fields]
    end
    
    def self.create_indexes_sql(table_name, indexes)
      indexes.map {|i| index_definition(table_name, i)}.join
    end
  
    CREATE_TABLE = "CREATE TABLE %s (%s);".freeze
    
    def self.create_table_sql(name, columns, indexes = nil)
      sql = CREATE_TABLE % [name, create_table_column_list(columns)]
      sql << create_indexes_sql(name, indexes) if indexes && !indexes.empty?
      sql
    end
    
    DROP_TABLE = "DROP TABLE %s CASCADE;".freeze
    
    def self.drop_table_sql(name)
      DROP_TABLE % name
    end
    
    class Generator
      attr_reader :table_name
    
      def initialize(table_name, &block)
        @table_name = table_name
        @primary_key = {:name => :id, :type => :serial, :primary_key => true}
        @columns = []
        @indexes = []
        instance_eval(&block)
      end
      
      def primary_key(name, type = nil, opts = nil)
        @primary_key = {
          :name => name, 
          :type => type || :serial,
          :primary_key => true
        }.merge(opts || {})
      end
      
      def primary_key_name
        @primary_key && @primary_key[:name]
      end
      
      def column(name, type, opts = nil)
        @columns << {:name => name, :type => type}.merge(opts || {})
      end
      
      def foreign_key(name, opts)
        @columns << {:name => name, :type => :integer}.merge(opts || {})
      end
      
      def has_column?(name)
        @columns.each {|c| return true if c[:name] == name}
        false
      end
      
      def index(columns, opts = nil)
        columns = [columns] unless columns.is_a?(Array)
        @indexes << {:columns => columns}.merge(opts || {})
      end
      
      def create_sql
        if @primary_key && !has_column?(@primary_key[:name])
          @columns.unshift(@primary_key)
        end
        Schema.create_table_sql(@table_name, @columns, @indexes)
      end
      
      def drop_sql
        Schema.drop_table_sql(@table_name)
      end
    end
    
    attr_reader :instructions
    
    def initialize(&block)
      @instructions = []
      instance_eval(&block) if block
    end
    
    def create_table(table_name, &block)
      @instructions << Generator.new(table_name, &block)
    end
    
    def create(db)
      @instructions.each do |s|
        db.execute(s.create_sql)
      end
    end
    
    def drop(db)
      @instructions.reverse_each do |s|
        db.execute(s.drop_sql) if db.table_exists?(s.table_name)
      end
    end
    
    def recreate(db)
      drop(db)
      create(db)
    end
  end
end

