module Scrooge
  module Optimizations
    module ResultSets
      class UpdateableResultSet
        
        # Contains a weak referernce to the result set, and can update from DB
        #
        
        attr_accessor :updaters_attributes
        attr_reader :callsite_signature, :scrooge_columns
        
        def initialize(result_set_array, klass, callsite_signature, scrooge_columns)
          if result_set_array
            @result_set_object_id = result_set_array.object_id
            @unique_id = result_set_array.unique_id ||= "#{Time.now.to_f}#{object_id}"  # avoid recycled object ids
          end
          @klass = klass  # expected class of items in the array
          @callsite_signature = callsite_signature
          @scrooge_columns = scrooge_columns
        end
        
        # Called by a ScroogedAttributes hash when it is asked for a column
        # that was not fetched from the DB
        #
        def reload_columns!(columns_to_fetch)
          reloaded_columns = hash_by_primary_key(reload_columns_for_ids(columns_to_fetch, result_set_ids))
          if !reloaded_columns.has_key?(@updaters_attributes[primary_key_name])
            raise ActiveRecord::MissingAttributeError, "scrooge cannot fetch missing attribute(s) #{columns_to_fetch.to_a.join(', ')} because record went away"
          else
            update_with(reloaded_columns)
          end
        end

        def reload_columns_for_ids(columns_to_fetch, result_ids_to_fetch)
          @klass.scrooge_reload(result_ids_to_fetch, columns_to_fetch + [primary_key_name])
        end

        def result_set_attributes
          rs = result_set
          return default_attributes unless rs
          rs.inject(default_attributes) do |memo, r|
            if r.is_a?(@klass)
              memo << r.instance_variable_get(:@attributes)
            end
            memo
          end.uniq
        end
        
        # The result set is weak referenced by its object_id
        # So we check that a unique id matches what we have remembered to make sure
        # that we got the right object (object ids are recycled by ruby)
        #
        def result_set
          return nil unless @result_set_object_id
          result_set = ObjectSpace._id2ref(@result_set_object_id)
          result_set.is_a?(ResultArray) && result_set.unique_id == @unique_id ? result_set : nil
        rescue RangeError
          nil
        end
        
        # When called from after_find and after_initialize, the object
        # being accessed (and causing the reload) is not in the result set yet.
        # We make sure that we add its attributes to result_set_attributes so it
        # gets reloaded too
        #
        def default_attributes
          [@updaters_attributes]
        end
        
        # Ids of the items to be reloaded
        #
        def result_set_ids
          result_set_attributes.inject([]) do |memo, attributes|
            unless attributes.fully_fetched
              memo << attributes[primary_key_name]
            end
            memo
          end
        end
        
        # Update the result set with attributes provided, as returned by
        # the sql server
        #
        def update_with(remaining_attributes)
          current_attributes = hash_by_primary_key(result_set_attributes)
          remaining_attributes.each do |r_id, r_att|
            old_attributes = current_attributes[r_id]
            if old_attributes
              old_attributes.update(r_att)
            end
          end
        end
        
        # Make an efficient data structure to access items when the
        # primary key is known
        #
        def hash_by_primary_key(rows)
          rows.inject({}) {|memo, row| memo[row[primary_key_name]] = row; memo}
        end
        
        def primary_key_name
          @klass.primary_key
        end
        
      end
    end
  end
end