module ReactiveRecord

  class Collection

    def initialize(target_klass, owner = nil, association = nil, *vector)
      @owner = owner  # can be nil if this is an outer most scope
      @association = association
      @target_klass = target_klass
      if owner and !owner.id and vector.length <= 1
        @collection = []
      elsif vector.length > 0
        @vector = vector
      elsif owner
        @vector = owner.backing_record.vector + [association.attribute]
      else
        @vector = [target_klass]
      end
      @scopes = {}
    end

    def dup_for_sync
      self.dup.instance_eval do
        @collection = @collection.dup if @collection
        @scopes = @scopes.dup
        self
      end
    end

    def all
      observed
      @dummy_collection.notify if @dummy_collection
      unless @collection
        @collection = []
        if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
          ids.each do |id|
            @collection << @target_klass.find_by(@target_klass.primary_key => id)
          end
        else
          @dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
          @dummy_record = self[0]
        end
      end
      @collection
    end

    def [](index)
      observed
      if (@collection || all).length <= index and @dummy_collection
        (@collection.length..index).each do |i|
          new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
          new_dummy_record.backing_record.attributes[@association.inverse_of] = @owner if @association and @association.inverse_of
          @collection << new_dummy_record
        end
      end
      @collection[index]
    end

    def ==(other_collection)
      observed
      return !@collection unless other_collection.is_a? Collection
      other_collection.observed
      my_collection = (@collection || []).select { |target| target != @dummy_record }
      other_collection = (other_collection ? (other_collection.collection || []) : []).select { |target| target != other_collection.dummy_record }
      my_collection == other_collection
    end

    def apply_scope(scope, *args)
      # The value returned is another ReactiveRecordCollection with the scope added to the vector
      # no additional action is taken
      scope = [scope, *args] if args.count > 0
      @scopes[scope] ||= Collection.new(@target_klass, @owner, @association, *@vector, [scope])
    end

    def count
      observed
      if @collection
        @collection.count
      elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
        @count
      else
        ReactiveRecord::Base.load_from_db(nil, *@vector, "*count")
        @count = 1
      end
    end

    alias_method :length, :count

    def proxy_association
      @association || self # returning self allows this to work with things like Model.all
    end

    def klass
      @target_klass
    end

    def <<(item)
      return delete(item) if item.destroyed? # pushing a destroyed item is the same as removing it
      backing_record = item.backing_record
      all << item unless all.include? item # does this use == if so we are okay...
      if backing_record and @owner and @association and inverse_of = @association.inverse_of and item.attributes[inverse_of] != @owner
        current_association = item.attributes[inverse_of]
        backing_record.virgin = false unless backing_record.data_loading?
        backing_record.update_attribute(inverse_of, @owner)
        current_association.attributes[@association.attribute].delete(item) if current_association and current_association.attributes[@association.attribute]
        @owner.backing_record.update_attribute(@association.attribute) # forces a check if association contents have changed from synced values
      end
      if item.id and @dummy_record
        @dummy_record.id = item.id
        @collection.delete(@dummy_record)
        @dummy_record = @collection.detect { |r| r.backing_record.vector.last =~ /^\*[0-9]+$/ }
        @dummy_collection = nil
      end
      notify_of_change self
    end

    [:first, :last].each do |method|
      define_method method do |*args|
        if args.count == 0
          all.send(method)
        else
          apply_scope(method, *args)
        end
      end
    end

    def replace(new_array)

      # not tested if you do all[n] where n > 0... this will create additional dummy items, that this will not sync up.
      # probably just moving things around so the @dummy_collection and @dummy_record are updated AFTER the new items are pushed
      # should work.

      if @dummy_collection
        @dummy_collection.notify
        array = new_array.is_a?(Collection) ? new_array.collection : new_array
        @collection.each_with_index do |r, i|
          r.id = new_array[i].id if array[i] and array[i].id and !r.new? and r.backing_record.vector.last =~ /^\*[0-9]+$/
        end
      end

      @collection.dup.each { |item| delete(item) } if @collection  # this line is a big nop I think
      @collection = []
      if new_array.is_a? Collection
        @dummy_collection = new_array.dummy_collection
        @dummy_record = new_array.dummy_record
        new_array.collection.each { |item| self << item } if new_array.collection
      else
        @dummy_collection = @dummy_record = nil
        new_array.each { |item| self << item }
      end
      notify_of_change new_array
    end

    def delete(item)
      notify_of_change(if @owner and @association and inverse_of = @association.inverse_of
        if backing_record = item.backing_record and backing_record.attributes[inverse_of] == @owner
          # the if prevents double update if delete is being called from << (see << above)
          backing_record.update_attribute(inverse_of, nil)
        end
        all.delete(item).tap { @owner.backing_record.update_attribute(@association.attribute) } # forces a check if association contents have changed from synced values
      else
        all.delete(item)
      end)
    end

    def loading?
      all # need to force initialization at this point
      @dummy_collection.loading?
    end

    def empty?  # should be handled by method missing below, but opal-rspec does not deal well with method missing, so to test...
      all.empty?
    end

    def method_missing(method, *args, &block)
      if [].respond_to? method
        all.send(method, *args, &block)
      elsif @target_klass.respond_to?(method) or (args.count == 1 && method =~ /^find_by_/)
        apply_scope(method, *args)
      else
        super
      end
    end

    protected

    def dummy_record
      @dummy_record
    end

    def collection
      @collection
    end

    def dummy_collection
      @dummy_collection
    end

    def notify_of_change(value = nil)
      React::State.set_state(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
      value
    end

    def observed
      React::State.get_state(self, "collection") unless ReactiveRecord::Base.data_loading?
    end

  end

end