module ActiveRecord
  module EavHashes
    # Used instead of nil when a nil value is assigned
    # (otherwise, the value will try to deserialize itself and
    #  that would break everything horrifically)
    class NilPlaceholder; end

    # Represent an EAV row. This class should NOT be used directly, instead it should be inherited from
    # by the class generated by eav_hash_for.
    class EavEntry < ActiveRecord::Base
      # prevent activerecord from thinking we're trying to do STI
      self.abstract_class = true

      # Tell ActiveRecord to convert the value to its DB storable format
      before_save :serialize_value

      # Let the key be assignable only once on creation
      attr_readonly :entry_key

      # Contains the values the value_type column should have based on the type of the value being stored
      SUPPORTED_TYPES = {
          :String     => 0,
          :Symbol     => 1,
          :Integer    => 2,
          :Fixnum     => 2,
          :Bignum     => 2,
          :Float      => 3,
          :Complex    => 4,
          :Rational   => 5,
          :Boolean    => 6, # For code readability
          :TrueClass  => 6,
          :FalseClass => 6,
          :Object     => 7 # anything else (including Hashes, Arrays) will be serialized to yaml and saved as Object
      }

      # Does some sanity checks.
      def after_initialize
        raise "key should be a string or symbol!" unless key.is_a? String or key.is_a? Symbol
        raise "value should not be empty!" if @value.is_a? String and value.empty?
        raise "value should not be nil!" if @value.nil?
      end

      # Gets the EAV row's value
      def value
        return nil if @value.is_a? NilPlaceholder
        @value.nil? ? deserialize_value : @value
      end

      # Sets the EAV row's value
      # @param [Object] val the value
      def value= (val)
        @value = (val.nil? ? NilPlaceholder.new : val)
      end


      def key
        k = read_attribute :entry_key
        (read_attribute :symbol_key) ? k.to_sym : k
      end

      # Raises an error if you try changing the key (unless no key is set)
      def key= (val)
        raise "Keys are immutable!" if read_attribute(:entry_key)
        raise "Key must be a string!" unless val.is_a?(String) or val.is_a?(Symbol)
        write_attribute :entry_key, val.to_s
        write_attribute :symbol_key, (val.is_a? Symbol)
      end

      # Gets the value_type column's value for the type of value passed
      # @param [Object] val the object whose value_type to determine
      def self.get_value_type (val)
        return nil if val.nil?
        ret = SUPPORTED_TYPES[val.class.name.to_sym]
        if ret.nil?
          ret = SUPPORTED_TYPES[:Object]
        end
        ret
      end

    private
      # Sets the value_type column to the appropriate value based on the value's type
      def update_value_type
        write_attribute :value_type, EavEntry.get_value_type(@value)
      end

      # Converts the value to its database-storable form and tells ActiveRecord that it's been changed (if it has)
      def serialize_value
        # Returning nil will prevent the row from being saved, to save some time since the EavHash that manages this
        # entry will have marked it for deletion.
        raise "Tried to save with a nil value!" if @value.nil? or @value.is_a? NilPlaceholder

        update_value_type
        if value_type == SUPPORTED_TYPES[:Object]
          write_attribute :value, YAML::dump(@value)
        else
          write_attribute :value, @value.to_s
        end

        read_attribute :value
      end

      # Converts the value from it's database representation to the type specified in the value_type column.
      def deserialize_value
        if @value.nil?
          @value = read_attribute :value
        end

        case value_type
          when SUPPORTED_TYPES[:Object] # or Hash, Array, etc.
            @value = YAML::load @value
          when SUPPORTED_TYPES[:Symbol]
            @value = @value.to_sym
          when SUPPORTED_TYPES[:Integer] # or Fixnum, Bignum
            @value = @value.to_i
          when SUPPORTED_TYPES[:Float]
            @value = @value.to_f
          when SUPPORTED_TYPES[:Complex]
            @value = Complex @value
          when SUPPORTED_TYPES[:Rational]
            @value = Rational @value
          when SUPPORTED_TYPES[:Boolean]
            @value = (@value == "true")
          else
            @value
        end
      end
    end
  end
end