module Searchlogic
  module Conditions
    # = Magic Methods
    #
    # Handles all method magic, creating methods on the fly, etc. This is needed for modifiers.
    module MagicMethods
      def self.included(klass)
        klass.metaclass.class_eval do
          include ClassMethods
          attr_accessor :added_class_level_conditions, :added_column_equals_conditions, :added_associations
        end
        
        klass.class_eval do
          include InstanceMethods
          alias_method_chain :initialize, :magic_methods
          alias_method_chain :method_missing, :magic_methods
        end
      end
      
      module ClassMethods # :nodoc:
        def column_details
          return @column_details if @column_details
          
          @column_details = []
          
          klass.columns.each do |column|
            column_detail = {:column => column}
            column_detail[:aliases] = case column.type
            when :datetime, :time, :timestamp
              [column.name.gsub(/_at$/, "")]
            when :date
              [column.name.gsub(/_at$/, "")]
            else
              []
            end
            
            @column_details << column_detail
          end
          
          @column_details
        end
      end
      
      module InstanceMethods # :nodoc:
        def initialize_with_magic_methods(*args)
          add_associations!
          add_column_equals_conditions!
          add_class_level_conditions!
          initialize_without_magic_methods(*args)
        end
        
        private
          def add_associations!
            return true if self.class.added_associations
          
            klass.reflect_on_all_associations.each do |association|
              self.class.class_eval <<-"end_eval", __FILE__, __LINE__
                def #{association.name}
                  return @#{association.name} unless @#{association.name}.nil?
                  @#{association.name} = Searchlogic::Conditions::Base.create_virtual_class(#{association.class_name}).new
                  @#{association.name}.object_name = :#{association.name}
                  @#{association.name}.protect = protect
                  objects << @#{association.name}
                  @#{association.name}
                end
            
                def #{association.name}=(conditions)
                  @conditions = nil
                  #{association.name}.conditions = conditions
                end
              
                def reset_#{association.name}!
                  objects.delete(#{association.name})
                  @#{association.name} = nil
                end
              end_eval
            end
          
            self.class.added_associations = true
          end
        
          def add_column_equals_conditions!
            return true if self.class.added_column_equals_conditions
            klass.column_names.each { |name| setup_condition(name) }
            self.class.added_column_equals_conditions = true
          end
          
          def add_class_level_conditions!
            return true if self.class.added_class_level_conditions
            class_level_conditions = self.class.conditions.select { |condition_class| !condition_class.condition_names_for_model.blank? }
            class_level_conditions.each do |condition_class|
              condition_class.condition_names_for_model.each_with_index do |condition_name, index|
                if index == 0
                  add_condition!(condition_class, condition_name, :column => klass.columns_hash[klass.primary_key])
                else
                  add_condition_alias!(condition_name, condition_class.condition_names_for_model.first)
                end
              end
            end
            self.class.added_class_level_conditions = true
          end
        
          def sanitize_method_name(name)
            name.gsub("=", "").gsub(/^(and|or)_/, "")
          end
        
          def extract_column_and_condition_from_method_name(name)
            name_parts = sanitize_method_name(name).split("_")
          
            condition_parts = []
            column = nil
            while column.nil? && name_parts.size > 0
              possible_column_name = name_parts.join("_")
            
              self.class.column_details.each do |column_detail|
                if column_detail[:column].name == possible_column_name || column_detail[:aliases].include?(possible_column_name)
                  column = column_detail
                  break
                end
              end
            
              condition_parts << name_parts.pop if !column
            end
          
            return if column.nil?
          
            condition_name = condition_parts.reverse.join("_")
            condition = nil
          
            # Find the real condition
            self.class.conditions.each do |condition_klass|
              if condition_klass.condition_names_for_column.include?(condition_name)
                condition = condition_klass
                break
              end
            end
                                         
            [column, condition]
          end
        
          def breakdown_method_name(name)
            column_detail, condition_klass = extract_column_and_condition_from_method_name(name)
            if !column_detail.nil? && !condition_klass.nil?
              # There were no modifiers
              return [[], column_detail, condition_klass]
            else
              # There might be modifiers
              name_parts = name.split("_of_")
              column_detail, condition_klass = extract_column_and_condition_from_method_name(name_parts.pop)
              if !column_detail.nil? && !condition_klass.nil?
                # There were modifiers, lets get their real names
                modifier_klasses = []
                name_parts.each do |modifier_name|
                  size_before = modifier_klasses.size
                  self.class.modifiers.each do |modifier_klass|
                    if modifier_klass.modifier_names.include?(modifier_name)
                      modifier_klasses << modifier_klass
                      break
                    end
                  end
                  return if modifier_klasses.size == size_before # there was an invalid modifer, return nil for everything and let it act as a nomethoderror
                end
              
                return [modifier_klasses, column_detail, condition_klass]
              end
            end
          
            nil
          end
        
          def build_method_name(modifier_klasses, column_name, condition_name)
            modifier_name_parts = []
            modifier_klasses.each { |modifier_klass| modifier_name_parts << modifier_klass.modifier_names.first }
            method_name_parts = []
            method_name_parts << modifier_name_parts.join("_of_") + "_of" unless modifier_name_parts.blank?
            method_name_parts << column_name
            method_name_parts << condition_name unless condition_name.blank?
            method_name_parts.join("_").underscore
          end
        
          def method_missing_with_magic_methods(name, *args, &block)
            if setup_condition(name)
              send(name, *args, &block)
            else
              method_missing_without_magic_methods(name, *args, &block)
            end
          end
        
          def setup_condition(name)
            modifier_klasses, column_detail, condition_klass = breakdown_method_name(name.to_s)
            if !column_detail.nil? && !condition_klass.nil?
              method_name = build_method_name(modifier_klasses, column_detail[:column].name, condition_klass.condition_names_for_column.first)
            
              if !added_condition?(method_name)
                column_type = column_sql = nil
                if !modifier_klasses.blank?
                  # Find the column type
                  column_type = modifier_klasses.first.return_type
                
                  # Build the column sql
                  column_sql = "{table}.{column}"
                  modifier_klasses.each do |modifier_klass|
                    next unless klass.connection.respond_to?(modifier_klass.adapter_method_name)
                    column_sql = klass.connection.send(modifier_klass.adapter_method_name, column_sql)
                  end
                end
            
                add_condition!(condition_klass, method_name, :column => column_detail[:column], :column_type => column_type, :column_sql_format => column_sql)
            
                ([column_detail[:column].name] + column_detail[:aliases]).each do |column_name|
                  condition_klass.condition_names_for_column.each do |condition_name|
                    alias_method_name = build_method_name(modifier_klasses, column_name, condition_name)
                    add_condition_alias!(alias_method_name, method_name) unless added_condition?(alias_method_name)
                  end
                end
              end
            
              alias_method_name = sanitize_method_name(name.to_s)
              add_condition_alias!(alias_method_name, method_name) unless added_condition?(alias_method_name)
            
              return true
            end
          
            false
          end
        
          def add_condition!(condition, name, options = {})
            options[:column] = options[:column].name if options[:column]
          
            self.class.class_eval <<-"end_eval", __FILE__, __LINE__
              def #{name}_object
                return @#{name} unless @#{name}.nil?
                @#{name} = #{condition.name}.new(klass, #{options.inspect})
                @#{name}.object_name = :#{name}
                objects << @#{name}
                @#{name}
              end
            
              def #{name}
                #{name}_object.value
              end
              alias_method :and_#{name}, :#{name}
              alias_method :or_#{name}, :#{name}
            
              def #{name}=(value)
                @conditions = nil
                #{name}_object.value = value
                value
              end
            
              def and_#{name}=(value)
                #{name}_object.explicit_any = false
                self.#{name} = value
              end
            
              def or_#{name}=(value)
                #{name}_object.explicit_any = true
                self.#{name} = value
              end
            
              def reset_#{name}!
                objects.delete(#{name}_object)
                @#{name} = nil
              end
            end_eval
          end
        
          def added_condition?(name)
            respond_to?("#{name}_object")
          end
        
          def add_condition_alias!(alias_name, name)
            self.class.class_eval do
              alias_method "#{alias_name}_object", "#{name}_object"
              alias_method alias_name, name
              alias_method "#{alias_name}=", "#{name}="
              alias_method "and_#{alias_name}=", "and_#{name}="
              alias_method "or_#{alias_name}=", "or_#{name}="
              alias_method "reset_#{alias_name}!", "reset_#{name}!"
            end
          end
      end
    end
  end
end