module ActiveGraph
  module Node
    module HasN
      module AssociationCypherMethods
        # Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition)
        def arrow_cypher(var = nil, properties = {}, create = false, reverse = false, length = nil)
          validate_origin!

          if create && length.present?
            fail(ArgumentError, 'rel_length option cannot be specified when creating a relationship')
          end

          direction_cypher(get_relationship_cypher(var, properties, create, length), create, reverse)
        end

        private

        def direction_cypher(relationship_cypher, create, reverse = false)
          case get_direction(create, reverse)
          when :out
            "-#{relationship_cypher}->"
          when :in
            "<-#{relationship_cypher}-"
          when :both
            "-#{relationship_cypher}-"
          end
        end

        def get_relationship_cypher(var, properties, create, length)
          relationship_type = relationship_type(create)
          relationship_name_cypher = ":`#{relationship_type}`" if relationship_type
          rel_length_cypher = cypher_for_rel_length(length)
          properties_string = get_properties_string(properties)

          "[#{var}#{relationship_name_cypher}#{rel_length_cypher}#{properties_string}]"
        end

        def get_properties_string(properties)
          p = properties.map do |key, value|
            "#{key}: #{value.inspect}"
          end.join(', ')
          p.empty? ? '' : " {#{p}}"
        end

        VALID_REL_LENGTH_SYMBOLS = {
          any: '*'
        }

        def cypher_for_rel_length(length)
          return nil if length.blank?

          validate_rel_length!(length)

          case length
          when Symbol then VALID_REL_LENGTH_SYMBOLS[length]
          when Integer then "*#{length}"
          when Range then cypher_for_range_rel_length(length)
          when Hash then cypher_for_hash_rel_length(length)
          end
        end

        def cypher_for_range_rel_length(length)
          range_end = length.end
          range_end = nil if range_end == Float::INFINITY
          "*#{length.begin}..#{range_end}"
        end

        def cypher_for_hash_rel_length(length)
          range_end = length[:max]
          range_end = nil if range_end == Float::INFINITY
          "*#{length[:min]}..#{range_end}"
        end

        def validate_rel_length!(length)
          message = rel_length_error_message(length)
          fail(ArgumentError, "Invalid value for rel_length (#{length.inspect}): #{message}") if message
          true
        end

        def rel_length_error_message(length)
          case length
          when Integer then 'cannot be negative' if length < 0
          when Symbol then rel_length_symbol_error_message(length)
          when Range then rel_length_range_error_message(length)
          when Hash then rel_length_hash_error_message(length)
          else 'should be a Symbol, Integer, Range or Hash'
          end
        end

        def rel_length_symbol_error_message(length)
          "expecting one of #{VALID_REL_LENGTH_SYMBOLS.keys.inspect}" if !VALID_REL_LENGTH_SYMBOLS.key?(length)
        end

        def rel_length_range_error_message(length)
          if length.begin > length.end
            'cannot be a decreasing Range'
          elsif length.begin < 0
            'cannot include negative values'
          end
        end

        def rel_length_hash_error_message(length)
          'Hash keys should be a subset of [:min, :max]' if (length.keys & [:min, :max]) != length.keys
        end
      end
    end
  end
end
