module Sunspot
  # 
  # This module contains singleton objects that represent the types that can be
  # indexed and searched using Sunspot. Plugin developers should be able to
  # add new constants to the Type module; as long as they implement the
  # appropriate methods, Sunspot should be able to integrate them (note that
  # this capability is untested at the moment). The required methods are:
  #
  # +indexed_name+::
  #   Convert a given field name into its form as stored in Solr. This
  #   generally means adding a suffix to match a Solr dynamicField definition.
  # +to_indexed+::
  #   Convert a value of this type into the appropriate Solr string
  #   representation.
  # +cast+::
  #   Convert a Solr string representation of a value into the appropriate
  #   Ruby type.
  #
  module Type
    class AbstractType #:nodoc:
      class <<self
        def instance
          @instance ||= new
        end
        private :new
      end

      def accepts_dynamic?
        true
      end
    end

    # 
    # Text is a special type that stores data for fulltext search. Unlike other
    # types, Text fields are tokenized and are made available to the keyword
    # search phrase. Text fields cannot be faceted, ordered upon, or used in
    # restrictions. Similarly, text fields are the only fields that are made
    # available to keyword search.
    #
    class TextType < AbstractType
      def indexed_name(name) #:nodoc:
        "#{name}_text"
      end

      def to_indexed(value) #:nodoc:
        value.to_s if value
      end

      def cast(text)
        text
      end

      def accepts_dynamic?
        false
      end
    end

    # 
    # The String type represents string data.
    #
    class StringType < AbstractType
      def indexed_name(name) #:nodoc:
        "#{name}_s"
      end

      def to_indexed(value) #:nodoc:
        value.to_s if value
      end

      def cast(string) #:nodoc:
        string
      end
    end

    # 
    # The Integer type represents integers.
    #
    class IntegerType < AbstractType
      def indexed_name(name) #:nodoc:
        "#{name}_i"
      end

      def to_indexed(value) #:nodoc:
        value.to_i.to_s if value
      end

      def cast(string) #:nodoc:
        string.to_i
      end
    end

    # 
    # The Long type indexes Ruby Fixnum and Bignum numbers into Java Longs
    #
    class LongType < IntegerType
      def indexed_name(name) #:nodoc:
        "#{name}_l"
      end
    end

    # 
    # The Float type represents floating-point numbers.
    #
    class FloatType < AbstractType
      def indexed_name(name) #:nodoc:
        "#{name}_f"
      end

      def to_indexed(value) #:nodoc:
        value.to_f.to_s if value
      end

      def cast(string) #:nodoc:
        string.to_f
      end
    end

    # 
    # The Double type indexes Ruby Floats (which are in fact doubles) into Java
    # Double fields
    #
    class DoubleType < FloatType
      def indexed_name(name)
        "#{name}_e"
      end
    end

    # 
    # The time type represents times. Note that times are always converted to
    # UTC before indexing, and facets of Time fields always return times in UTC.
    #
    class TimeType < AbstractType
      XMLSCHEMA = "%Y-%m-%dT%H:%M:%SZ"

      def indexed_name(name) #:nodoc:
        "#{name}_d"
      end

      def to_indexed(value) #:nodoc:
        if value
          value_to_utc_time(value).strftime(XMLSCHEMA)
        end
      end

      def cast(string) #:nodoc:
        begin
          Time.xmlschema(string)
        rescue ArgumentError
          DateTime.strptime(string, XMLSCHEMA)
        end
      end

      private

      def value_to_utc_time(value)
        if value.respond_to?(:utc)
          value.utc
        elsif value.respond_to?(:new_offset)
          value.new_offset
        else
          begin
            Time.parse(value.to_s).utc
          rescue ArgumentError
            DateTime.parse(value.to_s).new_offset
          end
        end
      end
    end

    # 
    # The DateType encapsulates dates (without time information). Internally,
    # Solr does not have a date-only type, so this type indexes data using
    # Solr's DateField type (which is actually date/time), midnight UTC of the
    # indexed date.
    #
    class DateType < TimeType
      def to_indexed(value) #:nodoc:
        if value
          time = 
            if %w(year mon mday).all? { |method| value.respond_to?(method) }
              Time.utc(value.year, value.mon, value.mday)
            else
              date = Date.parse(value.to_s)
              Time.utc(date.year, date.mon, date.mday)
            end
          super(time)
        end
      end

      def cast(string) #:nodoc:
        time = super
        Date.civil(time.year, time.mon, time.mday)
      end
    end

    # 
    # Store integers in a TrieField, which makes range queries much faster.
    #
    class TrieIntegerType < IntegerType
      def indexed_name(name)
        "#{super}t"
      end
    end

    # 
    # Store floats in a TrieField, which makes range queries much faster.
    #
    class TrieFloatType < FloatType
      def indexed_name(name)
        "#{super}t"
      end
    end

    # 
    # Index times using a TrieField. Internally, trie times are indexed as
    # Unix timestamps in a trie integer field, as TrieField does not support
    # datetime types natively. This distinction should have no effect from the
    # standpoint of the library's API.
    #
    class TrieTimeType < TimeType
      def indexed_name(name)
        "#{super}t"
      end
    end


    # 
    # The boolean type represents true/false values. Note that +nil+ will not be
    # indexed at all; only +false+ will be indexed with a false value.
    #
    class BooleanType < AbstractType
      def indexed_name(name) #:nodoc:
        "#{name}_b"
      end

      def to_indexed(value) #:nodoc:
        unless value.nil?
          value ? 'true' : 'false'
        end
      end

      def cast(string) #:nodoc:
        case string
        when 'true'
          true
        when 'false'
          false
        end
      end
    end

    class ClassType < AbstractType
      def indexed_name(name) #:nodoc:
        'class_name'
      end

      def to_indexed(value) #:nodoc:
        value.name
      end

      def cast(string) #:nodoc:
        Sunspot::Util.full_const_get(string)
      end
    end
  end
end