module ActiveRecord
  module EavHashes
    # Wraps a bunch of EavEntries and lets you use them like you would a hash
    # This class should not be used directly and you should instead let eav_hash_for create one for you
    class EavHash
      # Creates a new EavHash. You should really let eav_hash_for do this for you...
      # @param [ActiveRecord::Base] owner the Model which will own this hash
      # @param [Hash] options the options hash which eav_hash generated
      def initialize(owner, options)
        Util::sanity_check options
        @owner = owner
        @options = options
      end

      # Saves any modified entries and deletes any which have been nil'd to save DB space
      def save_entries
        # The entries are lazy-loaded, so don't do anything if they haven't been accessed or modified
        return unless (@entries and @changes_made)

        @entries.values.each do |entry|
          if entry.value.nil?
            entry.delete
          else
            set_entry_owner(entry)
            entry.save
          end
        end
      end

      # Gets the value of an EAV attribute
      # @param [String, Symbol] key
      def [](key)
        raise "Key must be a string or a symbol!" unless key.is_a?(String) or key.is_a?(Symbol)
        load_entries_if_needed
        return @entries[key].value if @entries[key]
        nil
      end

      # Sets the value of the EAV attribute `key` to `value`
      # @param [String, Symbol] key the attribute
      # @param [Object] value the value
      def []=(key, value)
        update_or_create_entry key, value
      end

      # I don't know why Ruby hashes don't have a shovel operator, but I will make damn sure that I
      # fight the power and stick it to the man by implementing it.
      # @param [Hash, EavHash] dirt the dirt to shovel (ba dum, tss)
      def <<(dirt)
        if dirt.is_a? Hash
          dirt.each do |key, value|
            update_or_create_entry key, value
          end
        elsif dirt.is_a? EavHash
          dirt.entries.each do |key, entry|
            update_or_create_entry key, entry.value
          end
        else
          raise "You can't shovel something that's not a Hash or EavHash here!"
        end

        self
      end

      # Gets the raw hash containing EavEntries by their keys
      def entries
        load_entries_if_needed
      end

      # Gets the actual values this EavHash contains
      def values
        load_entries_if_needed

        ret = []
        @entries.values.each do |value|
          ret << value
        end

        ret
      end

      # Gets the keys this EavHash manages
      def keys
        load_entries_if_needed
        @entries.keys
      end

      # Emulates Hash.each
      def each (&block)
        as_hash.each &block
      end

      # Emulates Hash.each_pair (same as each)
      def each_pair (&block)
        each &block
      end

      # Empties the hash by setting all the values to nil
      # (without committing them, of course)
      def clear
        load_entries_if_needed
        @entries.each do |_, entry|
          entry.value = nil
        end
      end

      # Returns a hash with each entry key mapped to its actual value,
      # not the internal EavEntry
      def as_hash
        load_entries_if_needed
        hsh = {}
        @entries.each do |k, entry|
          hsh[k] = entry.value
        end

        hsh
      end

      # Take the crap out of #inspect calls
      def inspect
        as_hash
      end

    private
      def update_or_create_entry(key, value)
        raise "Key must be a string or a symbol!" unless key.is_a?(String) or key.is_a?(Symbol)
        load_entries_if_needed

        @changes_made = true
        @owner.updated_at = Time.now

        if @entries[key]
          @entries[key].value = value
        else
          new_entry = @options[:entry_class].new
          set_entry_owner(new_entry)
          new_entry.key = key
          new_entry.value = value

          @entries[key] = new_entry

          value
        end
      end

      # Since entries are lazy-loaded, this is called just before an operation on an entry happens and
      # loads the rows only once per EavHash lifetime.
      def load_entries_if_needed
        if @entries.nil?
          @entries = {}
          rows_from_model = @owner.send("#{@options[:entry_assoc_name]}")
          rows_from_model.each do |row|
            @entries[row.key] = row
          end
        end

        @entries
      end

      # Sets an entry's owner ID. This is called when we save attributes for a model which has just been
      # created and not committed to the DB prior to having its EAV hash(es) modified
      # @param [EavEntry] entry the entry whose owner to change
      def set_entry_owner(entry)
        entry.send "#{@options[:parent_assoc_name]}_id=", @owner.id
      end
    end
  end
end
