module Neo4j
  module 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
          [String, Symbol, Integer, Hash].each do |arg_class|
            from_method = "from_#{arg_class.name.downcase}"
            return send(from_method, @arg) if @arg.is_a?(arg_class) && self.respond_to?(from_method)
          end

          fail ArgError
        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_hash(value)
          if self.respond_to?(:from_key_and_value)
            value.map do |k, v|
              from_key_and_value k, v
            end
          else
            fail ArgError
          end
        end

        def from_string(value)
          value
        end

        def node_from_key_and_value(key, value, options = {})
          var = var_from_key_and_value(key, value, options[:prefer] || :var)
          label = label_from_key_and_value(key, value, options[:prefer] || :var)
          attributes = attributes_from_key_and_value(key, value)

          "(#{var}#{format_label(label)}#{attributes_string(attributes)})"
        end

        def var_from_key_and_value(key, value, prefer = :var)
          case value
          when String, Symbol, Class, Module
            key
          when Hash
            if value.values.none? { |v| v.is_a?(Hash) }
              key if prefer == :var
            else
              key
            end
          else
            fail ArgError, value
          end
        end

        def label_from_key_and_value(key, value, prefer = :var)
          case value
          when String, Symbol
            value
          when Class, Module
            defined?(value::CYPHER_LABEL) ? value::CYPHER_LABEL : value.name
          when Hash
            if value.values.map(&:class) == [Hash]
              value.first.first
            else
              key if value.values.none? { |v| v.is_a?(Hash) } && prefer == :label
            end
          else
            fail ArgError, value
          end
        end

        def attributes_from_key_and_value(key, value)
          return nil unless value.is_a?(Hash)

          if value.values.map(&:class) == [Hash]
            value.first[1]
          else
            value
          end
        end

        class << self
          attr_reader :keyword

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

          def to_cypher(clauses)
            string = clause_string(clauses)
            string.strip!

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

        private

        def key_value_string(key, value, previous_keys = [], force_equals = false)
          param = (previous_keys << key).join('_')
          param.gsub!(/[^a-z0-9]+/i, '_')
          param.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
          label_string.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)
          return '' if not 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
            fail ArgError, 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
                clause_id = "neo_id_#{v}"
                @params[clause_id] = v.to_i
                "ID(#{key}) = {#{clause_id}}"
              else
                "#{key}.#{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).flatten.map {|value| "(#{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)
          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)
          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)
          clause_id = "#{self.class.keyword.downcase}_#{value}"
          @params[clause_id] = value.to_i
          "{#{clause_id}}"
        end

        def from_integer(value)
          clause_id = "#{self.class.keyword.downcase}_#{value}"
          @params[clause_id] = value
          "{#{clause_id}}"
        end

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

      class SkipClause < Clause
        @keyword = 'SKIP'

        def from_string(value)
          clause_id = "#{self.class.keyword.downcase}_#{value}"
          @params[clause_id] = value.to_i
          "{#{clause_id}}"
        end

        def from_integer(value)
          clause_id = "#{self.class.keyword.downcase}_#{value}"
          @params[clause_id] = value
          "{#{clause_id}}"
        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
            fail ArgError, 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
            fail ArgError, 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
            fail ArgError, 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
            fail ArgError, value
          end
        end

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