module ThinkingSphinx
  class FacetSearch < Hash
    attr_accessor :args, :options
    
    def initialize(*args)
      ThinkingSphinx.context.define_indexes
      
      @options      = args.extract_options!
      @args         = args
      
      set_default_options
      
      populate
    end
    
    def for(hash = {})
      for_options = {:with => {}}.merge(options)
      
      hash.each do |key, value|
        attrib = ThinkingSphinx::Facet.attribute_name_from_value(key, value)
        for_options[:with][attrib] = underlying_value key, value
      end
      
      ThinkingSphinx.search *(args + [for_options])
    end
    
    def facet_names
      @facet_names ||= begin
        names = options[:all_facets] ?
          facet_names_for_all_classes : facet_names_common_to_all_classes
        
        names.delete "class_crc" unless options[:class_facet]
        names
      end
    end
    
    private
    
    def set_default_options
      options[:all_facets]  ||= false
      if options[:class_facet].nil?
        options[:class_facet] = ((options[:classes] || []).length != 1)
      end
    end
    
    def populate
      facet_names.each do |name|
        search_options = facet_search_options.merge(:group_by => name)
        add_from_results name, ThinkingSphinx.search(
          *(args + [search_options])
        )
      end
    end
    
    def facet_search_options
      config = ThinkingSphinx::Configuration.instance
      max    = config.configuration.searchd.max_matches || 1000
      
      options.merge(
        :group_function => :attr,
        :limit          => max,
        :max_matches    => max,
        :page           => 1
      )
    end
    
    def facet_classes
      (
        options[:classes] || ThinkingSphinx.context.indexed_models.collect { |model|
          model.constantize
        }
      ).select { |klass| klass.sphinx_facets.any? }
    end
    
    def all_facets
      facet_classes.collect { |klass|
        klass.sphinx_facets
      }.flatten.select { |facet|
        options[:facets].blank? || Array(options[:facets]).include?(facet.name)
      }
    end
    
    def facet_names_for_all_classes
      all_facets.group_by { |facet|
        facet.name
      }.collect { |name, facets|
        if facets.collect { |facet| facet.type }.uniq.length > 1
          raise "Facet #{name} exists in more than one model with different types"
        end
        facets.first.attribute_name
      }
    end
    
    def facet_names_common_to_all_classes
      facet_names_for_all_classes.select { |name|
        facet_classes.all? { |klass|
          klass.sphinx_facets.detect { |facet|
            facet.attribute_name == name
          }
        }
      }
    end
    
    def add_from_results(facet, results)
      name = ThinkingSphinx::Facet.name_for(facet)
      
      self[name]  ||= {}
      
      return if results.empty?
      
      facet = facet_from_object(results.first, facet) if facet.is_a?(String)
      
      results.each_with_groupby_and_count { |result, group, count|
        facet_value = facet.value(result, group)
        
        self[name][facet_value] ||= 0
        self[name][facet_value]  += count
      }
    end
    
    def underlying_value(key, value)
      case value
      when Array
        value.collect { |item| underlying_value(key, item) }
      when String
        value.to_crc32
      else
        value
      end
    end
    
    def facet_from_object(object, name)
      facet = nil
      klass = object.class
      
      while klass != ::ActiveRecord::Base && facet.nil?
        facet = klass.sphinx_facets.detect { |facet|
          facet.attribute_name == name
        }
        klass = klass.superclass
      end
      
      facet
    end
  end
end