module Neo4j::Core
  module QueryClauses

    class ArgError < StandardError
      attr_reader :arg_part
      def initialize(arg_part = nil)
        super
        @arg_part = arg_part
      end
    end


    class Clause
      include CypherTranslator

      attr_reader :params

      def initialize(arg, options = {})
        @arg = arg
        @options = options
        @params = {}
      end

      def value
        if @arg.is_a?(String)
          self.from_string @arg

        elsif @arg.is_a?(Symbol) && self.respond_to?(:from_symbol)
          self.from_symbol @arg

        elsif @arg.is_a?(Integer) && self.respond_to?(:from_integer)
          self.from_integer @arg

        elsif @arg.is_a?(Hash)
          if self.respond_to?(:from_hash)
            self.from_hash @arg
          elsif self.respond_to?(:from_key_and_value)
            @arg.map do |key, value|
              self.from_key_and_value key, value
            end
          else
            raise ArgError.new
          end

        else
          raise ArgError.new
        end

      rescue ArgError => arg_error
        message = "Invalid argument for #{self.class.keyword}.  Full arguments: #{@arg.inspect}"
        message += " | Invalid part: #{arg_error.arg_part.inspect}" if arg_error.arg_part

        raise ArgumentError, message
      end

      def from_string(value)
        value
      end

      def node_from_key_and_value(key, value, options = {})
        prefer = options[:prefer] || :var

        var, label_string, attributes_string = nil

        case value
        when String, Symbol
          var = key
          label_string = value
        when Hash
          if !value.values.any? {|v| v.is_a?(Hash) }
            case prefer
            when :var
              var = key
            when :label
              label_string = key
            end
          else
            var = key
          end

          if value.size == 1 && value.values.first.is_a?(Hash)
            label_string, attributes = value.first
            attributes_string = attributes_string(attributes)
          else
            attributes_string = attributes_string(value)
          end
        when Class, Module
          var = key
          label_string = defined?(value::CYPHER_LABEL) ? value::CYPHER_LABEL : value.name
        else
          raise ArgError.new(value)
        end

        "(#{var}#{format_label(label_string)}#{attributes_string})"
      end

      class << self
        attr_reader :keyword

        def from_args(args, options = {})
          args.flatten.map do |arg|
            if !arg.respond_to?(:empty?) || !arg.empty?
              self.new(arg, options)
            end
          end.compact
        end

        def to_cypher(clauses)
          string = clause_string(clauses)

          "#{@keyword} #{string}" if string.to_s.strip.size > 0
        end
      end

      private

      def key_value_string(key, value, previous_keys = [], force_equals = false)
        param = (previous_keys + [key]).join('_').gsub(/[^a-z0-9]+/i, '_').gsub(/^_+|_+$/, '')
        @params[param.to_sym] = value

        if !value.is_a?(Array) || force_equals
          "#{key} = {#{param}}"
        else
          "#{key} IN {#{param}}"
        end
      end

      def format_label(label_string)
        label_string = label_string.to_s.strip
        if !label_string.empty? && label_string[0] != ':'
          label_string = "`#{label_string}`" unless label_string.match(' ')
          label_string = ":#{label_string}"
        end
        label_string
      end

      def attributes_string(attributes)
        attributes_string = attributes.map do |key, value|
          v = if value.nil?
                'null'
              else
                value.to_s.match(/^{.+}$/) ? value : value.inspect
              end
          "#{key}: #{v}"
        end.join(', ')

        " {#{attributes_string}}"
      end
    end

    class StartClause < Clause
      @keyword = 'START'

      def from_symbol(value)
        from_string(value.to_s)
      end

      def from_key_and_value(key, value)
        case value
        when String, Symbol
          "#{key} = #{value}"
        else
          raise ArgError.new(value)
        end
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class WhereClause < Clause
      @keyword = 'WHERE'

      def from_key_and_value(key, value, previous_keys = [])
        case value
        when Hash
          value.map do |k, v|
            if k.to_sym == :neo_id
              "ID(#{key}) = #{v.to_i}"
            else
              key.to_s + '.' + from_key_and_value(k, v, previous_keys + [key])
            end
          end.join(' AND ')
        when NilClass
          "#{key} IS NULL"
        when Regexp
          pattern = (value.casefold? ? "(?i)" : "") + value.source
          "#{key} =~ #{escape_value(pattern.gsub(/\\/, '\\\\\\'))}"
        when Array
          key_value_string(key, value, previous_keys)
        else
          key_value_string(key, value, previous_keys)
        end
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(' AND ')
        end
      end
    end


    class MatchClause < Clause
      @keyword = 'MATCH'

      def from_symbol(value)
        from_string(value.to_s)
      end

      def from_key_and_value(key, value)
        self.node_from_key_and_value(key, value)
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class OptionalMatchClause < MatchClause
      @keyword = 'OPTIONAL MATCH'
    end

    class WithClause < Clause
      @keyword = 'WITH'

      def from_symbol(value)
        from_string(value.to_s)
      end

      def from_key_and_value(key, value)
        "#{value} AS #{key}"
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class UsingClause < Clause
      @keyword = 'USING'

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(" #{@keyword} ")
        end
      end
    end

    class CreateClause < Clause
      @keyword = 'CREATE'

      def from_string(value)
        value
      end

      def from_symbol(value)
        "(:#{value})"
      end

      def from_hash(hash)
        if hash.values.any? {|value| value.is_a?(Hash) }
          hash.map do |key, value|
            from_key_and_value(key, value)
          end
        else
          "(#{attributes_string(hash)})"
        end
      end

      def from_key_and_value(key, value)
        self.node_from_key_and_value(key, value, prefer: :label)
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class CreateUniqueClause < CreateClause
      @keyword = 'CREATE UNIQUE'
    end

    class MergeClause < CreateClause
      @keyword = 'MERGE'
    end

    class DeleteClause < Clause
      @keyword = 'DELETE'

      def from_symbol(value)
        from_string(value.to_s)
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class OrderClause < Clause
      @keyword = 'ORDER BY'

      def from_symbol(value)
        from_string(value.to_s)
      end

      def from_key_and_value(key, value)
        case value
        when String, Symbol
          "#{key}.#{value}"
        when Array
          value.map do |v|
            if v.is_a?(Hash)
              from_key_and_value(key, v)
            else
              "#{key}.#{v}"
            end
          end
        when Hash
          value.map do |k, v|
            "#{key}.#{k} #{v.upcase}"
          end
        end
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class LimitClause < Clause
      @keyword = 'LIMIT'

      def from_string(value)
        value.to_i
      end

      def from_integer(value)
        value
      end

      class << self
        def clause_string(clauses)
          clauses.last.value
        end
      end
    end

    class SkipClause < Clause
      @keyword = 'SKIP'

      def from_string(value)
        value.to_i
      end

      def from_integer(value)
        value
      end

      class << self
        def clause_string(clauses)
          clauses.last.value
        end
      end
    end

    class SetClause < Clause
      @keyword = 'SET'

      def from_key_and_value(key, value)
        case value
        when String, Symbol
          "#{key} = #{value}"
        when Hash
          if @options[:set_props]
            attribute_string = value.map {|k, v| "#{k}: #{v.inspect}" }.join(', ')
            "#{key} = {#{attribute_string}}"
          else
            value.map do |k, v|
              key_value_string("#{key}.`#{k}`", v, ['setter'], true)
            end
          end
        else
          raise ArgError.new(value)
        end
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class OnCreateSetClause < SetClause
      @keyword = 'ON CREATE SET'

      def initialize(*args)
        super
        @options[:set_props] = false
      end
    end

    class OnMatchSetClause < OnCreateSetClause
      @keyword = 'ON MATCH SET'
    end

    class RemoveClause < Clause
      @keyword = 'REMOVE'

      def from_key_and_value(key, value)
        case value
        when /^:/
          "#{key}:#{value[1..-1]}"
        when String
          "#{key}.#{value}"
        when Symbol
          "#{key}:#{value}"
        else
          raise ArgError.new(value)
        end

      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end

    class UnwindClause < Clause
      @keyword = 'UNWIND'

      def from_key_and_value(key, value)
        case value
        when String, Symbol
          "#{value} AS #{key}"
        when Array
          "#{value.inspect} AS #{key}"
        else
          raise ArgError.new(value)
        end
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(' UNWIND ')
        end
      end
    end

    class ReturnClause < Clause
      @keyword = 'RETURN'

      def from_symbol(value)
        from_string(value.to_s)
      end

      def from_key_and_value(key, value)
        case value
        when Array
          value.map do |v|
            from_key_and_value(key, v)
          end.join(', ')
        when String, Symbol
          "#{key}.#{value}"
        else
          raise ArgError.new(value)
        end
      end

      class << self
        def clause_string(clauses)
          clauses.map(&:value).join(', ')
        end
      end
    end


  end
end

