module ReactiveRecord
  # creates getters for various method types
  # TODO replace sync_attribute calls with direct logic
  module Getters
    def get_belongs_to(assoc, reload = nil)
      getter_common(assoc.attribute, reload) do |has_key, attr|
        next if new?
        if id.present?
          value = fetch_by_id(attr, @model.primary_key)
          klass = fetch_by_id(attr, 'model_name')
          klass &&= Object.const_get(klass)
        end
        value = find_association(assoc, value, klass)
        sync_ignore_dummy attr, value, has_key
      end&.cast_to_current_sti_type
    end

    def get_has_many(assoc, reload = nil)
      getter_common(assoc.attribute, reload) do |_has_key, attr|
        if new?
          @attributes[attr] = Collection.new(assoc.klass, @ar_instance, assoc)
        else
          sync_attribute attr, Collection.new(assoc.klass, @ar_instance, assoc, *vector, attr)
        end
      end
    end

    def get_attr_value(attr, reload = nil)
      non_relationship_getter_common(attr, reload) do
        sync_attribute attr, convert(attr, model.columns_hash[attr][:default])
      end
    end

    def get_primary_key_value
      non_relationship_getter_common(model.primary_key, false)
    end

    def get_server_method(attr, reload = nil)
      non_relationship_getter_common(attr, reload) do |has_key|
        # SPLAT BUG: was sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
        sync_ignore_dummy attr, Base.load_from_db(self, *(vector || [nil]), *attr), has_key
      end
    end

    def get_ar_aggregate(aggr, reload = nil)
      getter_common(aggr.attribute, reload) do |has_key, attr|
        if new?
          @attributes[attr] = aggr.klass.new.backing_record.link_aggregate(attr, self)
        else
          sync_ignore_dummy attr, new_from_vector(aggr.klass, self, *vector, attr), has_key
        end
      end
    end

    def get_non_ar_aggregate(attr, reload = nil)
      non_relationship_getter_common(attr, reload)
    end

    private

    def virtual_fetch_on_server_warning(attr)
      log(
        "Warning fetching virtual attributes (#{model.name}.#{attr}) during prerendering "\
        'on a changed or new model is not implemented.',
        :warning
      )
    end

    def sync_ignore_dummy(attr, value, has_key)
      # ignore the value if its a Dummy value and there is already a value present
      # this is used to implement reloading.  During the reload while we are waiting we
      # want the current attribute (if its present) to not change.  Once the fetch
      # is complete the fetch process will reload the attribute
      value = @attributes[attr] if has_key && value.is_a?(Base::DummyValue)
      sync_attribute(attr, value)
    end

    def non_relationship_getter_common(attr, reload, &block)
      getter_common(attr, reload) do |has_key|
        if new?
          yield has_key if block
        elsif on_opal_client?
          # SPLAT BUG: was sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
          sync_ignore_dummy attr, Base.load_from_db(self, *(vector || [nil]), *attr), has_key
        elsif id.present?
          sync_attribute attr, fetch_by_id(attr)
        else
          # Not sure how to test this branch, it may never execute this line?
          # If we are on opal_server then we should always be getting an id before getting here
          # but if we do vector might not be set up properly to fetch the attribute
          puts "*** Syncing attribute in getters.rb without an id. This may cause problems. ***"
          puts "*** Report this to hyperstack.org if you see this message: vector =  #{[*vector, attr]}"
          sync_attribute attr, Base.fetch_from_db([*vector, attr])
        end
      end
    end

    def getter_common(attribute, reload)
      @virgin = false unless data_loading?
      return if @destroyed
      if @attributes.key? attribute
        current_value = @attributes[attribute]
        current_value.notify if current_value.is_a? Base::DummyValue
        if reload
          virtual_fetch_on_server_warning(attribute) if on_opal_server? && changed?
          yield true, attribute
        else
          current_value
        end
      else
        virtual_fetch_on_server_warning(attribute) if on_opal_server? && changed?
        yield false, attribute
      end.tap { Hyperstack::Internal::State::Variable.get(self, attribute) unless data_loading? }
    end

    def find_association(association, id, klass)
      instance = if id
        find(klass, klass.primary_key => id)
      elsif association.polymorphic?
        new_from_vector(nil, nil, *vector, association.attribute)
      else
        new_from_vector(association.klass, nil, *vector, association.attribute)
      end
      return instance if instance.is_a? DummyPolymorph
      inverse_of = association.inverse_of(instance)
      instance_backing_record_attributes = instance.attributes
      inverse_association = association.klass.reflect_on_association(inverse_of)
      # HMT-TODO: don't we need to do something with the through association case.
      # perhaps we never hit this point...
      if association.through_association?
        IsomorphicHelpers.log "*********** called #{ar_instance}.find_association(#{association.attribute}) which is has many through!!!!!!!", :error
      end
      if inverse_association.collection?
        instance_backing_record_attributes[inverse_of] = if id and id != ""
          Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
        else
          Collection.new(@model, instance, inverse_association, *vector, association.attribute, inverse_of)
        end unless instance_backing_record_attributes[inverse_of]
        instance_backing_record_attributes[inverse_of].replace [@ar_instance]
      else
        instance_backing_record_attributes[inverse_of] = @ar_instance
      end unless association.through_association? || instance_backing_record_attributes.key?(inverse_of)
      instance
    end

    def link_aggregate(attr, parent)
      self.aggregate_owner = parent
      self.aggregate_attribute = attr
      @ar_instance
    end

    def fetch_by_id(*vector)
      Base.fetch_from_db([@model, *find_by_vector(@model.primary_key => id), *vector])
    end
  end
end
