require 'active_support/inflector'

module OrientSupport
  module Support

=begin
supports
  where: 'string'
  where: { property: 'value', property: value, ... }
  where: ['string, { property: value, ... }, ... ]

Used by update and select

_Usecase:_
 ORD.compose_where 'z=34', {u:6}
  => "where z=34 and u = 6" 
=end

    #
		def compose_where *arg , &b
			arg = arg.flatten.compact

			unless arg.blank? 
				g= generate_sql_list( arg , &b)
				"where #{g}" unless g.empty?
			end
    end

=begin
designs a list of "Key =  Value" pairs combined by "and" or the binding  provided by the block
   ORD.generate_sql_list  where: 25 , upper: '65' 
    => "where = 25 and upper = '65'"
   ORD.generate_sql_list(  con_id: 25 , symbol: :G) { ',' } 
    => "con_id = 25 , symbol = 'G'"

If »NULL« should be addressed, { key: nil } is translated to "key = NULL"  (used by set:  in update and upsert),
{ key: [nil]  } is translated to "key is NULL" ( used by where )

=end
		def generate_sql_list attributes = {},  &b
			fill = block_given? ? yield : 'and'
			case attributes 
			when ::Hash
				attributes.map do |key, value|
					case value
					when nil
						"#{key} =  NULL"
					when ::Array
						if value == [nil]
						"#{key} is NULL"
						else	
						"#{key} in #{value.to_orient}"
						end
					when Range
						"#{key} between #{value.first} and #{value.last} " 
					else #  String, Symbol, Time, Trueclass, Falseclass ...
						"#{key} = #{value.to_or}"
					end
				end.join(" #{fill} ")
			when ::Array
				attributes.map{|y| generate_sql_list y, &b }.join( " #{fill} " )
			when String
				attributes
			when Symbol, Numeric
				attributes.to_s
			end		
		end


    # used both in OrientQuery and MatchConnect
		# while and where depend on @q, a struct
		def while_s  value=nil     # :nodoc:
			if value.present?
				@q[:while] << value
				self
			elsif @q[:while].present?
				"while #{ generate_sql_list( @q[:while] ) }"
			end
		end
		def where  value=nil     # :nodoc:
			if value.present?
				if value.is_a?( Hash ) && value.size >1
												value.each {| a,b| where( {a => b} ) }
				else
					@q[:where] <<  value
				end
				self
			elsif @q[:where].present?
				"where #{ generate_sql_list( @q[:where] ){ @fill || 'and' } }"
			end
		end

		def as a=nil
			if a
				@q[:as] = a   # subsequent calls overwrite older entries
			else
				if @q[:as].blank?
					nil
				else
					"as: #{ @q[:as] }"
				end
			end
		end
	end  # module 

	######################## MatchConnection ###############################

	MatchAttributes = Struct.new(:edge, :direction, :as, :count, :where, :while, :max_depth , :depth_alias, :path_alias, :optional )

	# where and while can be composed incremental
	# direction, as, connect and edge cannot be changed after initialisation
  class MatchConnection
    include Support

    def initialize edge= nil, direction: :both, as: nil, count: 1, **args

      the_edge = edge.is_a?( Class ) ?  edge.ref_name : edge.to_s   unless edge.nil? || edge == E
			@q =  MatchAttributes.new  the_edge ,  # class
								direction, #		  may be :both, :in, :out
								as,				 #      a string
								count,     #      a number 
								args[:where],
								args[:while],
								args[:max_depth],
								args[:depth_alias],      # not implemented
								args[:path_alias],       # not implemented
								args[:optional]          # not implemented
    end

    def direction= dir
      @q[:direction] =  dir
    end


		def direction
			fillup =  @q[:edge].present? ? @q[:edge] : ''
			case @q[:direction]
			when :both
				".both(#{fillup})"
			when :in
				".in(#{fillup})"
			when :out
				".out(#{fillup})"
      when :both_vertex, :bothV
				".bothV()"
			when :out_vertex, :outV
				".outV()"
			when :in_vertex, :inV
				".inV()"
     when :both_edge, :bothE
			 ".bothE(#{fillup})"
			when :out_edge, :outE
				".outE(#{fillup})"
			when :in_edge, :outE
				".inE(#{fillup})"
			end

		end

		def count c=nil
			if c
				@q[:count] = c
			else
				@q[:count]
			end
		end

		def max_depth d=nil
			if d.nil?
				@q[:max_depth].present? ? "maxDepth: #{@q[:max_depth] }" : nil
			else
				@q[:max_depth] = d
			end
		end
		def edge
			@q[:edge]
		end

		def compose
				where_statement =( where.nil? || where.size <5 ) ? nil : "where: ( #{ generate_sql_list( @q[:where] ) })"
				while_statement =( while_s.nil? || while_s.size <5) ? nil : "while: ( #{ generate_sql_list( @q[:while] )})"
				
				ministatement = "{"+ [ as, where_statement, while_statement, max_depth].compact.join(', ') + "}"
				ministatement = "" if ministatement=="{}"

     (1 .. count).map{|x| direction }.join("") + ministatement

    end
		alias :to_s :compose
    
  end  # class


	######################## MatchStatement ################################

	MatchSAttributes = Struct.new(:match_class, :as, :where )
  class MatchStatement
    include Support
    def initialize match_class, as: 0,  **args
			reduce_class = ->(c){ c.is_a?(Class) ? c.ref_name : c.to_s }

			@q =  MatchSAttributes.new( reduce_class[match_class],  # class
								as.respond_to?(:zero?) && as.zero? ?  reduce_class[match_class].pluralize : as	,			
								args[ :where ])

			@query_stack = [ self ]
		end

		def match_alias
			"as: #{@q[:as]}"
		end



		# used for the first compose-statement of a compose-query
		def compose_simple
				where_statement = where.is_a?(String) && where.size <3 ?  nil :  "where: ( #{ generate_sql_list( @q[:where] ) })"
			'{'+ [ "class: #{@q[:match_class]}",  as , where_statement].compact.join(', ') + '}'
		end


		def << connection
			@query_stack << connection
			self  # return MatchStatement
		end
		#
		def compile &b
     "match " + @query_stack.map( &:to_s ).join + return_statement( &b )
		end


		# executes the standard-case.
		# returns
		#  * as: :hash   : an array of  hashes
		#  * as: :array  : an array of hash-values 
		#  * as  :flatten: a simple array of hash-values
		#
		# The optional block is used to customize the output. 
		# All previously defiend »as«-Statements are provided though the control variable.
		#
		# Background
		# A match query   "Match {class aaa, as: 'aa'} return aa "  
		#
		# returns [ aa: { result of the query, a Vertex or a value-item  }, aa: {}...}, ...] ]
		# (The standard case)
		#
		# A match query   "Match {class aaa, as: 'aa'} return aa.name "  
		# returns [ aa.name: { name  }, aa.name: { name }., ...] ]
		# 
		# Now, execute( as: :flatten){ "aa.name" }  returns
		#  [name1, name2 ,. ...]
		#
		#
		# Return statements  (examples from https://orientdb.org/docs/3.0.x/sql/SQL-Match.html)
		#  "person.name as name, friendship.since as since, friend.name as friend"
		#
		#  " person.name + \" is a friend of \" + friend.name as friends"
		#
		#  "$matches"
		#  "$elements"
		#  "$paths"
		#  "$pathElements"
		#
		#    
		#
		def execute as: :hash, &b 
			r = V.db.execute{ compile &b }
			case as
			when :hash
				r
			when :array
			 r.map{|y| y.values}
			when :flatten
			 r.map{|y| y.values}.orient_flatten 
			else
				raise ArgumentError, "Specify parameter «as:» with :hash, :array, :flatten"
		 end
		end
#		def compose
#
#			'{'+ [ "class: #{@q[:match_class]}", 
#					"as: #{@as}" , where, while_s, 
#						@maxdepth >0 ? "maxdepth: #{maxdepth}": nil  ].compact.join(', ')+'}'
#		end

		alias :to_s :compose_simple


##  return_statement
		#
		# summarizes defined as-statements ready to be included as last parameter
		# in the match-statement-stack
		#
		# They can be modified through a block.
		#
		# i.e
		#
		# t= TestQuery.match(  where: {a: 9, b: 's'}, as: nil ) << E.connect("<-", as: :test) 
		# t.return_statement{|y| "#{y.last}.name"} 
		#
		# =>> " return  test.name"
		#
		#return_statement is always called through compile
		#
		# t.compile{|y| "#{y.last}.name"} 

 private		
		def return_statement
			resolve_as = ->{  		@query_stack.map{|s| s.as.split(':').last unless s.as.nil? }.compact }
			" return " + statement = if block_given? 
										a= yield resolve_as[] 
										a.is_a?(Array) ? a.join(', ') :  a
									else
										resolve_as[].join(', ')
									end

			
		end
		
	end  # class


	######################## OrientQuery ###################################

	QueryAttributes =  Struct.new( :kind,  :projection, :where, :let, :order, :while, :misc, 
																:class, :return,  :aliases, :database, 
																:set, :remove, :group, :skip, :limit, :unwind )
	
	class OrientQuery
    include Support


#
    def initialize  **args
			@q =  QueryAttributes.new args[:kind] ||	'select' ,
								[], #		 :projection 
								[], # :where ,
								[], # :let ,
								[], # :order,
								[], # :while,
								[] , # misc
								'',  # class
								'',  #  return
								[],   # aliases
								'',  # database
								[],   #set,
								[]  # remove
			  args.each{|k,v| send k, v}
				@fill = block_given? ?   yield  : 'and'
		end
		

=begin
  where: "r > 9"                          --> where r > 9
  where: {a: 9, b: 's'}                   --> where a = 9 and b = 's'
  where:[{ a: 2} , 'b > 3',{ c: 'ufz' }]  --> where a = 2 and b > 3 and c = 'ufz'
=end
		def method_missing method, *arg, &b   # :nodoc: 
			if method ==:while || method=='while'
				while_s arg.first
			else
				@q[:misc] << method.to_s <<  generate_sql_list(arg) 
			end 
			self
    end

		def misc   # :nodoc:
			@q[:misc].join(' ') unless @q[:misc].empty?
		end

    def subquery  # :nodoc: 
      nil
    end

	
		def kind value=nil
			if value.present?
				@q[:kind] = value
				self
			else
			@q[:kind]
			end
		end
=begin
  Output the compiled query
  Parameter: destination (rest, batch )
  If the query is submitted via the REST-Interface (as get-command), the limit parameter is extracted.
=end

		def compose(destination: :batch)
			if kind.to_sym == :update 
				return_statement = "return after " + ( @q[:aliases].empty? ?  "$current" : @q[:aliases].first.to_s)
				[ 'update', target, set, remove, return_statement , where, limit ].compact.join(' ')
			elsif kind.to_sym == :update!
				[ 'update', target, set,  where, limit, misc ].compact.join(' ')
			elsif kind.to_sym == :create
				[ "CREATE VERTEX", target, set ].compact.join(' ')
			#	[ kind, target, set,  return_statement ,where,  limit, misc ].compact.join(' ')
			elsif kind.to_sym == :upsert 
				return_statement = "return after " + ( @q[:aliases].empty? ?  "$current" : @q[:aliases].first.to_s)
				[ "update", target, set,"upsert",  return_statement , where, limit, misc  ].compact.join(' ')
				#[ kind,  where, return_statement ].compact.join(' ')
			elsif destination == :rest
				[ kind, projection, from, let, where, subquery,  misc, order, group_by, unwind, skip].compact.join(' ')
			else
				[ kind, projection, from, let, where, subquery,  while_s,  misc, order, group_by, limit, unwind, skip].compact.join(' ')
			end
		end
		alias :to_s :compose


		def to_or
			compose.to_or
		end

		def target arg =  nil
			if arg.present?
				@q[:database] =  arg
				self # return query-object
			elsif @q[:database].present? 
				the_argument =  @q[:database]
				case @q[:database]
									when ActiveOrient::Model   # a single record
										the_argument.rrid
									when self.class	      # result of a query
										' ( '+ the_argument.compose + ' ) '
									when Class
										the_argument.ref_name
									else
										if the_argument.to_s.rid?	  # a string with "#ab:cd"
											the_argument
										else		  # a database-class-name
											the_argument.to_s  
										end
									end
			else
				raise "cannot complete until a target is specified"
			end
		end

=begin
	from can either be a Databaseclass to operate on or a Subquery providing data to query further
=end
		def from arg = nil
			if arg.present?
				@q[:database] =  arg
				self # return query-object
			elsif  @q[:database].present? # read from
				"from #{ target }"
			end
		end


		def order  value = nil
			if value.present?
				@q[:order] << value
				self
			elsif @q[:order].present?

				"order by " << @q[:order].compact.flatten.map do |o|
					case o
					when String, Symbol, Array
						o.to_s
					else
						o.map{|x,y| "#{x} #{y}"}.join(" ")
					end  # case
				end.join(', ')
			else
				''
			end # unless
		end	  # def


    def database_class            # :nodoc:
  	    @q[:database]
    end

    def database_class= arg   # :nodoc:
  	  @q[:database] = arg 
    end

		def distinct d
			@q[:projection] << "distinct " +  generate_sql_list( d ){ ' as ' }
			self
		end

class << self
		def mk_simple_setter *m
			m.each do |def_m|
				define_method( def_m ) do | value=nil |
						if value.present?
							@q[def_m]  = value
							self
						elsif @q[def_m].present?
						 "#{def_m.to_s}  #{generate_sql_list(@q[def_m]){' ,'}}"
						end
				end
			end
		end
		def mk_std_setter *m
			m.each do |def_m|
				define_method( def_m  ) do | value = nil |
					if value.present?
						@q[def_m] << case value
													when String
														value
													when ::Hash
														value.map{|k,v| "#{k} = #{v.to_or}"}.join(", ")
													else
														raise "Only String or Hash allowed in  #{def_m} statement"
													end
						self
					elsif @q[def_m].present?
						"#{def_m.to_s} #{@q[def_m].join(',')}"	
					end # branch
				end     # def_method
			end  # each
		end  #  def
end # class << self
		mk_simple_setter :limit, :skip, :unwind 
		mk_std_setter :set, :remove

		def let       value = nil
			if value.present?
				@q[:let] << value
				self
			elsif @q[:let].present?
				"let " << @q[:let].map do |s|
					case s
					when String
						s
					when ::Hash  
						s.map do |x,y| 
							# if the symbol: value notation of Hash is used, add "$" to the key
							x =  "$#{x.to_s}"  unless x.is_a?(String) && x[0] == "$"
							"#{x} = #{ case y 
																		when self.class
																			"(#{y.compose})"
																		else
																			y.to_orient
																		end }"
						end
					end
				end.join(', ')
			end
		end
#
		def projection value= nil  # :nodoc:
			if value.present?
				@q[:projection] << value
				self
			elsif  @q[:projection].present?
				@q[:projection].compact.map do | s |
					case s
					when ::Array
						s.join(', ')
					when String, Symbol
						s.to_s
					when ::Hash
						s.map{ |x,y| "#{x} as #{y}"}.join( ', ')
					end
				end.join( ', ' )
			end
		end

			
		
	  def group value = nil
			if value.present?
     	@q[:group] << value
			self
			elsif @q[:group].present?
			 "group by #{@q[:group].join(', ')}"
			end
    end
 
		alias order_by order 
		alias group_by group
		
		def get_limit  # :nodoc: 
    	@q[:limit].nil? ? -1 : @q[:limit].to_i
    end

		def expand item
			@q[:projection] =[ " expand ( #{item.to_s} )" ]
			self
    end

		# connects by adding {in_or_out}('edgeClass')
		def connect_with in_or_out, via: nil
			 argument = " #{in_or_out}(#{via.to_or if via.present?})"
		end
		# adds a connection
		#  in_or_out:  :out --->  outE('edgeClass').in[where-condition] 
		#              :in  --->  inE('edgeClass').out[where-condition]

		def nodes in_or_out = :out, via: nil, where: nil, expand: true
			 condition = where.present? ?  "[ #{generate_sql_list(where)} ]" : ""
			 start =  if in_or_out  == :in
									'inE'
								elsif in_or_out ==  :out
									'outE'
								else
									"both"
								end
			 the_end =  if in_or_out == :in 
										'.out' 
									elsif in_or_out == :out
										'.in'
									else
										''
									end
			 argument = " #{start}(#{[via].flatten.map(&:to_or).join(',') if via.present?})#{the_end}#{condition} "

			 if expand.present?
				 send :expand, argument
			 else
				 @q[:projection]  << argument 
			 end
			 self
		end


		# returns nil if the query was not sucessfully executed
		def execute(reduce: false)
			#puts "Compose: #{compose}"
			result = V.orientdb.execute{ compose }
			return nil unless result.is_a?(::Array)
			result =  result.map{|x| yield x } if block_given?
			return  result.first if reduce && result.size == 1
			## standard case: return Array
			OrientSupport::Array.new( work_on: resolve_target, work_with: result.orient_flatten)   
		end
:protected
		def resolve_target
			if @q[:database].is_a? OrientSupport::OrientQuery
				@q[:database].resolve_target
			else
				@q[:database]
			end
		end

	#	end
	end # class


end # module