module Neo4j::ActiveRel
  module Query
    extend ActiveSupport::Concern

    module ClassMethods
      # Returns the object with the specified neo4j id.
      # @param [String,Integer] id of node to find
      # @param [Neo4j::Session] session optional
      def find(id, session = self.neo4j_session)
        fail "Unknown argument #{id.class} in find method (expected String or Integer)" if !(id.is_a?(String) || id.is_a?(Integer))
        find_by_id(id, session)
      end

      # Loads the relationship using ID
      def find_by_id(key, session = Neo4j::Session.current!)
        result = session.query.match('(n1)-[r1]->(n2)')
           .where('r1.ID' => key.to_i)
           .limit(1)
           .return(rel_pluck_str)
           .first
        result.r1 if result
      end

      # Performs a very basic match on the relationship.
      # This is not executed lazily, it will immediately return matching objects.
      # To use a string, prefix the property with "r1"
      # @example Match with a string
      #   MyRelClass.where('r1.grade > r1')
      def where(args = {})
        where_query.where(where_string(args)).pluck(rel_pluck_str)
      end

      # Performs a basic match on the relationship, returning all results.
      # This is not executed lazily, it will immediately return matching objects.
      def all
        all_query.pluck(rel_pluck_str)
      end

      def rel_pluck_str
        '{data: r1.data,
          self: "/"+id(r1),
          type: type(r1),
          start: n1.ID, end: n2.ID} as r1'
      end

      def match_rel(q)
        Neo4j::Session.query.match(q).pluck(rel_pluck_str)
      end

      def query_with_edges(q, params, fields)
        s = Neo4j::Session.query.match(q)
        s = s.params(params) if params
        s = s.limit params[:limit] if params[:limit]
        s.pluck("{
          id: r1.ID, category: type(r1), #{fields},
          parent_id: n1.ID, child_id: n2.ID
        } as r1")
      end

      def first
        all_query.limit(1).order('ID(r1)').pluck(rel_pluck_str).first
      end

      def last
        all_query.limit(1).order('ID(r1) DESC').pluck(rel_pluck_str).first
      end

      private

      def where_query
        Neo4j::Session.query.match("#{cypher_string(:outbound)}-[r1:`#{self._type}`]->#{cypher_string(:inbound)}")
      end

      def all_query
        type = self._type == '' ?  '' : ":`#{self._type}`"
        Neo4j::Session.query.match("#{cypher_string}-[r1"+type+"]->#{cypher_string(:inbound)}")
      end

      def cypher_string(dir = :outbound)
        case dir
        when :outbound
          identifier = '(n1'
          identifier + (_from_class == :any ? ')' : cypher_label(:outbound))
        when :inbound
          identifier = '(n2'
          identifier + (_to_class == :any ? ')' : cypher_label(:inbound))
        end
      end

      def cypher_label(dir = :outbound)
        if dir == :outbound and _from_class
          target_class = as_constant(_from_class)
          return ":`#{target_class.mapped_label_name}`)"
        elsif dir == :inbound and _to_class
          target_class = as_constant(_to_class)
          return ":`#{target_class.mapped_label_name}`)"
        end
        ')'
      end

      def as_constant(given_class)
        case given_class
        when String
          given_class.constantize
        when Symbol
          given_class.to_s.constantize
        else
          given_class
        end
      end

      def where_string(args)
        case args
        when Hash
          args.map { |k, v| v.is_a?(Integer) ? "r1.#{k} = #{v}" : "r1.#{k} = '#{v}'" }.join(', ')
        else
          args
        end
      end
    end
  end
end
