# a list of models which called acts_as_solr
$solr_indexed_models = []

module ActsAsSolr #:nodoc:

  module ActsMethods

    # declares a class as solr-searchable
    #
    # ==== options:
    # fields:: This option can be used to specify only the fields you'd
    #          like to index. If not given, all the attributes from the
    #          class will be indexed. You can also use this option to
    #          include methods that should be indexed as fields
    #
    #           class Movie < ActiveRecord::Base
    #             acts_as_solr :fields => [:name, :description, :current_time]
    #             def current_time
    #               Time.now.to_s
    #             end
    #           end
    #
    #          Each field passed can also be a hash with the value being a field type
    #
    #           class Electronic < ActiveRecord::Base
    #             acts_as_solr :fields => [{:price => :range_float}]
    #             def current_time
    #               Time.now
    #             end
    #           end
    #
    #          The field types accepted are:
    #
    #          :float:: Index the field value as a float (ie.: 12.87)
    #          :integer:: Index the field value as an integer (ie.: 31)
    #          :boolean:: Index the field value as a boolean (ie.: true/false)
    #          :date:: Index the field value as a date (ie.: Wed Nov 15 23:13:03 PST 2006)
    #          :string:: Index the field value as a text string, not applying the same indexing
    #                    filters as a regular text field
    #          :range_integer:: Index the field value for integer range queries (ie.:[5 TO 20])
    #          :range_float:: Index the field value for float range queries (ie.:[14.56 TO 19.99])
    #
    #          Setting the field type preserves its original type when indexed
    #
    #          The field may also be passed with a hash value containing options
    #
    #          class Author < ActiveRecord::Base
    #            acts_as_solr :fields => [{:full_name => {:type => :text, :as => :name}}]
    #            def full_name
    #              self.first_name + ' ' + self.last_name
    #            end
    #          end
    #
    #          The options accepted are:
    #
    #          :type:: Index the field using the specified type
    #          :as:: Index the field using the specified field name
    #
    # additional_fields:: This option takes fields to be include in the index
    #                     in addition to those derived from the database. You
    #                     can also use this option to include custom fields
    #                     derived from methods you define. This option will be
    #                     ignored if the :fields option is given. It also accepts
    #                     the same field types as the option above
    #
    #                      class Movie < ActiveRecord::Base
    #                       acts_as_solr :additional_fields => [:current_time]
    #                       def current_time
    #                         Time.now.to_s
    #                       end
    #                      end
    #
    # exclude_fields:: This option taks an array of fields that should be ignored from indexing:
    #
    #                    class User < ActiveRecord::Base
    #                      acts_as_solr :exclude_fields => [:password, :login, :credit_card_number]
    #                    end
    #
    # include:: This option can be used for association indexing, which
    #           means you can include any :has_one, :has_many, :belongs_to
    #           and :has_and_belongs_to_many association to be indexed:
    #
    #            class Category < ActiveRecord::Base
    #              has_many :books
    #              acts_as_solr :include => [:books]
    #            end
    #
    #           Each association may also be specified as a hash with an option hash as a value
    #
    #           class Book < ActiveRecord::Base
    #             belongs_to :author
    #             has_many :distribution_companies
    #             has_many :copyright_dates
    #             has_many :media_types
    #             acts_as_solr(
    #               :fields => [:name, :description],
    #               :include => [
    #                 {:author => {:using => :fullname, :as => :name}},
    #                 {:media_types => {:using => lambda{|media| type_lookup(media.id)}}}
    #                 {:distribution_companies => {:as => :distributor, :multivalued => true}},
    #                 {:copyright_dates => {:as => :copyright, :type => :date}}
    #               ]
    #             ]
    #
    #           The options accepted are:
    #
    #           :type:: Index the associated objects using the specified type
    #           :as:: Index the associated objects using the specified field name
    #           :using:: Index the associated objects using the value returned by the specified method or proc.  If a method
    #                    symbol is supplied, it will be sent to each object to look up the value to index; if a proc is
    #                    supplied, it will be called once for each object with the object as the only argument
    #           :multivalued:: Index the associated objects using one field for each object rather than joining them
    #                          all into a single field
    #
    # facets:: This option can be used to specify the fields you'd like to
    #          index as facet fields
    #
    #           class Electronic < ActiveRecord::Base
    #             acts_as_solr :facets => [:category, :manufacturer]
    #           end
    #
    # boost:: You can pass a boost (float) value that will be used to boost the document and/or a field. To specify a more
    #         boost for the document, you can either pass a block or a symbol. The block will be called with the record
    #         as an argument, a symbol will result in the according method being called:
    #
    #           class Electronic < ActiveRecord::Base
    #             acts_as_solr :fields => [{:price => {:boost => 5.0}}], :boost => 10.0
    #           end
    #
    #           class Electronic < ActiveRecord::Base
    #             acts_as_solr :fields => [{:price => {:boost => 5.0}}], :boost => proc {|record| record.id + 120*37}
    #           end
    #
    #           class Electronic < ActiveRecord::Base
    #             acts_as_solr :fields => [{:price => {:boost => :price_rating}}], :boost => 10.0
    #           end
    #
    # if:: Only indexes the record if the condition evaluated is true. The argument has to be
    #      either a symbol, string (to be eval'ed), proc/method, or class implementing a static
    #      validation method. It behaves the same way as ActiveRecord's :if option.
    #
    #        class Electronic < ActiveRecord::Base
    #          acts_as_solr :if => proc{|record| record.is_active?}
    #        end
    #
    # offline:: Assumes that your using an outside mechanism to explicitly trigger indexing records, e.g. you only
    #           want to update your index through some asynchronous mechanism. Will accept either a boolean or a block
    #           that will be evaluated before actually contacting the index for saving or destroying a document. Defaults
    #           to false. It doesn't refer to the mechanism of an offline index in general, but just to get a centralized point
    #           where you can control indexing. Note: This is only enabled for saving records. acts_as_solr doesn't always like
    #           it, if you have a different number of results coming from the database and the index. This might be rectified in
    #           another patch to support lazy loading.
    #
    #             class Electronic < ActiveRecord::Base
    #               acts_as_solr :offline => proc {|record| record.automatic_indexing_disabled?}
    #             end
    #
    # auto_commit:: The commit command will be sent to Solr only if its value is set to true:
    #
    #                 class Author < ActiveRecord::Base
    #                   acts_as_solr :auto_commit => false
    #                 end
    #
    # dynamic_attributes: Default false. When true, requires a has_many relationship to a DynamicAttribute
    #                     (:name, :value) model. Then, all dynamic attributes will be mapped as normal attributes
    #                    in Solr, so you can filter like this: Model.find_by_solr "#{dynamic_attribute.name}:Lorem"
    #
    # taggable: Default false. When true, indexes tags with field name tag. Tags are taken from taggings.tag
    #
    # spatial: Default false. When true, indexes model.local.latitude and model.local.longitude as coordinates.
    # 
    def acts_as_solr(options={}, solr_options={}, &deferred_solr_configuration)

      $solr_indexed_models << self

      extend ClassMethods
      include InstanceMethods
      include CommonMethods
      include ParserMethods

      define_solr_configuration_methods

      acts_as_taggable_on :tags if options[:taggable]
      has_many :dynamic_attributes, :as => "dynamicable" if options[:dynamic_attributes]
      has_one :local, :as => "localizable" if options[:spatial]

      after_save    :solr_save
      after_destroy :solr_destroy

      if deferred_solr_configuration
        self.deferred_solr_configuration = deferred_solr_configuration
      else
        process_acts_as_solr(options, solr_options)
      end
    end

    def process_acts_as_solr(options, solr_options)
      process_solr_options(options, solr_options)
    end

    def define_solr_configuration_methods
      # I'd like to use cattr_accessor, but it does not support lazy loaders and delegation to the class in the instance methods.
      # TODO: Reconcile with cattr_accessor, or a more appropriate method.
      class_eval(<<-EOS, __FILE__, __LINE__)
      @@configuration = nil unless defined?(@@configuration)
      @@solr_configuration = nil unless defined?(@@solr_configuration)
      @@deferred_solr_configuration = nil unless defined?(@@deferred_solr_configuration)

      def self.configuration
        return @@configuration if @@configuration
        process_deferred_solr_configuration
        @@configuration
      end
      def configuration
        self.class.configuration
      end
      def self.configuration=(value)
        @@configuration = value
      end
      def configuration=(value)
        self.class.configuration = value
      end

      def self.solr_configuration
        return @@solr_configuration if @@solr_configuration
        process_deferred_solr_configuration
        @@solr_configuration
      end
      def solr_configuration
        self.class.solr_configuration
      end
      def self.solr_configuration=(value)
        @@solr_configuration = value
      end
      def solr_configuration=(value)
        self.class.solr_configuration = value
      end

      def self.deferred_solr_configuration
        return @@deferred_solr_configuration if @@deferred_solr_configuration
        @@deferred_solr_configuration
      end
      def deferred_solr_configuration
        self.class.deferred_solr_configuration
      end
      def self.deferred_solr_configuration=(value)
        @@deferred_solr_configuration = value
      end
      def deferred_solr_configuration=(value)
        self.class.deferred_solr_configuration = value
      end
      EOS
    end

    def process_deferred_solr_configuration
      return unless deferred_solr_configuration
      options, solr_options = deferred_solr_configuration.call
      self.deferred_solr_configuration = nil
      self.process_solr_options(options, solr_options)
    end

    def process_solr_options(options={}, solr_options={})
      self.configuration = {
        :fields => nil,
        :format => :objects,
        :additional_fields => nil,
        :dynamic_attributes => false,
        :exclude_fields => [],
        :auto_commit => ['production'].include?(Rails.env) ? false : true,
        :include => nil,
        :facets => nil,
        :boost => nil,
        :if => "true",
        :offline => false,
        :spatial => false,
      }
      self.solr_configuration = {
        :type_field => "type_s",
        :primary_key_field => "pk_s",
        :default_boost => 1.0,
      }

      solr_options ||= {}
      raise "Invalid options: #{(options.keys-configuration.keys).join(',')}" unless (options.keys-configuration.keys).empty?
      raise "Invalid solr options: #{(solr_options.keys-solr_configuration.keys).join(',')}" unless (solr_options.keys-solr_configuration.keys).empty?

      configuration.update(options) if options.is_a?(Hash)
      solr_configuration.update(solr_options) if solr_options.is_a?(Hash)
      Deprecation.validate_index(configuration)

      configuration[:solr_fields] = {}
      configuration[:solr_includes] = {}

      if configuration[:fields].respond_to?(:each)
        process_fields(configuration[:fields])
      else
        process_fields(self.new.attributes.keys.map { |k| k.to_sym })
        process_fields(configuration[:additional_fields])
      end

      process_includes(configuration[:include]) if configuration[:include]
    end

    def after_save_reindex(associations, options = {})
      extend ActsAsSolr::CommonMethods
      Array(associations).each do |association|
        after_save do |ar|
          if options[:with] == :delayed_job
            delay(:run_at => (Date.today+1.day).to_time+4.hours).solr_batch_add_association ar, association
          else
            solr_batch_add_association ar, association
          end
        end
      end
    end

    private

    def get_field_value(field)
      field_name, options = determine_field_name_and_options(field)
      configuration[:solr_fields][field_name] = options

      define_method "#{field_name}_for_solr" do
        value = self.send field_name
        case options[:type]
        when :date # format dates properly; return nil for nil dates
          value ? (value.respond_to?(:utc) ? value.utc : value).strftime("%Y-%m-%dT%H:%M:%SZ") : nil
        else
          value
        end
      end
    end

    def process_fields(raw_field)
      if raw_field.respond_to?(:each)
        raw_field.each do |field|
          next if configuration[:exclude_fields].include?(field)
          get_field_value(field)
        end
      end
    end

    def process_includes(includes)
      Array(includes).each do |assoc|
        field_name, options = determine_field_name_and_options(assoc)
        configuration[:solr_includes][field_name] = options
      end
    end

    def determine_field_name_and_options(field)
      if field.is_a?(Hash)
        name = field.keys.first
        options = field.values.first
        if options.is_a?(Hash)
          [name, {:type => type_for_field(field)}.merge(options)]
        else
          [name, {:type => options}]
        end
      else
        [field, {:type => type_for_field(field)}]
      end
    end

    def type_for_field(field)
      if configuration[:facets] && configuration[:facets].include?(field)
        :facet
      elsif column = columns_hash[field.to_s]
        column_type = format_column_type(column.type)
        
        case column_type
          when :string then :string
          when :datetime then :date
          when :time then :date
          else column_type
        end
      else
        :text
      end
    end
    
    def format_column_type(type)
      if type.class.eql? Symbol
        type
      else
        type.to_s.eql?("ObjectId") ? :string : type.to_s.downcase.to_sym
      end
    end
  end
end

