module Neo4j::Shared
  module Persistence
    extend ActiveSupport::Concern

    USES_CLASSNAME = []

    def update_model
      return if !changed_attributes || changed_attributes.empty?

      changed_props = attributes.select { |k, _| changed_attributes.include?(k) }
      changed_props = self.class.declared_property_manager.convert_properties_to(self, :db, changed_props)
      _persisted_obj.update_props(changed_props)
      changed_attributes.clear
    end

    # Convenience method to set attribute and #save at the same time
    # @param [Symbol, String] attribute of the attribute to update
    # @param [Object] value to set
    def update_attribute(attribute, value)
      send("#{attribute}=", value)
      self.save
    end

    # Convenience method to set attribute and #save! at the same time
    # @param [Symbol, String] attribute of the attribute to update
    # @param [Object] value to set
    def update_attribute!(attribute, value)
      send("#{attribute}=", value)
      self.save!
    end

    def create_or_update
      # since the same model can be created or updated twice from a relationship we have to have this guard
      @_create_or_updating = true
      apply_default_values
      result = _persisted_obj ? update_model : create_model
      if result == false
        Neo4j::Transaction.current.failure if Neo4j::Transaction.current
        false
      else
        true
      end
    rescue => e
      Neo4j::Transaction.current.failure if Neo4j::Transaction.current
      raise e
    ensure
      @_create_or_updating = nil
    end

    def apply_default_values
      return if self.class.declared_property_defaults.empty?
      self.class.declared_property_defaults.each_pair do |key, value|
        self.send("#{key}=", value) if self.send(key).nil?
      end
    end

    # Returns +true+ if the record is persisted, i.e. it's not a new record and it was not destroyed
    def persisted?
      !new_record? && !destroyed?
    end

    # Returns +true+ if the record hasn't been saved to Neo4j yet.
    def new_record?
      !_persisted_obj
    end

    alias_method :new?, :new_record?

    def destroy
      freeze
      _persisted_obj && _persisted_obj.del
      @_deleted = true
    end

    def exist?
      _persisted_obj && _persisted_obj.exist?
    end

    # Returns +true+ if the object was destroyed.
    def destroyed?
      @_deleted || _destroyed_double_check?
    end

    # These two methods should be removed in 6.0.0
    def _destroyed_double_check?
      if _active_record_destroyed_behavior?
        false
      else
        (!new_record? && !exist?)
      end
    end

    def _active_record_destroyed_behavior?
      fail 'Remove this workaround in 6.0.0' if Neo4j::VERSION >= '6.0.0'

      !!Neo4j::Config[:_active_record_destroyed_behavior]
    end
    # End of two methods which should be removed in 6.0.0


    # @return [Hash] all defined and none nil properties
    def props
      attributes.reject { |_, v| v.nil? }.symbolize_keys
    end

    # @return true if the attributes hash has been frozen
    def frozen?
      @attributes.frozen?
    end

    def freeze
      @attributes.freeze
      self
    end

    def reload
      return self if new_record?
      association_proxy_cache.clear
      changed_attributes && changed_attributes.clear
      unless reload_from_database
        @_deleted = true
        freeze
      end
      self
    end

    def reload_from_database
      # TODO: - Neo4j::IdentityMap.remove_node_by_id(neo_id)
      if reloaded = self.class.load_entity(neo_id)
        send(:attributes=, reloaded.attributes)
      end
      reloaded
    end

    # Updates this resource with all the attributes from the passed-in Hash and requests that the record be saved.
    # If saving fails because the resource is invalid then false will be returned.
    def update(attributes)
      self.attributes = process_attributes(attributes)
      save
    end
    alias_method :update_attributes, :update

    # Same as {#update_attributes}, but raises an exception if saving fails.
    def update!(attributes)
      self.attributes = process_attributes(attributes)
      save!
    end
    alias_method :update_attributes!, :update!

    def cache_key
      if self.new_record?
        "#{model_cache_key}/new"
      elsif self.respond_to?(:updated_at) && !self.updated_at.blank?
        "#{model_cache_key}/#{neo_id}-#{self.updated_at.utc.to_s(:number)}"
      else
        "#{model_cache_key}/#{neo_id}"
      end
    end

    private

    def model_cache_key
      self.class.model_name.cache_key
    end

    def create_magic_properties
    end

    def update_magic_properties
      self.updated_at = DateTime.now if respond_to?(:updated_at=) && changed? && !updated_at_changed?
    end

    # Inserts the _classname property into an object's properties during object creation.
    def set_classname(props, check_version = true)
      props[:_classname] = self.class.name if self.class.cached_class?(check_version)
    end

    def set_timestamps
      now = DateTime.now
      self.created_at ||= now if respond_to?(:created_at=)
      self.updated_at ||= now if respond_to?(:updated_at=)
    end

    module ClassMethods
      # Determines whether a model should insert a _classname property. This can be used to override the automatic matching of returned
      # objects to models.
      def cached_class?(check_version = true)
        uses_classname? || (!!Neo4j::Config[:cache_class_names] && (check_version ? neo4j_session.version < '2.1.5' : true))
      end

      # @return [Boolean] status of whether this model will add a _classname property
      def uses_classname?
        Neo4j::Shared::Persistence::USES_CLASSNAME.include?(self.name)
      end

      # Adds this model to the USES_CLASSNAME array. When new rels/nodes are created, a _classname property will be added. This will override the
      # automatic matching of label/rel type to model.
      #
      # You'd want to do this if you have multiple models for the same label or relationship type. When it comes to labels, there isn't really any
      # reason to do this because you can have multiple labels; on the other hand, an argument can be made for doing this with relationships since
      # rel type is a bit more restrictive.
      #
      # It could also be speculated that there's a slight performance boost to using _classname since the gem immediately knows what model is responsible
      # for a returned object. At the same time, it is a bit restrictive and changing it can be a bit of a PITA. Use carefully!
      def set_classname
        Neo4j::Shared::Persistence::USES_CLASSNAME << self.name
      end

      # Removes this model from the USES_CLASSNAME array. When new rels/nodes are create, no _classname property will be injected. Upon returning of
      # the object from the database, it will be matched to a model using its relationship type or labels.
      def unset_classname
        Neo4j::Shared::Persistence::USES_CLASSNAME.delete self.name
      end
    end
  end
end