module Neo4j
  module Server
    class CypherNode < Neo4j::Node
      include Neo4j::Server::Resource
      include Neo4j::Core::CypherTranslator
      include Neo4j::Core::ActiveEntity

      def initialize(session, value)
        @session = session

        @neo_id = if value.is_a?(Hash)
                    hash = value['data']
                    @props = Hash[hash.map { |k, v| [k.to_sym, v] }]
                    @labels = value['metadata']['labels'].map!(&:to_sym) if value['metadata']
                    value['id'] # value['self'].match(/\d+$/)[0].to_i
                  else
                    value
                  end
      end

      attr_reader :neo_id

      def inspect
        "CypherNode #{neo_id} (#{object_id})"
      end

      # TODO, needed by neo4j-cypher
      def _java_node
        self
      end

      # (see Neo4j::Node#create_rel)
      def create_rel(type, other_node, props = nil)
        ids_hash = {start_neo_id: neo_id, end_neo_id: other_node.neo_id}
        props_with_ids = props.nil? ? ids_hash : cypher_prop_list(props).merge(ids_hash)
        id = @session._query_or_fail(rel_string(type, other_node, props), true, props_with_ids)
        data_hash = {'type' => type, 'data' => props, 'start' => neo_id, 'end' => other_node.neo_id, 'id' => id}
        CypherRelationship.new(@session, data_hash)
      end

      def rel_string(type, other_node, props)
        "MATCH (a), (b) WHERE ID(a) = {start_neo_id} AND ID(b) = {end_neo_id} CREATE (a)-[r:`#{type}` #{prop_identifier(props)}]->(b) RETURN ID(r)"
      end

      # (see Neo4j::Node#props)
      def props
        if @props
          @props
        else
          hash = @session._query_entity_data("#{match_start} RETURN n", nil, neo_id: neo_id)
          @props = Hash[hash['data'].map { |k, v| [k.to_sym, v] }]
        end
      end

      def refresh
        @props = nil
      end

      # (see Neo4j::Node#remove_property)
      def remove_property(key)
        refresh
        @session._query_or_fail("#{match_start} REMOVE n.`#{key}`", false,  neo_id: neo_id)
      end

      # (see Neo4j::Node#set_property)
      def set_property(key, value)
        refresh
        @session._query_or_fail("#{match_start} SET n.`#{key}` = { value }", false,  value: value, neo_id: neo_id)
        value
      end

      # (see Neo4j::Node#props=)
      def props=(properties)
        refresh
        @session._query_or_fail("#{match_start} SET n = { props }", false,  props: properties, neo_id: neo_id)
        properties
      end

      def remove_properties(properties)
        refresh
        q = "#{match_start} REMOVE " + properties.map do |k|
          "n.`#{k}`"
        end.join(', ')
        @session._query_or_fail(q, false, neo_id: neo_id)
      end

      # (see Neo4j::Node#update_props)
      def update_props(properties)
        refresh
        return if properties.empty?

        removed_keys = properties.keys.select { |k| properties[k].nil? }
        remove_properties(removed_keys) unless removed_keys.empty?
        properties_to_set = properties.keys - removed_keys
        return if properties_to_set.empty?
        props_list = cypher_prop_list(properties)[:props].merge(neo_id: neo_id)
        @session._query_or_fail("#{match_start} SET #{cypher_properties(properties_to_set)}", false, props_list)
        properties
      end

      # (see Neo4j::Node#get_property)
      def get_property(key)
        @props ? @props[key.to_sym] : @session._query_or_fail("#{match_start} RETURN n.`#{key}`", true, neo_id: neo_id)
      end

      # (see Neo4j::Node#labels)
      def labels
        @labels ||= @session._query_or_fail("#{match_start} RETURN labels(n) as labels", true, neo_id: neo_id).map!(&:to_sym)
      end

      def _cypher_label_list(labels_list)
        ':' + labels_list.map { |label| "`#{label}`" }.join(':')
      end

      def add_label(*new_labels)
        @session._query_or_fail("#{match_start} SET n #{_cypher_label_list(new_labels)}", false, neo_id: neo_id)
        new_labels.each { |label| labels << label }
      end

      def remove_label(*target_labels)
        @session._query_or_fail("#{match_start} REMOVE n #{_cypher_label_list(target_labels)}", false, neo_id: neo_id)
        target_labels.each { |label| labels.delete(label) } unless labels.nil?
      end

      def set_label(*label_names)
        q = "#{match_start} #{remove_labels_if_needed} #{set_labels_if_needed(label_names)}"
        @session._query_or_fail(q, false, neo_id: neo_id)
      end

      # (see Neo4j::Node#del)
      def del
        query = match_start_query.optional_match('n-[r]-()').delete(:n, :r)
        @session._query_or_fail(query, false)
      end

      alias_method :delete, :del
      alias_method :destroy, :del

      # (see Neo4j::Node#exist?)
      def exist?
        @session._query("#{match_start} RETURN ID(n)", neo_id: neo_id).data.empty? ? false : true
      end

      # (see Neo4j::Node#node)
      def node(match = {})
        ensure_single_relationship { match(CypherNode, 'p as result LIMIT 2', match) }
      end

      # (see Neo4j::Node#rel)
      def rel(match = {})
        ensure_single_relationship { match(CypherRelationship, 'r as result LIMIT 2', match) }
      end

      # (see Neo4j::Node#rel?)
      def rel?(match = {})
        result = match(CypherRelationship, 'r as result', match)
        !!result.first
      end

      # (see Neo4j::Node#nodes)
      def nodes(match = {})
        match(CypherNode, 'p as result', match)
      end

      # (see Neo4j::Node#rels)
      def rels(match = {dir: :both})
        match(CypherRelationship, 'r as result', match)
      end

      # @private
      def match(clazz, returns, match = {})
        to_dir = {outgoing: ->(rel) { "-#{rel}->" },
                  incoming: ->(rel) { "<-#{rel}-" },
                  both:     ->(rel) { "-#{rel}-" }}

        cypher_rel = match[:type] ? "[r:`#{match[:type]}`]" : '[r]'
        between_id = match[:between] ? "MATCH (p) WHERE ID(p) = #{match[:between].neo_id}" : ''
        dir_func = to_dir[match[:dir] || :both]
        cypher = "#{match_start} #{between_id} MATCH (n)#{dir_func.call(cypher_rel)}(p) RETURN #{returns}"
        r = @session._query(cypher, neo_id: neo_id)
        r.raise_error if r.error?
        _map_result(r)
      end

      def _map_result(r)
        r.to_node_enumeration.map(&:result)
      end

      private

      def cypher_properties(properties_to_set)
        properties_to_set.map! { |k| "n.`#{k}` = {`#{k}`}" }.join(',')
      end

      def remove_labels_if_needed
        if labels.empty?
          ''
        else
          " REMOVE n #{_cypher_label_list(labels)}"
        end
      end

      def set_labels_if_needed(label_names)
        if label_names.empty?
          ''
        else
          " SET n #{_cypher_label_list(label_names.map(&:to_sym).uniq)}"
        end
      end

      def ensure_single_relationship(&block)
        result = yield
        fail "Expected to only find one relationship from node #{neo_id} matching #{match.inspect} but found #{result.count}" if result.count > 1
        result.first
      end

      def match_start(identifier = 'n')
        "MATCH (#{identifier}) WHERE ID(#{identifier}) = {neo_id}"
      end

      def match_start_query(identifier = :n)
        @session.query.match(identifier).where(identifier => {neo_id: neo_id}).with(identifier)
      end
    end
  end
end