#    This file is part of Metasm, the Ruby assembly manipulation suite
#    Copyright (C) 2006-2009 Yoann GUILLOT
#
#    Licence is LGPL, see LICENCE in the top-level directory

module Metasm
module Gui
class Graph
	# one box, has a text, an id, and a list of other boxes to/from
	class Box
		attr_accessor :id, :x, :y, :w, :h
		attr_accessor :to, :from # other boxes linked (arrays)
		attr_accessor :content
		attr_accessor :direct_to
		def initialize(id, content=nil)
			@id = id
			@x = @y = @w = @h = 0
			@to, @from = [], []
			@content = content
		end
		def [](a) @content[a] end
		#def inspect ; puts caller ; "#{Expression[@id] rescue @id.inspect}" end
	end

	attr_accessor :id, :box, :box_id, :root_addrs, :view_x, :view_y, :keep_split
	def initialize(id)
		@id = id
		@root_addrs = []
		@view_x = @view_y = -0xfff_ffff
		clear
	end

	# empty @box
	def clear
		@box = []
		@box_id = {}
	end

	# link the two boxes (by id)
	def link_boxes(id1, id2)
		raise "unknown index 1 #{id1}" if not b1 = @box_id[id1]
		raise "unknown index 2 #{id2}" if not b2 = @box_id[id2]
		b1.to   |= [b2]
		b2.from |= [b1]
	end

	# creates a new box, ensures id is not already taken
	def new_box(id, content=nil)
		raise "duplicate id #{id}" if @box_id[id]
		b = Box.new(id, content)
		@box << b
		@box_id[id] = b
		b
	end

	# returns the [x1, y1, x2, y2] of the rectangle encompassing all boxes
	def boundingbox
		minx = @box.map { |b| b.x }.min.to_i
		miny = @box.map { |b| b.y }.min.to_i
		maxx = @box.map { |b| b.x + b.w }.max.to_i
		maxy = @box.map { |b| b.y + b.h }.max.to_i
		[minx, miny, maxx, maxy]
	end

	# a -> b -> c -> d (no other in/outs)
	def pattern_layout_col(groups)
		# find head
		return if not head = groups.find { |g|
			g.to.length == 1 and
			g.to[0].from.length == 1 and
			(g.from.length != 1 or g.from[0].to.length != 1)
		}

		# find full sequence
		ar = [head]
		while head.to.length == 1 and head.to[0].from.length == 1
			head = head.to[0]
			ar << head
		end

		# recenter boxes (a -> [b, c, d] => center a around b+c+d)
		(ar.length - 1).times { |i|
			g = ar[i]
			gn = ar[i+1]
			withchld = g.content.find_all { |b| not b.to.empty? }
			wc = withchld[0]
			if withchld.length == 1 and wc.to.length >= 1 and wc.to & gn.content == wc.to
				tox = wc.to.map { |b| b.x }.min
				toxmax = wc.to.map { |b| b.x + b.w }.max
				# dx1: center wc center around (wc.to.xmin + wc.to.xmax)/2
				dx1 = tox + (toxmax-tox)/2 - (wc.x + wc.w/2)
				# dx2: center wc center around the mean of the centers of wc.to
				dx2 = wc.to.map { |b| b.x + b.w/2 }.inject(0) { |a, b| a+b } / wc.to.length - (wc.x + wc.w/2)
				dx = (dx1 + dx2) / 2
				ar.length.times { |j|
					ar[j].w += dx
					ar[j].content.each { |b| b.x += (j<=i ? dx/2 : -dx/2) }
				}
			end
		}

		# move boxes inside this group
		maxw = ar.map { |g| g.w }.max
		fullh = ar.inject(0) { |h, g| h + g.h }
		cury = -fullh/2
		ar.each { |g|
			dy = cury - g.y
			g.content.each { |b| b.y += dy }
			cury += g.h
		}

		# create remplacement group
		newg = Box.new(nil, ar.map { |g| g.content }.flatten)
		newg.w = maxw
		newg.h = fullh
		newg.x = -newg.w/2
		newg.y = -newg.h/2
		newg.from = ar.first.from - ar
		newg.to = ar.last.to - ar
		# fix xrefs
		newg.from.each { |g| g.to -= ar ; g.to << newg }
		newg.to.each { |g| g.from -= ar ; g.from << newg }
		# fix groups
		groups[groups.index(head)] = newg
		ar.each { |g| groups.delete g }

		true
	end

	# if a group has no content close to its x/x+w borders, shrink it
	def group_remove_hz_margin(g, maxw=16)
		if g.content.empty?
			g.x = -maxw/2 if g.x < -maxw/2
			g.w = maxw if g.w > maxw
			return
		end

		margin_left = g.content.map { |b| b.x }.min - g.x
		margin_right = g.x+g.w - g.content.map { |b| b.x+b.w }.max
		if margin_left + margin_right > maxw
			g.w -= margin_left + margin_right - maxw
			dx = (maxw/2 + margin_right - margin_left)/2
			g.content.each { |b| b.x += dx }
			g.x = -g.w/2
		end
	end

	# a -> [b, c, d] -> e
	def pattern_layout_line(groups)
		# find head
		ar = []
		groups.each { |g|
			if g.from.length == 1 and g.to.length <= 1 and g.from.first.to.length > 1
				ar = g.from.first.to.find_all { |gg| gg.from == g.from and gg.to == g.to }
			elsif g.from.empty? and g.to.length == 1 and g.to.first.from.length > 1
				ar = g.to.first.from.find_all { |gg| gg.from == g.from and gg.to == g.to }
			else ar = []
			end
			break if ar.length > 1
		}
		return if ar.length <= 1

		ar.each { |g| group_remove_hz_margin(g) }

		# move boxes inside this group
		#ar = ar.sort_by { |g| -g.h }
		maxh = ar.map { |g| g.h }.max
		fullw = ar.inject(0) { |w, g| w + g.w }
		curx = -fullw/2
		ar.each { |g|
			# if no to, put all boxes at bottom ; if no from, put them at top
			case [g.from.length, g.to.length]
			when [1, 0]; dy = (g.h - maxh)/2
			when [0, 1]; dy = (maxh - g.h)/2
			else dy = 0
			end

			dx = curx - g.x
			g.content.each { |b| b.x += dx ; b.y += dy }
			curx += g.w
		}

		# shrink horizontally if possible
		(ar.length - 1).times { |i1|
			g1 = ar[i1]
			g2 = ar[i1+1]
			next if g1.content.empty? or g2.content.empty?
			# only work with full groups, dont try to interleave gaps
			# see if all of one's boxes can be slightly moved inside the other
			g1ymin = g1.content.map { |b| b.y - 9 }.min
			g1ymax = g1.content.map { |b| b.y + b.h + 9 }.max
			g2ymin = g2.content.map { |b| b.y - 9 }.min
			g2ymax = g2.content.map { |b| b.y + b.h + 9 }.max
			g1_matchg2 = g1.content.find_all { |b| b.y + b.h > g2ymin and b.y < g2ymax }
			g2_matchg1 = g2.content.find_all { |b| b.y + b.h > g1ymin and b.y < g1ymax }
			if g1_matchg2.length > 0 and g2_matchg1.length > 0
				g1_up = g1.content.find_all { |b| b.y + b.h < g2ymin }
				g1_down = g1.content.find_all { |b| b.y > g2ymax }
				g2_up = g2.content.find_all { |b| b.y + b.h < g1ymin }
				g2_down = g2.content.find_all { |b| b.y > g1ymax }
				# avoid moving into an arrow
				xmin = g1_matchg2.map { |b| b.x + b.w + 8 }.max
				xmax = g2_matchg1.map { |b| b.x - 8 }.min
				if g1_up.length > 0 and g1_down.length > 0
					xmin = [xmin, g1_up.map { |b| b.x + b.w/2 + 8 }.max, g1_down.map { |b| b.x + b.w/2 + 8 }.max].max
				end
				if g2_up.length > 0 and g2_down.length > 0
					xmax = [xmax, g2_up.map { |b| b.x + b.w/2 + 8 }.min, g2_down.map { |b| b.x + b.w/2 + 8 }.min].min
				end
				dx = xmax - xmin
				if dx > 0
					ar.length.times { |i2|
						ar[i2].content.each { |b| b.x += (i1 >= i2 ? dx/2 : -dx/2) }
					}
				end
			end
		}

		# add a 'margin-top' proportionnal to the ar width
		# this gap should be relative to the real boxes and not possible previous gaps when
		# merging lines (eg long line + many if patterns -> dont duplicate gaps)
		boxen = ar.map { |g| g.content }.flatten
		fullw = boxen.map { |g| g.x + g.w + 8 }.max - boxen.map { |g| g.x - 8 }.min
		realh = boxen.map { |g| g.y + g.h }.max - boxen.map { |g| g.y }.min
		if maxh < realh + fullw/4
			maxh = realh + fullw/4
		end

		# create remplacement group
		newg = Box.new(nil, ar.map { |g| g.content }.flatten)
		newg.w = fullw
		newg.h = maxh
		newg.x = -newg.w/2
		newg.y = -newg.h/2
		newg.from = ar.first.from
		newg.to = ar.first.to
		# fix xrefs
		newg.from.each { |g| g.to -= ar ; g.to << newg }
		newg.to.each { |g| g.from -= ar ; g.from << newg }
		# fix groups
		groups[groups.index(ar.first)] = newg
		ar.each { |g| groups.delete g }

		true
	end

	# a -> b -> c & a -> c
	def pattern_layout_ifend(groups)
		# find head
		return if not head = groups.find { |g|
			g.to.length == 2 and
			((g.to[0].from.length == 1 and g.to[0].to.length == 1 and g.to[0].to[0] == g.to[1]) or
			 (g.to[1].from.length == 1 and g.to[1].to.length == 1 and g.to[1].to[0] == g.to[0]))
		}

		if head.to[0].to.include?(head.to[1])
			ten = head.to[0]
		else
			ten = head.to[1]
		end

		# stuff 'then' inside the 'if'
		# move 'if' up, 'then' down
		head.content.each { |g| g.y -= ten.h/2 }
		ten.content.each { |g| g.y += head.h/2 }
		head.h += ten.h
		head.y -= ten.h/2

		# widen 'if'
		# this adds a phantom left side
		# drop existing margins first
		group_remove_hz_margin(ten)
		dw = ten.w - head.w/2
		if dw > 0
			# need to widen head to fit ten
			head.w += 2*dw
			head.x -= dw
		end

		# merge
		ten.content.each { |g| g.x += -ten.x }
		head.content.concat ten.content

		head.to.delete ten
		head.to[0].from.delete ten

		groups.delete ten

		true

	end

	def pattern_layout_complex(groups)
		order = order_graph(groups)
		uniq = nil
		if groups.sort_by { |g| order[g] }.find { |g|
			next if g.to.length <= 1
			# list all nodes reachable for every 'to'
			reach = g.to.map { |t| list_reachable(t) }
			# list all nodes reachable only from a single 'to'
			uniq = []
			reach.each_with_index { |r, i|
				# take all nodes reachable from there ...
				u = uniq[i] = r.dup
				u.delete_if { |k, v| k.content.empty? }	# ignore previous layout_complex artifacts
				reach.each_with_index { |rr, ii|
					next if i == ii
					# ... and delete nodes reachable from anywhere else
					rr.each_key { |k| u.delete k }
				}
			}
			uniq.delete_if { |u| u.length <= 1 }
			!uniq.empty?
		}
			# now layout every uniq subgroup independently
			uniq.each { |u|
				subgroups = groups.find_all { |g| u[g] }

				# isolate subgroup from external links
				# change all external links into a single empty box
				newtop = Box.new(nil, [])
				newtop.x = -8 ; newtop.y = -9
				newtop.w = 16 ; newtop.h = 18
				newbot = Box.new(nil, [])
				newbot.x = -8 ; newbot.y = -9
				newbot.w = 16 ; newbot.h = 18
				hadfrom = [] ; hadto = []
				subgroups.each { |g|
					g.to.dup.each { |t|
						next if u[t]
						newbot.from |= [g]
						g.to.delete t
						hadto << t
						g.to |= [newbot]
					}
					g.from.dup.each { |f|
						next if u[f]
						newtop.to |= [g]
						g.from.delete f
						hadfrom << f
						g.from |= [newtop]
					}
				}
				subgroups << newtop << newbot

				# subgroup layout
				auto_arrange_step(subgroups) while subgroups.length > 1
				newg = subgroups[0]

				# patch 'groups'
				idx = groups.index { |g| u[g] }
				groups.delete_if { |g| u[g] }
				groups[idx, 0] = [newg]

				# restore external links & fix xrefs
				hadfrom.uniq.each { |f|
					f.to.delete_if { |t| u[t] }
					f.to |= [newg]
					newg.from |= [f]
				}
				hadto.uniq.each { |t|
					t.from.delete_if { |f| u[f] }
					t.from |= [newg]
					newg.to |= [t]
				}
			}

			true
		end
	end

	# find the minimal set of nodes from which we can reach all others
	# this is done *before* removing cycles in the graph
	# returns the order (Hash group => group_order)
	# roots have an order of 0
	def order_graph(groups)
		roots = groups.find_all { |g| g.from.empty? }
		o = {}	# tentative order
		todo = []

		loop do
			roots.each { |g|
				o[g] ||= 0
				todo |= g.to.find_all { |gg| not o[gg] }
			}

			# order nodes from the tentative roots
			until todo.empty?
				n = todo.find { |g| g.from.all? { |gg| o[gg] } } || order_solve_cycle(todo, o)
				todo.delete n
				o[n] = n.from.map { |g| o[g] }.compact.max + 1
				todo |= n.to.find_all { |g| not o[g] }
			end
			break if o.length >= groups.length

			# pathological cases

			if noroot = groups.find_all { |g| o[g] and g.from.find { |gg| not o[gg] } }.sort_by { |g| o[g] }.first
				# we picked a root in the middle of the graph, walk up
				todo |= noroot.from.find_all { |g| not o[g] }
				until todo.empty?
					n = todo.find { |g| g.to.all? { |gg| o[gg] } } ||
					    todo.sort_by { |g| g.to.map { |gg| o[gg] }.compact.min }.first
					todo.delete n
					o[n] = n.to.map { |g| o[g] }.compact.min - 1
					todo |= n.from.find_all { |g| not o[g] }
				end
				# setup todo for next fwd iteration
				todo |= groups.find_all { |g| not o[g] and g.from.find { |gg| o[gg] } }
			else
				# disjoint graph, start over from one other random node
				roots << groups.find { |g| not o[g] }
			end
		end

		if o.values.find { |rank| rank < 0 }
			# did hit a pathological case, restart with found real roots
			roots = groups.find_all { |g| not g.from.find { |gg| o[gg] < o[g] } }
			o = {}
			todo = []
			roots.each { |g|
				o[g] ||= 0
				todo |= g.to.find_all { |gg| not o[gg] }
			}
			until todo.empty?
				n = todo.find { |g| g.from.all? { |gg| o[gg] } } || order_solve_cycle(todo, o)
				todo.delete n
				o[n] = n.from.map { |g| o[g] }.compact.max + 1
				todo |= n.to.find_all { |g| not o[g] }
			end

			# there's something screwy around here !
			raise "moo" if o.length < groups.length
		end

		o
	end

	def order_solve_cycle(todo, o)
		# 'todo' has no trivial candidate
		# pick one node from todo which no other todo can reach
		# exclude pathing through already ordered nodes
		todo.find { |t1|
			not todo.find { |t2| t1 != t2 and can_find_path(t2, t1, o.dup) }
		} ||
		# some cycle heads are mutually recursive
		todo.sort_by { |t1|
			# find the one who can reach the most others
			[todo.find_all { |t2| t1 != t2 and can_find_path(t1, t2, o.dup) }.length,
			# and with the highest rank
			 t1.from.map { |gg| o[gg] }.compact.max]
		}.last
	end

	# checks if there is a path from src to dst avoiding stuff in 'done'
	def can_find_path(src, dst, done={})
		todo = [src]
		while g = todo.pop
			next if done[g]
			return true if g == dst
			done[g] = true
			todo.concat g.to
		end
		false
	end

	# returns a hash with true for every node reachable from src (included)
	def list_reachable(src, done={})
		todo = [src]
		while g = todo.pop
			next if done[g]
			done[g] = true
			todo.concat g.to
		end
		done
	end

	# revert looping edges in groups
	def make_tree(groups, order)
		# now we have the roots and node orders
		#  revert cycling edges - o(chld) < o(parent)
		order.each_key { |g|
			g.to.dup.each { |gg|
				if order[gg] < order[g]
					# cycling edge, revert
					g.to.delete gg
					gg.from.delete g
					g.from |= [gg]
					gg.to |= [g]
				end
			}
		}
	end

	# group groups in layers of same order
	# create dummy groups along long edges so that no path exists between non-contiguous layers
	def create_layers(groups, order)
		newemptybox = lambda {
			b = Box.new(nil, [])
			b.x = -8
			b.y = -9
			b.w = 16
			b.h = 18
			groups << b
			b
		}

		newboxo = {}

		order.each_key { |g|
			og = order[g] || newboxo[g]
			g.to.dup.each { |gg|
				ogg = order[gg] || newboxo[gg]
				if ogg > og+1
					# long edge, expand
					sq = [g]
					(ogg - 1 - og).times { |i| sq << newemptybox[] }
					sq << gg
					gg.from.delete g
					g.to.delete gg
					newboxo[g] ||= order[g]
					sq.inject { |g1, g2|
						g1.to |= [g2]
						g2.from |= [g1]
						newboxo[g2] = newboxo[g1]+1
						g2
					}
					raise if newboxo[gg] != ogg
				end
			}
		}

		order.update newboxo

		# layers[o] = [list of nodes of order o]
		layers = []
		groups.each { |g|
			(layers[order[g]] ||= []) << g
		}

		layers
	end

	# take all groups, order them by order, layout as layers
	# always return a single group holding everything
	def layout_layers(groups)
		order = order_graph(groups)
		# already a tree
		layers = create_layers(groups, order)
		return if layers.empty?

		layers.each { |l| l.each { |g| group_remove_hz_margin(g) } }

		# widest layer width
		maxlw = layers.map { |l| l.inject(0) { |ll, g| ll + g.w } }.max

		# center the 1st layer boxes on a segment that large
		x0 = -maxlw/2.0
		curlw = layers[0].inject(0) { |ll, g| ll + g.w }
		dx0 = (maxlw - curlw) / (2.0*layers[0].length)
		layers[0].each { |g|
			x0 += dx0
			g.x = x0
			x0 += g.w + dx0
		}

		# at this point, the goal is to reorder the most populated layer the best we can, and
		# move other layers' boxes accordingly
		layers[1..-1].each { |l|
			# for each subsequent layer, reorder boxes based on their ties with the previous layer
			i = 0
			l.replace l.sort_by { |g|
				# we know g.from is not empty (g would be in @layer[0])
				medfrom = g.from.inject(0.0) { |mx, gg| mx + (gg.x + gg.w/2.0) } / g.from.length
				# on ties, keep original order
				[medfrom, i]
			}
			# now they are reordered, update their #x accordingly
			# evenly distribute them in the layer
			x0 = -maxlw/2.0
			curlw = l.inject(0) { |ll, g| ll + g.w }
			dx0 = (maxlw - curlw) / (2.0*l.length)
			l.each { |g|
				x0 += dx0
				g.x = x0
				x0 += g.w + dx0
			}
		}

		layers[0...-1].reverse_each { |l|
			# for each subsequent layer, reorder boxes based on their ties with the previous layer
			i = 0
			l.replace l.sort_by { |g|
				if g.to.empty?
					# TODO floating end
					medfrom = 0
				else
					medfrom = g.to.inject(0.0) { |mx, gg| mx + (gg.x + gg.w/2.0) } / g.to.length
				end
				# on ties, keep original order
				[medfrom, i]
			}
			# now they are reordered, update their #x accordingly
			x0 = -maxlw/2.0
			curlw = l.inject(0) { |ll, g| ll + g.w }
			dx0 = (maxlw - curlw) / (2.0*l.length)
			l.each { |g|
				x0 += dx0
				g.x = x0
				x0 += g.w + dx0
			}
		}

		# now the boxes are (hopefully) sorted correctly
		# position them according to their ties with prev/next layer
		# from the maxw layer (positionning = packed), propagate adjacent layers positions
		maxidx = (0..layers.length).find { |i| l = layers[i] ; l.inject(0) { |ll, g| ll + g.w } == maxlw }
		# list of layer indexes to walk
		ilist = [maxidx]
		ilist.concat((maxidx+1...layers.length).to_a) if maxidx < layers.length-1
		ilist.concat((0..maxidx-1).to_a.reverse) if maxidx > 0
		layerbox = []
		ilist.each { |i|
			l = layers[i]
			curlw = l.inject(0) { |ll, g| ll + g.w }
			# left/rightmost acceptable position for the current box w/o overflowing on the right side
			minx = -maxlw/2.0
			maxx = minx + (maxlw-curlw)

			# replace whole layer with a box
			newg = layerbox[i] = Box.new(nil, l.map { |g| g.content }.flatten)
			newg.w = maxlw
			newg.h = l.map { |g| g.h }.max
			newg.x = -newg.w/2
			newg.y = -newg.h/2
			# dont care for from/to, we'll return a single box anyway

			l.each { |g|
				ref = (i < maxidx) ? g.to : g.from
				# TODO elastic positionning around the ideal position
				# (g and g+1 may have the same med, then center both on it)
				if i == maxidx
					nx = minx
				elsif ref.empty?
					nx = (minx+maxx)/2
				else
					# center on the outline of rx
					# may want to center on rx center's center ?
					rx = ref.sort_by { |gg| gg.x }
					med = (rx.first.x + rx.last.x + rx.last.w - g.w) / 2.0
					nx = [[med, minx].max, maxx].min
				end
				dx = nx+g.w/2
				g.content.each { |b| b.x += dx }
				minx = nx+g.w
				maxx += g.w
			}
		}

		newg = Box.new(nil, layerbox.map { |g| g.content }.flatten)
		newg.w = layerbox.map { |g| g.w }.max
		newg.h = layerbox.inject(0) { |h, g| h + g.h }
		newg.x = -newg.w/2
		newg.y = -newg.h/2

		# vertical: just center each box on its layer
		y0 = newg.y
		layerbox.each { |lg|
			lg.content.each { |b|
				b.y += y0-lg.y
			}
			y0 += lg.h
		}

		groups.replace [newg]
	end


	# place boxes in a good-looking layout
	# create artificial 'group' container for boxes, that will later be merged in geometrical patterns
	def auto_arrange_init
		# 'group' is an array of boxes
		# all groups are centered on the origin
		h = {}	# { box => group }
		@groups = @box.map { |b|
			b.x = -b.w/2
			b.y = -b.h/2
			g = Box.new(nil, [b])
			g.x = b.x - 8
			g.y = b.y - 9
			g.w = b.w + 16
			g.h = b.h + 18
			h[b] = g
			g
		}

		# init group.to/from
		# must always point to something that is in the 'groups' array
		# no self references
		# a box is in one and only one group in 'groups'
		@groups.each { |g|
			g.to   = g.content.first.to.map   { |t| h[t] if h[t] != g }.compact
			g.from = g.content.first.from.map { |f| h[f] if h[f] != g }.compact
		}

		# order boxes
		order = order_graph(@groups)

		# remove cycles from the graph
		make_tree(@groups, order)
	end

	def auto_arrange_step(groups=@groups)
		pattern_layout_col(groups) or pattern_layout_line(groups) or
			pattern_layout_ifend(groups) or pattern_layout_complex(groups) or
			layout_layers(groups)
	end

	def auto_arrange_post
		auto_arrange_movebox
		#auto_arrange_vertical_shrink
	end

	# actually move boxes inside the groups
	def auto_arrange_movebox
		@groups.each { |g|
			dx = (g.x + g.w/2).to_i
			dy = (g.y + g.h/2).to_i
			g.content.each { |b|
				b.x += dx
				b.y += dy
			}
		}
	end

	def auto_arrange_vertical_shrink
		# vertical shrink
		# TODO stuff may shrink vertically more if we could move it slightly horizontally...
		@box.sort_by { |b| b.y }.each { |b|

			next if b.from.empty?
			# move box up to its from, unless something blocks the way

			min_y = b.from.map { |bb|
					bb.y+bb.h
				}.find_all { |by|
					by <= b.y
				}.max

			moo = []
			moo << 8*b.from.length
			moo << 8*b.from[0].to.length
			cx = b.x+b.w/2
			moo << b.from.map { |bb| (cx - (bb.x+bb.w/2)).abs }.max / 10
			cx = b.from[0].x+b.from[0].w/2
			moo << b.from[0].to.map { |bb| (cx - (bb.x+bb.w/2)).abs }.max / 10
			margin_y = 16 + moo.max

			next if not min_y or b.y <= min_y + margin_y

			blocking = @box.find_all { |bb|
					next if bb == b
					bb.y+bb.h > min_y and bb.y+bb.h < b.y and
					bb.x-12 < b.x+b.w and bb.x+bb.w+12 > b.x
				}

			may_y = blocking.map { |bb| bb.y+bb.h } << min_y

			do_y = may_y.sort.map { |by| by + margin_y }.find { |by|
				# should not collision with b if moved to by+margin_y
				not blocking.find { |bb|
					bb.x-12 < b.x+b.w and bb.x+bb.w+12 > b.x and
					bb.y-12 < by+b.h and bb.y+bb.h+12 > by
				}
			}

			b.y = do_y if do_y < b.y

			# no need to re-sort outer loop
		}

		# TODO
		# energy-minimal positionning of boxes from this basic layout
		# avoid arrow confusions
	end

	def auto_arrange_boxes
		auto_arrange_init
		nil while @groups.length > 1 and auto_arrange_step
		auto_arrange_post
		@groups = []
	end

	# gives a text representation of the current graph state
	def dump_layout(groups=@groups)
		groups.map { |g| "#{groups.index(g)} -> #{g.to.map { |t| groups.index(t) }.sort.inspect}" }
	end
end





class GraphViewWidget < DrawableWidget
	attr_accessor :dasm, :caret_box, :curcontext, :zoom, :margin
	# bool, specifies if we should display addresses before instrs
	attr_accessor :show_addresses

	def initialize_widget(dasm, parent_widget)
		@dasm = dasm
		@parent_widget = parent_widget

		@show_addresses = false

		@caret_box = nil
		@selected_boxes = []
		@shown_boxes = []
		@mousemove_origin = @mousemove_origin_ctrl = nil
		@curcontext = Graph.new(nil)
		@want_focus_addr = nil
		@margin = 8
		@zoom = 1.0
		@default_color_association = ColorTheme.merge :hlbox_bg => :palegrey, :box_bg => :white,
				:arrow_hl => :red, :arrow_cond => :darkgreen, :arrow_uncond => :darkblue,
				:arrow_direct => :darkred, :box_bg_shadow => :black, :background => :paleblue
		# @othergraphs = ?	(to keep user-specified formatting)
	end

	def view_x; @curcontext.view_x; end
	def view_x=(vx); @curcontext.view_x = vx; end
	def view_y; @curcontext.view_y; end
	def view_y=(vy); @curcontext.view_y = vy; end

	def resized(w, h)
		redraw
	end

	def find_box_xy(x, y)
		x = view_x+x/@zoom
		y = view_y+y/@zoom
		@shown_boxes.to_a.reverse.find { |b| b.x <= x and b.x+b.w > x and b.y <= y-1 and b.y+b.h > y+1 }
	end

	def mouse_wheel_ctrl(dir, x, y)
		case dir
		when :up
			if @zoom < 100
				# zoom in
				oldzoom = @zoom
				@zoom *= 1.1
				@zoom = 1.0 if (@zoom-1.0).abs < 0.05
				@curcontext.view_x += (x / oldzoom - x / @zoom)
				@curcontext.view_y += (y / oldzoom - y / @zoom)
			end
		when :down
			if @zoom > 1.0/1000
				# zoom out
				oldzoom = @zoom
				@zoom /= 1.1
				@zoom = 1.0 if (@zoom-1.0).abs < 0.05
				@curcontext.view_x += (x / oldzoom - x / @zoom)
				@curcontext.view_y += (y / oldzoom - y / @zoom)
			end
		end
		redraw
	end

	def mouse_wheel(dir, x, y)
		case dir
		when :up; @curcontext.view_y -= height/4 / @zoom
		when :down; @curcontext.view_y += height/4 / @zoom
		end
		redraw
	end

	def mousemove(x, y)
		return if not @mousemove_origin

		dx = (x - @mousemove_origin[0])/@zoom
		dy = (y - @mousemove_origin[1])/@zoom
		@mousemove_origin = [x, y]
		if @selected_boxes.empty?
			@curcontext.view_x -= dx ; @curcontext.view_y -= dy
		else
			@selected_boxes.each { |b| b.x += dx ; b.y += dy }
		end
		redraw
	end

	def mouserelease(x, y)
		mousemove(x, y)
		@mousemove_origin = nil

		if @mousemove_origin_ctrl
			x1 = view_x + @mousemove_origin_ctrl[0]/@zoom
			x2 = x1 + (x - @mousemove_origin_ctrl[0])/@zoom
			x1, x2 = x2, x1 if x1 > x2
			y1 = view_y + @mousemove_origin_ctrl[1]/@zoom
			y2 = y1 + (y - @mousemove_origin_ctrl[1])/@zoom
			y1, y2 = y2, y1 if y1 > y2
			@selected_boxes |= @curcontext.box.find_all { |b| b.x >= x1 and b.x + b.w <= x2 and b.y >= y1 and b.y + b.h <= y2 }
			redraw
			@mousemove_origin_ctrl = nil
		end
	end

	def click_ctrl(x, y)
		if b = find_box_xy(x, y)
			if @selected_boxes.include? b
				@selected_boxes.delete b
			else
				@selected_boxes << b
			end
			redraw
		else
			@mousemove_origin_ctrl = [x, y]
		end
	end

	def click(x, y)
		@mousemove_origin = [x, y]
		if b = find_box_xy(x, y)
			@selected_boxes = [b] if not @selected_boxes.include? b
			@caret_box = b
			@caret_x = (view_x+x/@zoom-b.x-1).to_i / @font_width
			@caret_y = (view_y+y/@zoom-b.y-1).to_i / @font_height
			update_caret
		else
			@selected_boxes = []
			@caret_box = nil
		end
		redraw
	end

	def setup_contextmenu(b, m)
		cm = new_menu
		addsubmenu(cm, 'copy _word') { clipboard_copy(@hl_word) if @hl_word }
		addsubmenu(cm, 'copy _line') { clipboard_copy(@caret_box[:line_text_col][@caret_y].map { |ss, cc| ss }.join) }
		addsubmenu(cm, 'copy _box')  {
			sb = @selected_boxes
			sb = [@curbox] if sb.empty?
			clipboard_copy(sb.map { |ob| ob[:line_text_col].map { |s| s.map { |ss, cc| ss }.join + "\r\n" }.join }.join("\r\n"))
		}	# XXX auto \r\n vs \n
		addsubmenu(m, '_clipboard', cm)
		addsubmenu(m, 'clone _window') { @parent_widget.clone_window(@hl_word, :graph) }
		addsubmenu(m, 'show descendants only') { hide_non_descendants(@selected_boxes) }
		addsubmenu(m, 'show ascendants only') { hide_non_ascendants(@selected_boxes) }
		addsubmenu(m, 'restore graph') { gui_update }
	end

	# if the target is a call to a subfunction, open a new window with the graph of this function (popup)
	def rightclick(x, y)
		if b = find_box_xy(x, y) and @zoom >= 0.90 and @zoom <= 1.1
			click(x, y)
			@mousemove_origin = nil
			m = new_menu
			setup_contextmenu(b, m)
			if @parent_widget.respond_to?(:extend_contextmenu)
				@parent_widget.extend_contextmenu(self, m, @caret_box[:line_address][@caret_y])
			end
			popupmenu(m, x, y)
		end
	end

	def doubleclick(x, y)
		@mousemove_origin = nil
		if b = find_box_xy(x, y)
			if @hl_word and @zoom >= 0.90 and @zoom <= 1.1
				@parent_widget.focus_addr(@hl_word)
			else
				@parent_widget.focus_addr((b[:addresses] || b[:line_address]).first)
			end
		elsif doubleclick_check_arrow(x, y)
		elsif @zoom == 1.0
			zoom_all
		else
			@curcontext.view_x += (x/@zoom - x)
			@curcontext.view_y += (y/@zoom - y)
			@zoom = 1.0
		end
		redraw
	end

	# check if the user clicked on the beginning/end of an arrow, if so focus on the other end
	def doubleclick_check_arrow(x, y)
		return if @margin*@zoom < 2
		x = view_x+x/@zoom
		y = view_y+y/@zoom
		sx = nil
		if bt = @shown_boxes.to_a.reverse.find { |b|
			y >= b.y+b.h-1 and y <= b.y+b.h-1+@margin+2 and
			sx = b.x+b.w/2 - b.to.length/2 * @margin/2 and
			x >= sx-@margin/2 and x <= sx+b.to.length*@margin/2	# should be margin/4, but add a little comfort margin
		}
			idx = (x-sx+@margin/4).to_i / (@margin/2)
			idx = 0 if idx < 0
			idx = bt.to.length-1 if idx >= bt.to.length
			if bt.to[idx]
				if @parent_widget
					@caret_box, @caret_y = bt, bt[:line_address].length-1
					@parent_widget.focus_addr bt.to[idx][:line_address][0]
				else
					focus_xy(bt.to[idx].x, bt.to[idx].y)
				end
			end
			true
		elsif bf = @shown_boxes.to_a.reverse.find { |b|
			y >= b.y-@margin-2 and y <= b.y and
			sx = b.x+b.w/2 - b.from.length/2 * @margin/2 and
			x >= sx-@margin/2 and x <= sx+b.from.length*@margin/2
		}
			idx = (x-sx+@margin/4).to_i / (@margin/2)
			idx = 0 if idx < 0
			idx = bf.from.length-1 if idx >= bf.from.length
			if bf.from[idx]
				if @parent_widget
					@caret_box, @caret_y = bf, bf[:line_address].length-1
					@parent_widget.focus_addr bf.from[idx][:line_address][-1]
				else
					focus_xy(bt.from[idx].x, bt.from[idx].y)
				end
			end
			true
		end
	end

	# update the zoom & view_xy to show the whole graph in the window
	def zoom_all
		minx, miny, maxx, maxy = @curcontext.boundingbox
		minx -= @margin
		miny -= @margin
		maxx += @margin
		maxy += @margin

		@zoom = [width.to_f/(maxx-minx), height.to_f/(maxy-miny)].min
		@zoom = 1.0 if @zoom > 1.0 or (@zoom-1.0).abs < 0.1
		@curcontext.view_x = minx + (maxx-minx-width/@zoom)/2
		@curcontext.view_y = miny + (maxy-miny-height/@zoom)/2
		redraw
	end

	def paint
		update_graph if @want_update_graph
		if @want_focus_addr and @curcontext.box.find { |b_| b_[:line_address].index(@want_focus_addr) }
			focus_addr(@want_focus_addr, false)
			@want_focus_addr = nil
			#zoom_all
		end

		@curcontext.box.each { |b|
			# reorder arrows so that endings do not overlap
			b.to = b.to.sort_by { |bt| bt.x+bt.w/2 }
			b.from = b.from.sort_by { |bt| bt.x+bt.w/2 }
		}
		# arrows drawn first to stay under the boxes
		# XXX precalc ?
		@curcontext.box.each { |b|
			b.to.each { |bt| paint_arrow(b, bt) }
		}

		@shown_boxes = []
		w_w = width
		w_h = height
		@curcontext.box.each { |b|
			next if b.x >= view_x+w_w/@zoom or b.y >= view_y+w_h/@zoom or b.x+b.w <= view_x or b.y+b.h <= view_y
			@shown_boxes << b
			paint_box(b)
		}
	end

	def set_color_arrow(b1, b2)
		if b1 == @caret_box or b2 == @caret_box
			draw_color :arrow_hl
		elsif b1.to.length == 1
			draw_color :arrow_uncond
		elsif b1.direct_to == b2.id
			draw_color :arrow_direct
		else
			draw_color :arrow_cond
		end
	end

	def paint_arrow(b1, b2)
		x1 = x1o = b1.x+b1.w/2-view_x
		y1 = b1.y+b1.h-view_y
		x2 = x2o = b2.x+b2.w/2-view_x
		y2 = b2.y-1-view_y
		margin = @margin
		x1 += (-(b1.to.length-1)/2 + b1.to.index(b2)) * margin/2
		x2 += (-(b2.from.length-1)/2 + b2.from.index(b1)) * margin/2
		return if (y1+margin < 0 and y2 < 0) or (y1 > height/@zoom and y2-margin > height/@zoom)	# just clip on y
		margin, x1, y1, x2, y2, b1w, b2w, x1o, x2o = [margin, x1, y1, x2, y2, b1.w, b2.w, x1o, x2o].map { |v| v*@zoom }


		# straighten vertical arrows if possible
		if y2 > y1 and (x1-x2).abs <= margin
			if b1.to.length == 1
				x1 = x2
			elsif b2.from.length == 1
				x2 = x1
			end
		end

		set_color_arrow(b1, b2)
		if margin > 1
			# draw arrow tip
			draw_line(x1, y1, x1, y1+margin)
			draw_line(x2, y2-margin+1, x2, y2)
			draw_line(x2-margin/2, y2-margin/2, x2, y2)
			draw_line(x2+margin/2, y2-margin/2, x2, y2)
			y1 += margin
			y2 -= margin-1
		end

		if y2 > y1 - b1.h*@zoom - 2*margin+1
			# straight arrow
			draw_line(x1, y1, x2, y2) if x1 != y1 or x2 != y2

		else
			# arrow goes up: navigate around b2
			x = (x1 <= x2 ? [x1o-b1w/2-margin, x2o-b2w/2-margin].min : [x1o+b1w/2+margin, x2o+b2w/2+margin].max)
			draw_line(x1, y1, x, y1)
			draw_line(x, y1, x, y2)
			draw_line(x, y2, x2, y2)
			draw_line(x1, y1+1, x, y1+1) # double
			draw_line(x+1, y1, x+1, y2)
			draw_line(x, y2+1, x2, y2+1)
		end
	end

	def set_color_boxshadow(b)
		draw_color :box_bg_shadow
	end

	def set_color_box(b)
		if @selected_boxes.include? b
			draw_color :hlbox_bg
		else
			draw_color :box_bg
		end
	end

	def paint_box(b)
		set_color_boxshadow(b)
		draw_rectangle((b.x-view_x+3)*@zoom, (b.y-view_y+4)*@zoom, b.w*@zoom, b.h*@zoom)
		set_color_box(b)
		draw_rectangle((b.x-view_x)*@zoom, (b.y-view_y+1)*@zoom, b.w*@zoom, b.h*@zoom)

		# current text position
		x = (b.x - view_x + 1)*@zoom
		y = (b.y - view_y + 1)*@zoom
		w_w = (b.x - view_x + b.w - @font_width)*@zoom
		w_h = (b.y - view_y + b.h - @font_height)*@zoom
		w_h = height if w_h > height

		if @parent_widget and @parent_widget.bg_color_callback
			ly = 0
			b[:line_address].each { |a|
				if c = @parent_widget.bg_color_callback[a]
					draw_rectangle_color(c, (b.x-view_x)*@zoom, (1+b.y-view_y+ly*@font_height)*@zoom, b.w*@zoom, (@font_height*@zoom).ceil)
				end
				ly += 1
			}
		end

		if @caret_box == b
			draw_rectangle_color(:cursorline_bg, (b.x-view_x)*@zoom, (1+b.y-view_y+@caret_y*@font_height)*@zoom, b.w*@zoom, @font_height*@zoom)
		end

		return if @zoom < 0.99 or @zoom > 1.1
		# TODO dynamic font size ?

		# renders a string at current cursor position with a color
		# must not include newline
		render = lambda { |str, color|
			next if y >= w_h+2 or x >= w_w
			draw_string_hl(color, x, y, str)
			x += str.length * @font_width
		}

		yoff = @font_height * @zoom
		b[:line_text_col].each { |list|
			list.each { |s, c| render[s, c] } if y >= -yoff
			x = (b.x - view_x + 1)*@zoom
			y += yoff
			break if y > w_h+2
		}

		if b == @caret_box and focus?
			cx = (b.x - view_x + 1 + @caret_x*@font_width)*@zoom
			cy = (b.y - view_y + 1 + @caret_y*@font_height)*@zoom
			draw_line_color(:caret, cx, cy, cx, cy+(@font_height-1)*@zoom)
		end
	end

	def gui_update
		@want_update_graph = true
		redraw
	end

	#
	# rebuild the code flow graph from @curcontext.roots
	# recalc the boxes w/h
	#
	def update_graph
		@want_update_graph = false

		ctx = @curcontext

		boxcnt = ctx.box.length
		arrcnt = ctx.box.inject(0) { |s, b| s + b.to.length + b.from.length }
		ctx.clear

		build_ctx(ctx)

		ctx.auto_arrange_boxes

		return if ctx != @curcontext

		if boxcnt != ctx.box.length or arrcnt != ctx.box.inject(0) { |s, b| s + b.to.length + b.from.length }
			zoom_all
		elsif @caret_box	# update @caret_box with a box at the same place
			bx = @caret_box.x + @caret_box.w/2
			by = @caret_box.y + @caret_box.h/2
			@caret_box = ctx.box.find { |cb| cb.x < bx and cb.x+cb.w > bx and cb.y < by and cb.y+cb.h > by }
		end
	end

	def load_dotfile(path)
		load_dot(File.read(path))
	end

	def load_dot(dota)
		@want_update_graph = false
		@curcontext.clear
		boxes = {}
		new_box = lambda { |text|
			b = @curcontext.new_box(text, :line_text_col => [[[text, :text]]])
			b.w = (text.length+1) * @font_width
			b.h = @font_height
			b
		}
		dota.scan(/^.*$/) { |l|
			a = l.strip.chomp(';').split(/->/).map { |s| s.strip.delete '"' }
			next if not id = a.shift
			b0 = boxes[id] ||= new_box[id]
			while id = a.shift
				b1 = boxes[id] ||= new_box[id]
				b0.to   |= [b1]
				b1.from |= [b0]
				b0 = b1
			end
		}
		redraw
	rescue Interrupt
		puts "dot_len #{boxes.length}"
	end

	# create the graph objects in ctx
	def build_ctx(ctx)
		# graph : block -> following blocks in same function
		block_rel = {}

		todo = ctx.root_addrs.dup
		done = [:default, Expression::Unknown]
		while a = todo.shift
			a = @dasm.normalize a
			next if done.include? a
			done << a
			next if not di = @dasm.di_at(a)
			if not di.block_head?
				block_rel[di.block.address] = [a]
				@dasm.split_block(a)
			end
			block_rel[a] = []
			di.block.each_to_samefunc(@dasm) { |t|
				t = @dasm.normalize t
				next if not @dasm.di_at(t)
				todo << t
				block_rel[a] << t
			}
			block_rel[a].uniq!
		end

		# populate boxes
		addr2box = {}
		todo = ctx.root_addrs.dup
		todo.delete_if { |t| not @dasm.di_at(t) }	# undefined func start
		done = []
		while a = todo.shift
			next if done.include? a
			done << a
			if not ctx.keep_split.to_a.include?(a) and from = block_rel.keys.find_all { |ba| block_rel[ba].include? a } and
					from.length == 1 and block_rel[from.first].length == 1 and
					addr2box[from.first] and lst = @dasm.decoded[from.first].block.list.last and
					lst.next_addr == a and (not lst.opcode.props[:saveip] or lst.block.to_subfuncret)
				box = addr2box[from.first]
			else
				box = ctx.new_box a, :addresses => [], :line_text_col => [], :line_address => []
			end
			@dasm.decoded[a].block.list.each { |di_|
				box[:addresses] << di_.address
				addr2box[di_.address] = box
			}
			todo.concat block_rel[a]
		end

		# link boxes
		ctx.box.each { |b|
			next if not di = @dasm.decoded[b[:addresses].last]
			a = di.block.address
			next if not block_rel[a]
			block_rel[a].each { |t|
				ctx.link_boxes(b.id, t)
				b.direct_to = t if t == di.next_addr
			}
		}

		# calc box dimensions/text
		ctx.box.each { |b|
			colstr = []
			curaddr = nil
			line = 0
			render = lambda { |str, col| colstr << [str, col] }
			nl = lambda {
				b[:line_address][line] = curaddr
				b[:line_text_col][line] = colstr
				colstr = []
				line += 1
			}
			b[:addresses].each { |addr|
				curaddr = addr
				if di = @dasm.di_at(curaddr)
					if di.block_head?
						# render dump_block_header, add a few colors
						b_header = '' ; @dasm.dump_block_header(di.block) { |l| b_header << l ; b_header << ?\n if b_header[-1] != ?\n }
						b_header.strip.each_line { |l| l.chomp!
							col = :comment
							col = :label if l[0, 2] != '//' and l[-1] == ?:
							render[l, col]
							nl[]
						}
					end
					render["#{Expression[curaddr]}   ", :address] if @show_addresses
					render[di.instruction.to_s.ljust(di.comment ? 18 : 0), :instruction]
					render[' ; ' + di.comment.join(' ')[0, 64], :comment] if di.comment
					nl[]
				else
					# TODO real data display (dwords, xrefs, strings..)
					if label = @dasm.get_label_at(curaddr)
						render[label + ' ', :label]
					end
					s = @dasm.get_section_at(curaddr)
					render['db '+((s and s[0].data.length > s[0].ptr) ? Expression[s[0].read(1)[0]].to_s : '?'), :text]
					nl[]
				end
			}
			b.w = b[:line_text_col].map { |strc| strc.map { |s, c| s }.join.length }.max.to_i * @font_width + 2
			b.w += 1 if b.w % 2 == 0	# ensure boxes have odd width -> vertical arrows are straight
			b.h = line * @font_height
		}
	end

	def keypress_ctrl(key)
		case key
		when ?F
			@parent_widget.inputbox('text to search in curview (regex)', :text => @hl_word) { |pat|
				re = /#{pat}/i
				list = [['addr', 'instr']]
				@curcontext.box.each { |b|
					b[:line_text_col].zip(b[:line_address]) { |l, a|
						str = l.map { |s, c| s }.join
						list << [Expression[a], str] if str =~ re
					}
				}
				@parent_widget.list_bghilight("search result for /#{pat}/i", list) { |i| @parent_widget.focus_addr i[0] }
			}
		when ?+; mouse_wheel_ctrl(:up, width/2, height/2)
		when ?-; mouse_wheel_ctrl(:down, width/2, height/2)
		else return false
		end
		true
	end

	def keypress(key)
		case key
		when :left
			if @caret_box
				if @caret_x > 0
					@caret_x -= 1
					update_caret
				elsif b = @curcontext.box.sort_by { |b_| -b_.x }.find { |b_| b_.x < @caret_box.x and
						b_.y < @caret_box.y+@caret_y*@font_height and
						b_.y+b_.h > @caret_box.y+(@caret_y+1)*@font_height }
					@caret_x = (b.w/@font_width).to_i
					@caret_y += ((@caret_box.y-b.y)/@font_height).to_i
					@caret_box = b
					update_caret
					redraw
				else
					@curcontext.view_x -= 20/@zoom
					redraw
				end
			else
				@curcontext.view_x -= 20/@zoom
				redraw
			end
		when :up
			if @caret_box
				if @caret_y > 0
					@caret_y -= 1
					update_caret
				elsif b = @curcontext.box.sort_by { |b_| -b_.y }.find { |b_| b_.y < @caret_box.y and
						b_.x < @caret_box.x+@caret_x*@font_width and
						b_.x+b_.w > @caret_box.x+(@caret_x+1)*@font_width }
					@caret_x += ((@caret_box.x-b.x)/@font_width).to_i
					@caret_y = b[:line_address].length-1
					@caret_box = b
					update_caret
					redraw
				else
					@curcontext.view_y -= 20/@zoom
					redraw
				end
			else
				@curcontext.view_y -= 20/@zoom
				redraw
			end
		when :right
			if @caret_box
				if @caret_x <= @caret_box[:line_text_col].map { |s| s.map { |ss, cc| ss }.join.length }.max
					@caret_x += 1
					update_caret
				elsif b = @curcontext.box.sort_by { |b_| b_.x }.find { |b_| b_.x > @caret_box.x and
						b_.y < @caret_box.y+@caret_y*@font_height and
						b_.y+b_.h > @caret_box.y+(@caret_y+1)*@font_height }
					@caret_x = 0
					@caret_y += ((@caret_box.y-b.y)/@font_height).to_i
					@caret_box = b
					update_caret
					redraw
				else
					@curcontext.view_x += 20/@zoom
					redraw
				end
			else
				@curcontext.view_x += 20/@zoom
				redraw
			end
		when :down
			if @caret_box
				if @caret_y < @caret_box[:line_address].length-1
					@caret_y += 1
					update_caret
				elsif b = @curcontext.box.sort_by { |b_| b_.y }.find { |b_| b_.y > @caret_box.y and
						b_.x < @caret_box.x+@caret_x*@font_width and
						b_.x+b_.w > @caret_box.x+(@caret_x+1)*@font_width }
					@caret_x += ((@caret_box.x-b.x)/@font_width).to_i
					@caret_y = 0
					@caret_box = b
					update_caret
					redraw
				else
					@curcontext.view_y += 20/@zoom
					redraw
				end
			else
				@curcontext.view_y += 20/@zoom
				redraw
			end
		when :pgup
			if @caret_box
				@caret_y -= (height/4/@zoom/@font_height).to_i
				@caret_y = 0 if @caret_y < 0
				update_caret(false)
			else
				@curcontext.view_y -= height/4/@zoom
				redraw
			end
		when :pgdown
			if @caret_box
				@caret_y += (height/4/@zoom/@font_height).to_i
				@caret_y = [@caret_box[:line_address].length-1, @caret_y].min
				update_caret(false)
			else
				@curcontext.view_y += height/4/@zoom
				redraw
			end
		when :home
			if @caret_box
				@caret_x = 0
				update_caret(false)
			else
				@curcontext.view_x = @curcontext.box.map { |b_| b_.x }.min-10
				@curcontext.view_y = @curcontext.box.map { |b_| b_.y }.min-10
				redraw
			end
		when :end
			if @caret_box
				@caret_x = @caret_box[:line_text_col][@caret_y].to_a.map { |ss, cc| ss }.join.length
				update_caret(false)
			else
				@curcontext.view_x = [@curcontext.box.map { |b_| b_.x+b_.w }.max-width/@zoom+10, @curcontext.box.map { |b_| b_.x }.min-10].max
				@curcontext.view_y = [@curcontext.box.map { |b_| b_.y+b_.h }.max-height/@zoom+10, @curcontext.box.map { |b_| b_.y }.min-10].max
				redraw
			end

		when :delete
			@selected_boxes.each { |b_|
				@curcontext.box.delete b_
				b_.from.each { |bb| bb.to.delete b_ }
				b_.to.each { |bb| bb.from.delete b_ }
			}
			redraw
		when :popupmenu
			if @caret_box
				cx = (@caret_box.x - view_x + 1 + @caret_x*@font_width)*@zoom
				cy = (@caret_box.y - view_y + 1 + @caret_y*@font_height)*@zoom
				rightclick(cx, cy)
			end

		when ?a
			t0 = Time.now
			puts 'autoarrange'
			@curcontext.auto_arrange_boxes
			redraw
			puts 'autoarrange done %.02f' % (Time.now - t0)
		when ?u
			gui_update

		when ?R
			load __FILE__
		when ?I	# create arbitrary boxes/links
			if @selected_boxes.empty?
				@fakebox ||= 0
				b = @curcontext.new_box "id_#@fakebox",
					:addresses => [], :line_address => [],
					:line_text_col => [[["  blublu #@fakebox", :text]]]
				b.w = @font_width * 15
				b.h = @font_height * 2
				b.x = rand(200) - 100
				b.y = rand(200) - 100

				@fakebox += 1
			else
				b1, *bl = @selected_boxes
				bl = [b1] if bl.empty?	# loop
				bl.each { |b2|
					if b1.to.include? b2
						b1.to.delete b2
						b2.from.delete b1
					else
						b1.to << b2
						b2.from << b1
					end
				}
			end
			redraw

		when ?1	# (numeric) zoom to 1:1
			if @zoom == 1.0
				zoom_all
			else
				@curcontext.view_x += (width/2 / @zoom - width/2)
				@curcontext.view_y += (height/2 / @zoom - height/2)
				@zoom = 1.0
			end
			redraw
		when :insert		# split curbox at @caret_y
			if @caret_box and a = @caret_box[:line_address][@caret_y] and @dasm.decoded[a]
				@dasm.split_block(a)
				@curcontext.keep_split ||= []
				@curcontext.keep_split |= [a]
				gui_update
				focus_addr a
			end
		else return false
		end
		true
	end

	def hide_non_descendants(list)
		reach = {}
		todo = list.dup
		while b = todo.pop
			next if reach[b]
			reach[b] = true
			b.to.each { |bb|
				todo << bb if bb.y+bb.h >= b.y
			}
		end

		@curcontext.box.delete_if { |bb|
			!reach[bb]
		}
		@curcontext.box.each { |bb|
			bb.from.delete_if { |bbb| !reach[bbb] }
			bb.to.delete_if { |bbb| !reach[bbb] }
		}
		redraw
	end

	def hide_non_ascendants(list)
		reach = {}
		todo = list.dup
		while b = todo.pop
			next if reach[b]
			reach[b] = true
			b.from.each { |bb|
				todo << bb if bb.y <= b.h+b.y
			}
		end

		@curcontext.box.delete_if { |bb|
			!reach[bb]
		}
		@curcontext.box.each { |bb|
			bb.from.delete_if { |bbb| !reach[bbb] }
			bb.to.delete_if { |bbb| !reach[bbb] }
		}
		redraw
	end

	# find a suitable array of graph roots, walking up from a block (function start/entrypoint)
	def dasm_find_roots(addr)
		todo = [addr]
		done = []
		roots = []
		default_root = nil
		while a = todo.shift
			next if not di = @dasm.di_at(a)
			b = di.block
			a = b.address
			if done.include? a
				default_root ||= a
				next
			end
			done << a
			newf = []
			b.each_from_samefunc(@dasm) { |f| newf << f }
			if newf.empty?
				roots << b.address
			else
				todo.concat newf
			end
		end
		roots << default_root if roots.empty? and default_root

		roots
	end

	def set_cursor_pos(p)
		addr, x = p
		focus_addr(addr)
		@caret_x = x
		update_caret
	end

	def get_cursor_pos
		[current_address, @caret_x]
	end

	# focus on addr
	# addr may be a dasm label, dasm address, dasm address in string form (eg "0DEADBEEFh")
	# addr must point to a decodedinstruction
	# if the addr is not found in curcontext, the code flow is walked up until a function
	# start or an entrypoint is found, then the graph is created from there
	# will call gui_update then
	def focus_addr(addr, can_update_context=true)
		return if @parent_widget and not addr = @parent_widget.normalize(addr)
		return if not @dasm.di_at(addr)

		# move window / change curcontext
		if b = @curcontext.box.find { |b_| b_[:line_address].index(addr) }
			@caret_box, @caret_x, @caret_y = b, 0, b[:line_address].rindex(addr)
			@curcontext.view_x += (width/2 / @zoom - width/2)
			@curcontext.view_y += (height/2 / @zoom - height/2)
			@zoom = 1.0

			update_caret
		elsif can_update_context
			@curcontext = Graph.new 'testic'
			@curcontext.root_addrs = dasm_find_roots(addr)
			@want_focus_addr = addr
			gui_update
		else
			return
		end
		true
	end

	def focus_xy(x, y)
		# dont move during a click
		return if @mousemove_origin

		# ensure the caret stays onscreen
		if not view_x
			@curcontext.view_x = x - width/5/@zoom
			redraw
		elsif @caret_box and @caret_box.w < width*27/30/@zoom
			# keep @caret_box full if possible
			if view_x + width/20/@zoom > @caret_box.x
				@curcontext.view_x = @caret_box.x-width/20/@zoom
			elsif view_x + width*9/10/@zoom < @caret_box.x+@caret_box.w
				@curcontext.view_x = @caret_box.x+@caret_box.w-width*9/10/@zoom
			end
		elsif view_x + width/20/@zoom > x
			@curcontext.view_x = x-width/20/@zoom
			redraw
		elsif view_x + width*9/10/@zoom < x
			@curcontext.view_x = x-width*9/10/@zoom
			redraw
		end

		if not view_y
			@curcontext.view_y = y - height/5/@zoom
			redraw
		elsif @caret_box and @caret_box.h < height*27/30/@zoom
			if view_y + height/20/@zoom > @caret_box.y
				@curcontext.view_y = @caret_box.y-height/20/@zoom
			elsif view_y + height*9/10/@zoom < @caret_box.y+@caret_box.h
				@curcontext.view_y = @caret_box.y+@caret_box.h-height*9/10/@zoom
			end
		elsif view_y + height/20/@zoom > y
			@curcontext.view_y = y-height/20/@zoom
			redraw
		elsif view_y + height*9/10/@zoom < y
			@curcontext.view_y = y-height*9/10/@zoom
			redraw
		end
	end

	# hint that the caret moved
	# redraw, change the hilighted word
	def update_caret(update_hlword = true)
		return if not b = @caret_box or not @caret_x or not l = @caret_box[:line_text_col][@caret_y]

		if update_hlword
			l = l.map { |s, c| s }.join
			@parent_widget.focus_changed_callback[] if @parent_widget and @parent_widget.focus_changed_callback and @oldcaret_y != @caret_y
			update_hl_word(l, @caret_x)
		end

		focus_xy(b.x + @caret_x*@font_width, b.y + @caret_y*@font_height)

		redraw
	end

	def current_address
		@caret_box ? @caret_box[:line_address][@caret_y] : @curcontext.root_addrs.first
	end
end
end
end
