module ReactiveRecord

  class Collection

    class DummySet
      def new
        @master ||= super
      end
      def method_missing(*args)
      end
    end

    def unsaved_children
      old_uc_already_being_called = @uc_already_being_called
      if @owner && @association
        @unsaved_children ||= Set.new
        unless @uc_already_being_called
          @uc_already_being_called = true
          #@owner.backing_record.update_attribute(@association.attribute)
        end
      else
        @unsaved_children ||= DummySet.new
      end
      @unsaved_children
    ensure
      @uc_already_being_called = old_uc_already_being_called
    end

    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 && !@association.through_association?
          @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_children = (@collection || []).select { |target| target != @dummy_record }
      if other_collection
        other_children = (other_collection.collection || []).select { |target| target != other_collection.dummy_record }
        return false unless my_children == other_children
        unsaved_children.to_a == other_collection.unsaved_children.to_a
      else
        my_children.empty? && unsaved_children.empty?
      end
    end
    # todo move following to a separate module related to scope updates ******************
    attr_reader   :vector
    attr_writer   :scope_description
    attr_writer   :parent
    attr_reader   :pre_sync_related_records

    def to_s
      "<Coll-#{object_id} - #{vector}>"
    end

    class << self
      def sync_scopes(broadcast)
        # record_with_current_values will return nil if data between
        # the broadcast record and the value on the client is out of sync
        # not running set_pre_sync_related_records will cause sync scopes
        # to refresh all related scopes
        React::State.bulk_update do
          record = broadcast.record_with_current_values
          apply_to_all_collections(
            :set_pre_sync_related_records,
            record, broadcast.new?
          ) if record
          record = broadcast.record_with_new_values
          apply_to_all_collections(
            :sync_scopes,
            record, record.destroyed?
          )
          record.backing_record.sync_unscoped_collection! if record.destroyed? || broadcast.new?
        end
      end

      def apply_to_all_collections(method, record, dont_gather)
        related_records = Set.new if dont_gather
        Base.outer_scopes.each do |collection|
          unless dont_gather
            related_records = collection.gather_related_records(record)
          end
          collection.send method, related_records, record
        end
      end
    end

    def gather_related_records(record, related_records = Set.new)
      merge_related_records(record, related_records)
      live_scopes.each do |collection|
        collection.gather_related_records(record, related_records)
      end
      related_records
    end

    def merge_related_records(record, related_records)
      if filter? && joins_with?(record)
        related_records.merge(related_records_for(record))
      end
      related_records
    end

    def filter?
      true
    end

    # is it necessary to check @association in the next 2 methods???

    def joins_with?(record)
      if @association && @association.through_association
        @association.through_association.klass == record.class
      else
        @target_klass == record.class
      end
    end

    def related_records_for(record)
      return [] unless @association
      attrs = record.backing_record.attributes
      return [] unless attrs[@association.inverse_of] == @owner
      if !@association.through_association
        [record]
      elsif (source = attrs[@association.source])
        [source]
      else
        []
      end
    end

    def collector?
      false
    end

    def filter_records(related_records)
      # possibly we should never get here???
      scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
      @scope_description.filter_records(related_records, scope_args)
    end

    def live_scopes
      @live_scopes ||= Set.new
    end

    def set_pre_sync_related_records(related_records, _record = nil)
      #related_records = related_records.intersection([*@collection]) <- deleting this works
      @pre_sync_related_records = related_records #in_this_collection related_records <- not sure if this works
      live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
    end

    def sync_scopes(related_records, record, filtering = true)
      #related_records = related_records.intersection([*@collection])
      #related_records = in_this_collection related_records
      live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
      notify_of_change unless related_records.empty?
    ensure
      @pre_sync_related_records = nil
    end

    def apply_scope(name, *vector)
      description = ScopeDescription.find(@target_klass, name)
      collection = build_child_scope(description, *description.name, *vector)
      collection.reload_from_db if name == "#{description.name}!"
      collection
    end

    def child_scopes
      @child_scopes ||= {}
    end

    def build_child_scope(scope_description, *scope_vector)
      child_scopes[scope_vector] ||= begin
        new_vector = @vector
        new_vector += [scope_vector] unless new_vector.nil? || scope_vector.empty?
        child_scope = Collection.new(@target_klass, nil, nil, *new_vector)
        child_scope.scope_description = scope_description
        child_scope.parent = self
        child_scope.extend ScopedCollection
        child_scope
      end
    end

    def link_to_parent
      return if @linked
      @linked = true
      if @parent
        @parent.link_child self
        sync_collection_with_parent unless collection
      else
        ReactiveRecord::Base.add_to_outer_scopes self
      end
      all if collector? # force fetch all so the collector can do its job
    end

    def link_child(child)
      live_scopes << child
      link_to_parent
    end

    def sync_collection_with_parent
      if @parent.collection
        if @parent.collection.empty?
          @collection = []
        elsif filter?
          @collection = filter_records(@parent.collection)
        end
      elsif @parent._count_internal(false).zero?  # just changed this from count.zero?
        @count = 0
      end
    end

    # end of stuff to move

    def reload_from_db(force = nil)
      if force || React::State.has_observers?(self, :collection)
        @out_of_date = false
        ReactiveRecord::Base.load_from_db(nil, *@vector, '*all') if @collection
        ReactiveRecord::Base.load_from_db(nil, *@vector, '*count')
      else
        @out_of_date = true
      end
      self
    end

    def observed
      return if @observing || ReactiveRecord::Base.data_loading?
      begin
        @observing = true
        link_to_parent
        reload_from_db(true) if @out_of_date
        React::State.get_state(self, :collection)
      ensure
        @observing = false
      end
    end

    def set_count_state(val)
      unless ReactiveRecord::WhileLoading.has_observers?
        React::State.set_state(self, :collection, collection, true)
      end
      @count = val
    end



    def _count_internal(load_from_client)
      # when count is called on a leaf, count_internal is called for each
      # ancestor.  Only the outermost count has load_from_client == true
      observed
      if @collection
        @collection.count
      elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
        @count
      else
        ReactiveRecord::Base.load_from_db(nil, *@vector, "*count") if load_from_client
        @count = 1
      end
    end

    def count
      _count_internal(true)
    end

    alias_method :length, :count

    # WHY IS THIS NEEDED?  Perhaps it was just for debug
    def collect(*args, &block)
      all.collect(*args, &block)
    end

    # def each_known_child
    #   [*collection, *client_pushes].each { |i| yield i }
    # end

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

    def klass
      @target_klass
    end

    def push_and_update_belongs_to(id)
      # example collection vector: TestModel.find(1).child_models.harrybarry
      # harrybarry << child means that
      # child.test_model = 1
      # so... we go back starting at this collection and look for the first
      # collection with an owner... that is our guy
      child = proxy_association.klass.find(id)
      push child
      set_belongs_to child
    end

    def set_belongs_to(child)
      if @owner
        child.send("#{@association.inverse_of}=", @owner) if @association
      elsif @parent
        @parent.set_belongs_to(child)
      end
      child
    end

    attr_reader :client_collection

    # appointment.doctor = doctor_value (i.e. through association is changing)
    # means appointment.doctor_value.patients << appointment.patient
    # and we have to appointment.doctor(current value).patients.delete(appointment.patient)


    def update_child(item)
      backing_record = item.backing_record
      if backing_record && @owner && @association && !@association.through_association? && item.attributes[@association.inverse_of] != @owner
        inverse_of = @association.inverse_of
        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
    end

    def push(item)
      item.itself # force get of at least the id
      if collection
        self.force_push item
      else
        unsaved_children << item
        update_child(item)
        @owner.backing_record.update_attribute(@association.attribute) if @owner && @association
        if !@count.nil?
          @count += item.destroyed? ? -1 : 1
          notify_of_change self
        end
      end
      self
    end

    alias << push

    def sort!(*args, &block)
      replace(sort(*args, &block))
    end

    def force_push(item)
      return delete(item) if item.destroyed? # pushing a destroyed item is the same as removing it
      all << item unless all.include? item # does this use == if so we are okay...
      update_child(item)
      if item.id and @dummy_record
        @dummy_record.id = item.id
        # we cant use == because that just means the objects are referencing
        # the same backing record.
        @collection.reject { |i| i.object_id == @dummy_record.object_id }
        @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)
      unsaved_children.clear
      new_array = new_array.to_a
      return self if new_array == @collection
      Base.load_data { internal_replace(new_array) }
      notify_of_change new_array
    end

    def internal_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)
      unsaved_children.delete(item)
      notify_of_change(
        if @owner && @association && !@association.through_association?
          inverse_of = @association.inverse_of
          if (backing_record = item.backing_record) && 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
          # forces a check if association contents have changed from synced values
          delete_internal(item) { @owner.backing_record.update_attribute(@association.attribute) }
        else
          delete_internal(item)
        end
      )
    end

    def delete_internal(item)
      if collection
        all.delete(item)
      elsif !@count.nil?
        @count -= 1
      end
      yield item if block_given?
      item
    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 ScopeDescription.find(@target_klass, method)
        apply_scope(method, *args)
      elsif args.count == 1 && method.start_with?('find_by_')
        apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first)
      elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}")
        apply_scope("_#{method}", *args).first
      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

  end

end