module ActiveGraph::Shared
  # Acts as a bridge between the node and rel models and ActiveGraph::Core::Query.
  # If the object is persisted, it returns a query matching; otherwise, it returns a query creating it.
  # This class does not execute queries, so it keeps no record of what identifiers have been set or what has happened in previous factories.
  class QueryFactory
    attr_reader :graph_object, :identifier

    def initialize(graph_object, identifier)
      @graph_object = graph_object
      @identifier = identifier.to_sym
    end

    def self.create(graph_object, identifier)
      factory_for(graph_object).new(graph_object, identifier)
    end

    def self.factory_for(graph_obj)
      case
      when graph_obj.respond_to?(:labels_for_create)
        NodeQueryFactory
      when graph_obj.respond_to?(:rel_type)
        RelQueryFactory
      else
        fail "Unable to find factory for #{graph_obj}"
      end
    end

    def query
      graph_object.persisted? ? match_query : create_query
    end

    # @param [ActiveGraph::Core::Query] query An instance of ActiveGraph::Core::Query upon which methods will be chained.
    def base_query=(query)
      return if query.blank?
      @base_query = query
    end

    def base_query
      @base_query || ActiveGraph::Base.new_query
    end

    protected

    def create_query
      fail 'Abstract class, not implemented'
    end

    def match_query
      base_query
        .match(match_string).where("ID(#{identifier}) = $#{identifier_id}")
        .params(identifier_id.to_sym => graph_object.neo_id)
    end

    def identifier_id
      @identifier_id ||= "#{identifier}_id"
    end

    def identifier_params
      @identifier_params ||= "#{identifier}_params"
    end
  end

  class NodeQueryFactory < QueryFactory
    protected

    def match_string
      "(#{identifier})"
    end

    def create_query
      return match_query if graph_object.persisted?
      labels = graph_object.labels_for_create.map { |l| ":`#{l}`" }.join
      base_query.create("(#{identifier}#{labels} $#{identifier}_params)").params(identifier_params => graph_object.props_for_create)
    end
  end

  class RelQueryFactory < QueryFactory
    protected

    def match_string
      "(#{graph_object.from_node_identifier})-[#{identifier}]->()"
    end

    def create_query
      return match_query if graph_object.persisted?
      create_props, set_props = filtered_props
      base_query.send(graph_object.create_method, query_string(create_props)).break
        .set(identifier => set_props)
        .params(params(create_props))
    end

    private

    def filtered_props
      ActiveGraph::Shared::FilteredHash.new(graph_object.props_for_create, graph_object.creates_unique_option).filtered_base
    end

    def query_string(create_props)
      "(#{graph_object.from_node_identifier})-[#{identifier}:`#{graph_object.type}` #{pattern(create_props)}]->(#{graph_object.to_node_identifier})"
    end

    def params(create_props)
      unique? ? create_props.transform_keys { |key| scoped(key).to_sym } : { namespace.to_sym => create_props }
    end

    def unique?
      graph_object.create_method == :create_unique
    end

    def pattern(create_props)
      unique? ? "{#{create_props.keys.map { |key| "#{key}: $#{scoped(key)}" }.join(', ')}}" : "$#{namespace}"
    end

    def scoped(key)
      "#{namespace}_#{key}"
    end

    def namespace
      "#{identifier}_create_props"
    end
  end
end
