#    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 CdecompListingWidget < DrawableWidget
	attr_accessor :dasm, :curaddr, :tabwidth

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

		@view_x = @view_y = 0	# coord of corner of view in characters
		@cwidth = @cheight = 1	# widget size in chars
		@line_text = []
		@line_text_col = []	# each line is [[:col, 'text'], [:col, 'text']]
		@curaddr = nil
		@tabwidth = 8

		@default_color_association = { :text => :black, :keyword => :blue, :caret => :black,
			  :background => :white, :hl_word => :palered, :localvar => :darkred,
			  :globalvar => :darkgreen, :intrinsic => :darkyellow }
	end

	def curfunc
		@dasm.c_parser and (@dasm.c_parser.toplevel.symbol[@curaddr] or @dasm.c_parser.toplevel.struct[@curaddr])
	end

	def click(x, y)
		@caret_x = (x-1).to_i / @font_width + @view_x
		@caret_y = y.to_i / @font_height + @view_y
		update_caret
	end

	def rightclick(x, y)
		click(x, y)
		if @dasm.c_parser and @dasm.c_parser.toplevel.symbol[@hl_word]
			@parent_widget.clone_window(@hl_word, :decompile)
		elsif @hl_word
			@parent_widget.clone_window(@hl_word)
		end
	end

	def doubleclick(x, y)
		click(x, y)
		@parent_widget.focus_addr(@hl_word)
	end

	def mouse_wheel(dir, x, y)
		case dir
		when :up
			if @caret_y > 0
				@view_y -= 4
				@caret_y -= 4
				@caret_y = 0 if @caret_y < 0
			end
		when :down
			if @caret_y < @line_text.length - 1
				@view_y += 4
				@caret_y += 4
				redraw
			end
		end
		redraw
	end

	def paint
		@cwidth = width/@font_width
		@cheight = height/@font_height

		# adjust viewport to cursor
		sz_x = @line_text.map { |l| l.length }.max.to_i + 1
		sz_y = @line_text.length.to_i + 1
		@view_x = @caret_x - @cwidth + 1 if @caret_x > @view_x + @cwidth - 1
		@view_x = @caret_x if @caret_x < @view_x
		@view_x = sz_x - @cwidth - 1 if @view_x >= sz_x - @cwidth
		@view_x = 0 if @view_x < 0

		@view_y = @caret_y - @cheight + 1 if @caret_y > @view_y + @cheight - 1
		@view_y = @caret_y if @caret_y < @view_y
		@view_y = sz_y - @cheight - 1 if @view_y >= sz_y - @cheight
		@view_y = 0 if @view_y < 0

		# current cursor position
		x = 1
		y = 0

		# renders a string at current cursor position with a color
		# must not include newline
		render = lambda { |str, color|
			# function ends when we write under the bottom of the listing
			if @hl_word
				stmp = str
				pre_x = 0
				while stmp =~ /^(.*?)(\b#{Regexp.escape @hl_word}\b)/
					s1, s2 = $1, $2
					pre_x += s1.length*@font_width
					hl_w = s2.length*@font_width
					draw_rectangle_color(:hl_word, x+pre_x, y, hl_w, @font_height)
					pre_x += hl_w
					stmp = stmp[s1.length+s2.length..-1]
				end
			end
			draw_string_color(color, x, y, str)
			x += str.length * @font_width
		}

		@line_text_col[@view_y, @cheight + 1].each { |l|
			cx = 0
			l.each { |c, t|
				cx += t.length
				if cx-t.length > @view_x + @cwidth + 1
				elsif cx < @view_x
				else
					t = t[(@view_x - cx + t.length)..-1] if cx-t.length < @view_x
					render[t, c]
				end
			}
			x = 1
			y += @font_height
		}

		if focus?
			# draw caret
			cx = (@caret_x-@view_x)*@font_width+1
			cy = (@caret_y-@view_y)*@font_height
			draw_line_color(:caret, cx, cy, cx, cy+@font_height-1)
		end
	
		@oldcaret_x, @oldcaret_y = @caret_x, @caret_y
	end

	def keypress(key)
		case key
		when :left
			if @caret_x >= 1
				@caret_x -= 1
				update_caret
			end
		when :up
			if @caret_y > 0
				@caret_y -= 1
				update_caret
			end
		when :right
			if @caret_x < @line_text[@caret_y].to_s.length
				@caret_x += 1
				update_caret
			end
		when :down
			if @caret_y < @line_text.length
				@caret_y += 1
				update_caret
			end
		when :home
			@caret_x = @line_text[@caret_y].to_s[/^\s*/].length
			update_caret
		when :end
			@caret_x = @line_text[@caret_y].to_s.length
			update_caret
		when ?n	# rename local/global variable
			f = curfunc.initializer if curfunc and curfunc.initializer.kind_of? C::Block
			n = @hl_word
			if (f and f.symbol[n]) or @dasm.c_parser.toplevel.symbol[n]
				@parent_widget.inputbox("new name for #{n}", :text => n) { |v|
					if v !~ /^[a-z_$][a-z_0-9$]*$/i
						@parent_widget.messagebox("invalid name #{v.inspect} !")
						next
					end
					if f and f.symbol[n]
						# TODO add/update comment to the asm instrs
						s = f.symbol[v] = f.symbol.delete(n)
						s.name = v
						f.decompdata[:stackoff_name][s.stackoff] = v if s.stackoff
					elsif @dasm.c_parser.toplevel.symbol[n]
						@dasm.rename_label(n, v)
						@curaddr = v if @curaddr == n                   
					end
					gui_update
				}
			end
		when ?r # redecompile
			@parent_widget.decompile(@curaddr)
		when ?t	# change variable type (you'll want to redecompile after that)
			f = curfunc.initializer if curfunc.kind_of? C::Variable and curfunc.initializer.kind_of? C::Block
			n = @hl_word
			cp = @dasm.c_parser
			if (f and s = f.symbol[n]) or s = cp.toplevel.symbol[n] or s = cp.toplevel.symbol[@curaddr]
				s_ = s.dup
				s_.initializer = nil if s.kind_of? C::Variable	# for static var, avoid dumping the initializer in the textbox
				s_.attributes &= C::Attributes::DECLSPECS if s_.attributes
				@parent_widget.inputbox("new type for #{s.name}", :text => s_.dump_def(cp.toplevel)[0].join(' ')) { |t|
					if t == ''
						if s.type.kind_of? C::Function and s.initializer and s.initializer.decompdata
							s.initializer.decompdata[:stackoff_type].clear
							s.initializer.decompdata.delete :return_type
						elsif s.kind_of? C::Variable and s.stackoff
							f.decompdata[:stackoff_type].delete s.stackoff
						end
						next
					end
					begin
						cp.lexer.feed(t)
						raise 'bad type' if not v = C::Variable.parse_type(cp, cp.toplevel, true)
						v.parse_declarator(cp, cp.toplevel)
						if s.type.kind_of? C::Function and s.initializer and s.initializer.decompdata
							# updated type of a decompiled func: update stack
							vt = v.type.untypedef
							vt = vt.type.untypedef if vt.kind_of? C::Pointer
							raise 'function forever !' if not vt.kind_of? C::Function
							# TODO _declspec
							ao = 1
							vt.args.to_a.each { |a|
								next if a.has_attribute_var('register')
								ao = (ao + [cp.sizeof(a), cp.typesize[:ptr]].max - 1) / cp.typesize[:ptr] * cp.typesize[:ptr]
								s.initializer.decompdata[:stackoff_name][ao] = a.name if a.name
								s.initializer.decompdata[:stackoff_type][ao] = a.type
								ao += cp.sizeof(a)
							}
							s.initializer.decompdata[:return_type] = vt.type
							s.type = v.type
						else
							f.decompdata[:stackoff_type][s.stackoff] = v.type if f and s.kind_of? C::Variable and s.stackoff
							s.type = v.type
						end
						gui_update
					rescue Object
						@parent_widget.messagebox([$!.message, $!.backtrace].join("\n"), "error")
					end
					cp.readtok until cp.eos?
				}
			end
		else return false
		end
		true
	end

	def get_cursor_pos
		[@curaddr, @caret_x, @caret_y]
	end

	def set_cursor_pos(p)
		focus_addr p[0]
		@caret_x, @caret_y = p[1, 2]
		update_caret
	end

	# hint that the caret moved
	# redraws the caret, change the hilighted word, redraw if needed
	def update_caret
		redraw if @caret_x < @view_x or @caret_x >= @view_x + @cwidth or @caret_y < @view_y or @caret_y >= @view_y + @cheight

		invalidate_caret(@oldcaret_x-@view_x, @oldcaret_y-@view_y)
		invalidate_caret(@caret_x-@view_x, @caret_y-@view_y)
		@oldcaret_x, @oldcaret_y = @caret_x, @caret_y

		redraw if update_hl_word(@line_text[@caret_y], @caret_x)
	end

	# focus on addr
	# returns true on success (address exists & decompiled)
	def focus_addr(addr)
		if @dasm.c_parser and (@dasm.c_parser.toplevel.symbol[addr] or @dasm.c_parser.toplevel.struct[addr])
			@curaddr = addr
			@caret_x = @caret_y = 0
			gui_update
			return true
		end

		return if not addr = @parent_widget.normalize(addr)

		# scan up to func start/entrypoint
		todo = [addr]
		done = []
		ep = @dasm.entrypoints.to_a.inject({}) { |h, e| h.update @dasm.normalize(e) => true }
		while addr = todo.pop
			next if not di = @dasm.di_at(addr)
			addr = di.block.address
			next if done.include?(addr) or not @dasm.di_at(addr)
			done << addr
			break if @dasm.function[addr] or ep[addr]
			empty = true
			@dasm.decoded[addr].block.each_from_samefunc(@dasm) { |na| empty = false ; todo << na }
			break if empty
		end
		@dasm.auto_label_at(addr, 'loc') if @dasm.get_section_at(addr) and not @dasm.get_label_at(addr)
		return if not l = @dasm.get_label_at(addr)
		@curaddr = l
		@caret_x = @caret_y = 0
		gui_update
		true
	end

	# returns the address of the data under the cursor
	def current_address
		@curaddr
	end

	def update_line_text
		@line_text = curfunc.dump_def(@dasm.c_parser.toplevel)[0].map { |l| l.gsub("\t", ' '*@tabwidth) }
		@line_text_col = []

		if f = curfunc and f.kind_of? C::Variable and f.initializer.kind_of? C::Block
			keyword_re = /\b(#{C::Keyword.keys.join('|')})\b/
			intrinsic_re = /\b(intrinsic_\w+)\b/
			lv = f.initializer.symbol.keys
			lv << '00' if lv.empty?
			localvar_re = /\b(#{lv.join('|')})\b/
			globalvar_re = /\b(#{f.initializer.outer.symbol.keys.join('|')})\b/
		end

		@line_text.each { |l|
			lc = []
			if f
				while l and l.length > 0
					if (i_k = (l =~ keyword_re)) == 0
						m = $1.length
						col = :keyword
					elsif (i_i = (l =~ intrinsic_re)) == 0
						m = $1.length
						col = :intrinsic
					elsif (i_l = (l =~ localvar_re)) == 0
						m = $1.length
						col = :localvar
					elsif (i_g = (l =~ globalvar_re)) == 0
						m = $1.length
						col = :globalvar
					else
						m = ([i_k, i_i, i_l, i_g, l.length] - [nil, false]).min
						col = :text
					end
					lc << [col, l[0, m]]
					l = l[m..-1]
				end
			else
				lc << [:text, l]
			end
			@line_text_col << lc
		}
	end

	def gui_update
		if not curfunc and not @decompiling ||= false
			@line_text = ['please wait']
			@line_text_col = [[[:text, 'please wait']]]
			redraw
			@decompiling = true
			@dasm.decompile_func(@curaddr)
			@decompiling = false
		end
		if curfunc
			update_line_text
			update_caret
		end
		redraw
	end
end
end
end
