#!/usr/bin/ruby
require 'Qt'
require 'set'
require 'socket'
require 'thread'

class KIpcClient < Qt::Object
  signals 'newMsg(const QString&)'

	def initialize(parent, serverName = '127.0.0.1', port = 9090)
    super parent
		@serverName = serverName
    @port = port
   
    @serverThread = Thread.new do
      server = TCPServer.new(9091)
      loop {
        client = server.accept
        msg = client.gets
        client.close
        emit newMsg(msg)
      }
      server.close
    end
	end

	def call(msg)
    s = TCPSocket.open('127.0.0.1', 9090)
    s.puts msg
    s.close
  end

end

class DbgEditArea < Qt::PlainTextEdit
  class SideWidget < Qt::Widget
    def initialize parent, mw, file
      super parent
      parent.connect SIGNAL('updateRequest(const QRect &,int)') do
        self.update
      end
      @breaks = Array.new
      @mw = mw
      @file = file
    end

    private def enumVisibleBlock(&visitor)
      te = parent
      block = te.firstVisibleBlock
      while block.isValid
        rc = te.blockBoundingGeometry(block).translated(te.contentOffset)
        rc.y += 4
        visitor.call block, rc
        block = block.next
      end
    end

    def paintEvent e
      p = Qt::Painter.new self

      enumVisibleBlock do |block, rc|
        break if rc.top > e.rect.bottom
        rc.setWidth self.rect.width - 2

        if @mw.match_break?(@file, block.blockNumber + 1)
          p.setBrush Qt::Brush.new(Qt::red,Qt::SolidPattern)
          p.drawEllipse rc.right - 30, rc.y, 10, 10
        end
        p.drawText rc, Qt::AlignRight | Qt::AlignTop, (block.blockNumber + 1).to_s
      end

      p.end()
    end
    
    def mouseDoubleClickEvent e
      enumVisibleBlock do |block, rc|
        break if rc.top > e.y
        if rc.top <= e.y and rc.bottom >= e.y
          if @mw.match_break?(@file, block.blockNumber + 1)
            @mw.set_break(@file, block.blockNumber + 1, false)
          else
            @mw.set_break(@file, block.blockNumber + 1, true)
          end
        update
        end
      end
      super
    end

  end

  def initialize fpath, mw
    super 
    cont = File.read(fpath)
    setPlainText cont
    setReadOnly true
    setTabStopWidth 12

    @hl = -1
    setFont Qt::Font.new "monospace"

    @side = SideWidget.new self, mw, fpath
    
    @inject_label = Qt::Label.new "&Inject:", self
    @inject_label.setFixedWidth 50
    @inject_label.setScaledContents true
    @inject_editor = Qt::LineEdit.new self
    @inject_label.setBuddy @inject_editor
    @bottom = Qt::HBoxLayout.new 
    @bottom.setSpacing 3
    @bottom.setContentsMargins 4, 4, 4, 4
    @bottom.addWidget @inject_label
    @bottom.addWidget @inject_editor

    setViewportMargins 40, 0, 0, @bottom.minimumSize.height


    MyShortcut.new "Esc", self do
      @inject_label.setText "&Inject:"
    end

    MyShortcut.new "F3", self do
      return if @inject_label.text != "&Find:" or @inject_editor.text.empty?
      window.find @inject_editor.text
    end
 
    {
      "Ctrl+F" => "&Find:",
      "Ctrl+G" => "&Line:",
      "F4" => @inject_label.text,
    }.each do |k, v| 
      MyShortcut.new k, self do
        @inject_label.setText v
        @inject_editor.setFocus
        @inject_editor.selectAll
      end
    end

    @inject_editor.connect SIGNAL('returnPressed()') do
      begin
        case @inject_label.text
        when "&Inject:"
          window.do_eval @inject_editor.text
        when "&Find:"
          window.find @inject_editor.text
        when "&Line:"
          window.locate_line @inject_editor.text
        end
      rescue => e
        window.message e
      end
      @inject_editor.selectAll
    end
  end
  def resizeEvent e
    @side.setGeometry 0, 0, 40, e.size.height

    rc = rect
    rc.setTop rc.height - @bottom.minimumSize.height
    @bottom.setGeometry rc
    super
  end
  def setHighLight lineno
    @hl = lineno
    if @hl != -1
      es = Qt::TextEdit::ExtraSelection.new
      hl_cur = Qt::TextCursor.new document.findBlockByLineNumber(@hl)
      es.cursor = hl_cur
      es.format.setBackground(Qt::Brush.new(Qt::Color.new(Qt::yellow).lighter(160)))
      es.format.setProperty(Qt::TextFormat::FullWidthSelection, Qt::Variant.new(true))
      setExtraSelections([es])

      setTextCursor hl_cur
      ensureCursorVisible
    else
      setExtraSelections []
    end
  end
end

class MyShortcut < Qt::Shortcut
  def initialize key, target, &block
    super Qt::KeySequence.new(key), target, &nil
    connect self, SIGNAL('activated()'), &block
  end
end

class VariablesDock < Qt::DockWidget
  class VariableTreeWidgetItem  < Qt::TreeWidgetItem
    def initialize parent, var_name, val, val_type
      @var_name = var_name
      @val = val
      super parent, [var_name, val, val_type]
      self.setToolTip 1, val
    end
    def subVals var
      if var.respond_to? :each_pair
        var.each_pair.collect do |k, v|
          [k.inspect, v]
        end
      elsif var.respond_to? :each 
        var.each.with_index.collect do |v, i|
          [i.to_s, v]
        end
      else
        var.instance_variables.collect do |m|
          [m.to_s, var.instance_variable_get(m)]
        end
      end
    end
    def loadSubVars
      subVals(@val).each do |x|
        VariableTreeWidgetItem.new self, x[0], x[1]
      end
    end
  end


  def initialize mw
    super "Variables", nil
    @mw = mw
    mw.addDockWidget(Qt::LeftDockWidgetArea, self)

    @lw = Qt::TreeWidget.new self
    labels = []
    labels << "Name" << "Value" << "Type"
    @lw.setHeaderLabels labels 
    setWidget(@lw)
  end
  def update
    @lw.clear
    return if @mw.variables.nil?
    local_vars = @mw.variables.split(';')
    local_vars.each do |val|
      var_info = val.split('|')
      VariableTreeWidgetItem.new @lw, var_info[0], var_info[1], var_info[2]
    end
  end
end

class StackDock < Qt::DockWidget
  def initialize mw
    super "Stack", nil
    @mw = mw
    mw.addDockWidget(Qt::LeftDockWidgetArea, self)

    @lw = Qt::ListWidget.new self
    setWidget(@lw)

    @lw.connect SIGNAL('itemActivated(QListWidgetItem*)') do |item|
      text = item.text
      if text =~ /^(.*?):(\d*):(.*)$/
        @mw.open $1, $2.to_i
      end
    end
  end
  def update
    @lw.clear
    return if @mw.stacks.empty?
    @mw.stacks.each do |stack|
      @lw.addItem stack
    end
  end
end

class MessageDock < Qt::DockWidget
  def initialize mw
    super "Message", nil
    @mw = mw
    mw.addDockWidget(Qt::BottomDockWidgetArea, self)

    @te = Qt::PlainTextEdit.new self
    @te.setFont Qt::Font.new "monospace"
    @te.setReadOnly true
    @te.setCenterOnScroll true
    setWidget(@te)
  end
  def format obj
    if obj.is_a? Exception
      str = "#{obj.class}: #{obj.message}\n"
      obj.backtrace.each do |stack|
        if stack.index File.expand_path("rdbg.rb") + ":"
          break
        end
        str += "    from #{stack}\n"
      end
      str
    else
      obj.to_s
    end
  end
  def message msg
    return if msg == nil
    msg_raw = format msg
    @te.appendPlainText msg_raw
    @te.ensureCursorVisible
  end
  def message_raw msg
    msg_raw = msg
    msg_raw += "\n" if msg_raw !~ /\n$/
    @te.insertPlainText msg_raw.to_s
    @te.ensureCursorVisible
  end
end

class DbgMainWindow < Qt::MainWindow
  slots 'onNewMsg(const QString&)'

  def initialize
    super
    @mdi = Qt::MdiArea.new 
    @mdi.setViewMode Qt::MdiArea::TabbedView
    setCentralWidget(@mdi)
    @files = {}
    @docks = []
    @variables = nil
    @stacks = []
    @msg_dock = MessageDock.new(self)
    @menu_bar = Qt::MenuBar.new(self)
    @breaks = {}
    @ipcclient = KIpcClient.new(self)
    connect(@ipcclient, SIGNAL('newMsg(const QString&)'),
      self , SLOT('onNewMsg(const QString&)'))
    @timer = Qt::Timer.new(self)

    menu = @menu_bar.addMenu("&File")
    connect menu.addAction("Open"), SIGNAL('triggered(bool)') do
    	self.openFile
    end
    setMenuBar(@menu_bar)

    MyShortcut.new "F10", self do step_over end
    MyShortcut.new "F11", self do step_into end
    MyShortcut.new "Shift+F11", self do step_out end
    MyShortcut.new "F5", self do run end
    MyShortcut.new "Shift+F5", self do terminate end
    MyShortcut.new "Ctrl+O", self do openFile end

    @docks << VariablesDock.new(self) << StackDock.new(self)
    @docks << @msg_dock
  end

  def onNewMsg msg
    @timer.stop
    if msg
      update_dbginfo(eval msg)
    end
  end
  
  def closeEvent e
    terminate
  end

  def event e
    super e
  end

  def openFile
    fileName = Qt::FileDialog::getOpenFileName(self, "Open File",
                                                   "C:",
                                                   "Ruby files (*.rb *.rbx)")
    debug fileName
  end

  attr_reader :variables
  attr_reader :stacks

  def open fpath, lineno = -1
    if @files[fpath]
      @mdi.setActiveSubWindow(@files[fpath])
    else
      sw = @mdi.addSubWindow(DbgEditArea.new fpath, self)
      sw.showMaximized
      sw.setWindowTitle fpath
      @files[fpath] = sw
    end
    @files[fpath].widget.setHighLight(lineno)
  end
  def debug fpath
    command = {:command => :debug, :params => fpath}.to_s
    @ipcclient.call command
    open fpath
  end
  def terminate
    command = {:command => :terminate, :params => nil}.to_s
    @ipcclient.call command
    $kxApp.quit
  end
  def run
    command = {:command => :run, :params => nil}.to_s
    @ipcclient.call command
    @timer.start 50
    @files.each_value do |file|
      file.widget.setHighLight(-1)
    end
  end

  def update_dbginfo(response)
    if response[:status] == :raised
      message response[:last_exception]
    end
    
    [:result, :outputs].each do |field|
      if !response[field].nil? 
        puts response[field].to_s
      end
    end
    @variables = response[:variables]
    @stacks = response[:stacks]
    
    if response[:status] == :terminate
      msg = Qt::MessageBox.new nil, "Hint", "Debug finished!"
      msg.show
      puts "<<<<<<<<<debug finished!>>>>>>>>>>>"
    elsif response[:status] == :breaked
      open response[:fpath], response[:lineno] - 1
    else
      open @fpath, -1
    end

    @docks.each do |d|
      d.update
    end    
  end


  def self.define_debug_method(name)
    define_method(name) do |param = nil|
      command = {:command => name, :params => param }.to_s
      @ipcclient.call(command)
      @timer.start 20
    end
  end

  define_debug_method :step_over
  define_debug_method :step_into
  define_debug_method :step_out
  define_debug_method :do_eval
  define_debug_method :peek
  define_debug_method :clear_last_exception

  def set_break(file, lineno, bset)
    if !@breaks.include?(file)
      @breaks[file] = []
    end
    linenos = @breaks[file]
    
    if bset and !linenos.include?(lineno)
      linenos << lineno
    else
      if !bset and linenos.include?(lineno)
        linenos.delete lineno
      end
    end
    command = {
      :command => :set_break,
      :file => file,
      :lineno =>lineno,
      :bset =>bset, }.to_s
    @ipcclient.call(command)
  end

  def match_break? fpath, lineno
    return false if !@breaks.include?(fpath)
    linenos = @breaks[fpath]
    return false if !linenos.include?(lineno)
    true
  end

  def locate_line lineno
    return if @mdi.activeSubWindow.nil?
    textEdit = @mdi.activeSubWindow.widget
    return if textEdit.nil?
    textBlock = textEdit.document.findBlockByLineNumber(lineno.to_i - 1)
    if textBlock.isValid
      cursor = Qt::TextCursor.new textBlock
      textEdit.setTextCursor cursor
      textEdit.centerOnScroll
    end
  end

  def find text
    return if @mdi.activeSubWindow.nil?
    @mdi.activeSubWindow.widget.find text, 0
  end

  def message msg
    @msg_dock.message msg
  end
  def message_raw msg
    @msg_dock.message_raw msg
    @msg_dock.upate
  end
end

@dbgmw = DbgMainWindow.new
@dbgmw.show
$kxLogger.install_receiver &@dbgmw.method(:message_raw)

#kso_plugin

#3B0778023BC116C653628EE20EFBD6C685AAE3F1682557CCAC8544E78F3281C840466E6D7067D2D56A0EEC0C485964668A1F183A04A3BAA974E15B02A650D787

#3B611170AD172381312379D41F5241472E7F234C30DD9C73B4DFB8C2989C58535613A15785846E70B8F1EC56CA6A805788A7202ACAA5BA8974FB33A7B6A7C80D