-- Copyright 2011-18 Paul Kulchenko, ZeroBrane LLC
-- Original authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
-- Integration with MobDebug
---------------------------------------------------------

local copas = require "copas"
local socket = require "socket"
local mobdebug = require "mobdebug"
local unpack = table.unpack or unpack

local ide = ide
local protodeb = setmetatable(ide:GetDebugger(), ide.proto.Debugger)
local debugger = protodeb
debugger.running = false -- true when the debuggee is running
debugger.listening = false -- true when the debugger is listening for a client
debugger.portnumber = ide.config.debugger.port or mobdebug.port -- the port # to use for debugging
debugger.watchCtrl = nil -- the watch ctrl that shows watch information
debugger.stackCtrl = nil -- the stack ctrl that shows stack information
debugger.toggleview = {
  bottomnotebook = true, -- output/console is "on" by default
  stackpanel = false, watchpanel = false, toolbar = false }
debugger.needrefresh = {} -- track components that may need a refresh
debugger.hostname = ide.config.debugger.hostname or (function()
  local hostname = socket.dns.gethostname()
  return hostname and socket.dns.toip(hostname) and hostname or "localhost"
end)()
debugger.imglist = ide:CreateImageList("STACK", "VALUE-CALL", "VALUE-LOCAL", "VALUE-UP", "VALUE-TABLE", "VALUE-FUNCTION", "VALUE-OBJECT", "VALUE-STRING", "VALUE-NUMBER", "VALUE-INSTANCE", "VALUE-MODULE", "VALUE-BOOL", "VALUE-PROPERTY")
local type_img = { ["table"] = 3, ["Object"] = 5, ["function"] = 4, ["string"] = 6, ["number"] = 7, ['instance'] = 8, ['module'] = 1, ["boolean"] = 10, ["property"] = 11, ["nil"] = -1 }
local image = { STACK = 0, LOCAL = 1, UPVALUE = 2, TABLE = 3, FUNCTION = 4, OBJECT = 5, STRING = 6, NUMBER = 7, INSTANCE = 8, MODULE = 9, BOOL = 10, PROPERTY = 11}
local notebook = ide.frame.notebook

local CURRENT_LINE_MARKER = StylesGetMarker("currentline")
local CURRENT_LINE_MARKER_VALUE = 2^CURRENT_LINE_MARKER
local BREAKPOINT_MARKER = StylesGetMarker("breakpoint")
local BREAKPOINT_MARKER_VALUE = 2^BREAKPOINT_MARKER

local activate = {CHECKONLY = "checkonly", NOREPORT = "noreport", CLEARALL = "clearall"}

local function serialize(value, options) return mobdebug.line(value, options) end

local function displayError(...) return ide:GetOutput():Error(...) end

local function fixUTF8(...)
  local t = {...}
  -- convert to escaped decimal code as these can only appear in strings
  local function fix(s) return '\\'..string.byte(s) end
  for i = 1, #t do t[i] = FixUTF8(t[i], fix) end
  return unpack(t)
end

local q = EscapeMagic
local MORE = "{...}"

function debugger:init(init)
  local o = {}
  -- merge known self and init values
  for k, v in pairs(self) do o[k] = v end
  for k, v in pairs(init or {}) do o[k] = v end
  return setmetatable(o, {__index = protodeb})
end

function debugger:updateWatchesSync()
  local debugger = self
  local watchCtrl = debugger.watchCtrl
  local pane = ide.frame.uimgr:GetPane("watchpanel")
  local shown = watchCtrl and (pane:IsOk() and pane:IsShown() or not pane:IsOk() and watchCtrl:IsShown())
  local canupdate = (debugger.server and not debugger.running and not debugger.scratchpad
    and not (debugger.options or {}).noeval)
  if shown and canupdate then
    local root = watchCtrl:GetRootItem()
    if not root or not root:IsOk() then return end

    local params = debugger:GetDataOptions({maxlength=false})
    local item = onlyitem or watchCtrl:GetFirstChild(root)
    while true do
      if not item:IsOk() then break end
      local expression = watchCtrl:GetItemExpression(item)
      if expression then
        _, values, err = debugger:evaluate(expression, {maxlevel = 1})
        local curchildren = watchCtrl:GetItemChildren(item)
        if err then
          err = err:gsub("%[.-%]:%d+:%s+","")
          watchCtrl:SetItemValueIfExpandable(item, nil)
        else
          if #values == 0 then values = {'nil'} end
          local _, res = LoadSafe("return "..values[1])
          watchCtrl:SetItemValueIfExpandable(item, res)
        end
        local valstr = table.concat(values, ", ")
        local temp = valstr:gsub("%{.*}", "")
        local image
        local complex = false
        local _type = valstr:match("%[%[(%w+)%: ")
        if _type ~= nil then
          if _type == "table" then            
            _type = debugger:evaluate("getmetatable("..expression..").__name", {maxlevel = 1})
            if _type == nil then
              image = "table"
              _type = image
            else
              image = "Object"
              complex = true
            end
          elseif _type == "Object" then
            local _, values, err = debugger:evaluate("getmetatable("..expression..").__name", {maxlevel = 1})
            _type = values[1]
            if debugger:evaluate("getmetatable("..expression..").__call", {maxlevel = 1}) ~= nil then
              image = "Object"
            else
              image = "instance"
            end
            complex = true
          else
            _type = "module"
          end 
        else
          _type = debugger:evaluate("type("..valstr..")", {maxlevel = 1})
        end
        _type = _type ~= nil and _type:match('%w+') or _type
        local newval
        if complex then
          newval = fixUTF8(expression ..(err and (' error: '..err) or  ' <'.._type..'>'))
        elseif _type == "function" then
          newval = expression.."()"
        elseif _type == "table" or _type == "module" then
            newval = expression
        else
          newval = fixUTF8(expression .. ' = '.. (err and ('error: '..err) or valstr))
        end
        local val = watchCtrl:GetItemText(item)
        watchCtrl:SetItemText(item, newval)
        image = image or _type or "nil"
        watchCtrl:SetItemImage(item, type_img[image])
        if val ~= newval then
          local newchildren = watchCtrl:GetItemChildren(item)
          if next(curchildren) ~= nil and next(newchildren) == nil then
            watchCtrl:SetItemHasChildren(item, true)
            watchCtrl:CollapseAndReset(item)
            watchCtrl:SetItemHasChildren(item, false)
          elseif next(curchildren) ~= nil and next(newchildren) ~= nil then
            watchCtrl:CollapseAndReset(item)
            watchCtrl:Expand(item)
          end
        end
      end

      if onlyitem then break end
      item = watchCtrl:GetNextSibling(item)
    end
    debugger.needrefresh.watches = false
  elseif not shown and canupdate then
    debugger.needrefresh.watches = true
  end
end

local callData = {}

function debugger:updateStackSync()
  local debugger = self
  local stackCtrl = debugger.stackCtrl
  local pane = ide.frame.uimgr:GetPane("stackpanel")
  local shown = stackCtrl and (pane:IsOk() and pane:IsShown() or not pane:IsOk() and stackCtrl:IsShown())
  local canupdate = debugger.server and not debugger.running and not debugger.scratchpad
  if shown and canupdate then
    local stack, _, err = debugger:stack(debugger:GetDataOptions({maxlength=false}))
    if not stack or #stack == 0 then
      stackCtrl:DeleteAll()
      if err then -- report an error if any
        stackCtrl:AppendItem(stackCtrl:AddRoot("Stack"), "Error: " .. err, image.STACK)
      end
      return
    end
    stackCtrl:Freeze()
    stackCtrl:DeleteAll()

    local forceexpand = ide.config.debugger.maxdatalevel == 1
    local params = debugger:GetDataOptions({maxlevel=1})
    local maxlen = tonumber(ide.config.debugger.maxdatalength)

    local root = stackCtrl:AddRoot("Stack")
    callData = {} -- reset call cache
    for _,frame in ipairs(stack) do
      -- check if the stack includes expected structures
      if type(frame) ~= "table" or type(frame[1]) ~= "table" or #frame[1] < 7 then break end

      -- "main chunk at line 24"
      -- "foo() at line 13 (defined at foobar.lua:11)"
      -- call = { source.name, source.source, source.linedefined,
      --   source.currentline, source.what, source.namewhat, source.short_src }
      local call = frame[1]

      -- format the function name to a readable user string
      local func = call[5] == "main" and "main chunk"
        or call[5] == "C" and (call[1] or "C function")
        or call[5] == "tail" and "tail call"
        or (call[1] or "anonymous function")

      -- format the function treeitem text string, including the function name
      local text = func ..
        (call[4] == -1 and '' or " at line "..call[4]) ..
        (call[5] ~= "main" and call[5] ~= "Lua" and ''
         or (call[3] > 0 and " (defined at "..call[7]..":"..call[3]..")"
                          or " (defined in "..call[7]..")"))

      -- create the new tree item for this level of the call stack
      local callitem = stackCtrl:AppendItem(root, text, image.STACK)

      -- register call data to provide stack navigation
      callData[callitem:GetValue()] = { call[2], call[4] }

      -- add the local variables to the call stack item
      for name,val in pairs(type(frame[2]) == "table" and frame[2] or {}) do
        -- format the variable name, value as a single line and,
        -- if not a simple type, the string value.
        local value = val[1]
        if type(value) == "string" and maxlen and #value > maxlen then value = value:sub(1,maxlen) end
        local text = name--("%s = %s"):format(name, fixUTF8(serialize(value, params)))
        local image = stackCtrl:GetImageFromValue(value)
        local item = stackCtrl:AppendItem(callitem, text, image)
        stackCtrl:SetItemValueIfExpandable(item, value, forceexpand)
        stackCtrl:SetItemName(item, name)
      end

      -- add the upvalues for this call stack level to the tree item
      for name,val in pairs(type(frame[3]) == "table" and frame[3] or {}) do
        local value = val[1]
        if type(value) == "string" and maxlen and #value > maxlen then value = value:sub(1,maxlen) end
        local text = name--("%s = %s"):format(name, fixUTF8(serialize(value, params)))
        local image = stackCtrl:GetImageFromValue(value)
        local item = stackCtrl:AppendItem(callitem, text, image)
        stackCtrl:SetItemValueIfExpandable(item, value, forceexpand)
        stackCtrl:SetItemName(item, image > 5 and (name..' = '..value) or name)
      end

      stackCtrl:SortChildren(callitem)
      stackCtrl:Expand(callitem)
    end
    stackCtrl:EnsureVisible(stackCtrl:GetFirstChild(root))
    stackCtrl:Thaw()
    stackCtrl:SetScrollPos(wx.wxHORIZONTAL, 0, true)
    debugger.needrefresh.stack = false
  elseif not shown and canupdate then
    debugger.needrefresh.stack = true
  end
end

function debugger:updateStackAndWatches()
  local debugger = self
  -- check if the debugger is running and may be waiting for a response.
  -- allow that request to finish, otherwise this function does nothing.
  if debugger.running then debugger:Update() end
  if debugger.server and not debugger.running then
    copas.addthread(function()
        local debugger = debugger
        debugger:updateStackSync()
        debugger:updateWatchesSync()
      end)
  end
end

function debugger:updateWatches(item)
  local debugger = self
  -- check if the debugger is running and may be waiting for a response.
  -- allow that request to finish, otherwise this function does nothing.
  if debugger.running then debugger:Update() end
  if debugger.server and not debugger.running then
    copas.addthread(function()
        local debugger = debugger
        debugger:updateWatchesSync(item)
      end)
  end
end

function debugger:updateStack()
  local debugger = self
  -- check if the debugger is running and may be waiting for a response.
  -- allow that request to finish, otherwise this function does nothing.
  if debugger.running then debugger:Update() end
  if debugger.server and not debugger.running then
    copas.addthread(function()
        local debugger = debugger
        debugger:updateStackSync()
      end)
  end
end

function debugger:toggleViews(show)
  local debugger = self
  -- don't toggle if the current state is the same as the new one
  local shown = debugger.toggleview.shown
  if (show and shown) or (not show and not shown) then return end
  debugger.toggleview.shown = nil
  local mgr = ide.frame.uimgr
  local refresh = false
  for view, needed in pairs(debugger.toggleview) do
    local bar = view == 'toolbar'
    local pane = mgr:GetPane(view)
    if show then -- starting debugging and pane is not shown
      -- show toolbar during debugging if hidden and not fullscreen
      debugger.toggleview[view] = (not pane:IsShown()
        and (not bar or not ide.frame:IsFullScreen()))
      if debugger.toggleview[view] and (needed or bar) then
        pane:Show()
        refresh = true
      end
    else -- completing debugging and pane is shown
      debugger.toggleview[view] = pane:IsShown() and needed
      if debugger.toggleview[view] then
        pane:Hide()
        refresh = true
      end
    end
  end
  if refresh then mgr:Update() end
  if show then debugger.toggleview.shown = true end
end

local function killProcess(pid)
  if not pid then return false end
  if wx.wxProcess.Exists(pid) then
    local _ = wx.wxLogNull() -- disable error popup; will report as needed
    -- using SIGTERM for some reason kills not only the debugee process,
    -- but also some system processes, which leads to a blue screen crash
    -- (at least on Windows Vista SP2)
    local ret = wx.wxProcess.Kill(pid, wx.wxSIGKILL, wx.wxKILL_CHILDREN)
    if ret == wx.wxKILL_OK then
      ide:Print(TR("Program stopped (pid: %d)."):format(pid))
    elseif ret ~= wx.wxKILL_NO_PROCESS then
      wx.wxMilliSleep(250)
      if wx.wxProcess.Exists(pid) then
        displayError(TR("Unable to stop program (pid: %d), code %d."):format(pid, ret))
        return false
      end
    end
  end
  return true
end

function debugger:ActivateDocument(file, line, activatehow)
  if activatehow == activate.CLEARALL then ClearAllCurrentLineMarkers() end

  local debugger = self
  if not file then return end
  line = tonumber(line)

  -- file can be a filename or serialized file content; deserialize first.
  -- check if the filename starts with '"' and is deserializable
  -- to avoid showing filenames that may look like valid lua code
  -- (for example: 'mobdebug.lua').
  local content
  if not wx.wxFileName(file):FileExists() and file:find('^"') then
    local ok, res = LoadSafe("return "..file)
    if ok then content = res end
  end

  -- in some cases filename can be returned quoted if the chunk is loaded with
  -- loadstring(chunk, "filename") instead of loadstring(chunk, "@filename")
  if content then
    -- if the returned content can be matched with a file, it's a file name
    local fname = GetFullPathIfExists(debugger.basedir, content) or content
    if wx.wxFileName(fname):FileExists() then file, content = fname, nil end
  elseif not wx.wxIsAbsolutePath(file) and debugger.basedir then
    file = debugger.basedir .. file
  end

  if PackageEventHandle("onDebuggerPreActivate", debugger, file, line) == false then return end

  local activated = false
  local indebugger = file:find('mobdebug%.lua$')
  local fileName = wx.wxFileName(file)
  local fileNameLower = wx.wxFileName(file:lower())

  for _, document in pairs(ide:GetDocuments()) do
    local editor = document:GetEditor()
    -- either the file name matches, or the content;
    -- when checking for the content remove all newlines as they may be
    -- reported differently from the original by the Lua engine.
    local ignorecase = ide.config.debugger.ignorecase or (debugger.options or {}).ignorecase
    local filePath = document:GetFilePath()
    if filePath and (fileName:SameAs(wx.wxFileName(filePath))
      or ignorecase and fileNameLower:SameAs(wx.wxFileName(filePath:lower())))
    or content and content:gsub("[\n\r]","") == editor:GetTextDyn():gsub("[\n\r]","") then
      ClearAllCurrentLineMarkers()
      if line then
        if line == 0 then -- special case; find the first executable line
          line = math.huge
          local loadstring = loadstring or load
          local func = loadstring(editor:GetTextDyn())
          if func then -- .activelines == {[3] = true, [4] = true, ...}
            for l in pairs(debug.getinfo(func, "L").activelines) do
              if l < line then line = l end
            end
          end
          if line == math.huge then line = 1 end
        end
        if debugger.runtocursor then
          local ed, ln = unpack(debugger.runtocursor)
          if ed:GetId() == editor:GetId() and ln == line then
            -- remove run-to breakpoint at this location
            debugger:breakpointToggle(ed, ln, false)
            debugger.runtocursor = nil
          end
        end
        local line = line - 1 -- editor line operations are zero-based
        editor:MarkerAdd(line, CURRENT_LINE_MARKER)
        editor:Refresh() -- needed for background markers that don't get refreshed (wx2.9.5)
        -- expand fold if the activated line is in a folded fragment
        if not editor:GetLineVisible(line) then editor:ToggleFold(editor:GetFoldParent(line)) end

        -- found and marked what we are looking for;
        -- don't need to activate with CHECKONLY (this assumes line is given)
        if activatehow == activate.CHECKONLY then return editor end

        local firstline = editor:DocLineFromVisible(editor:GetFirstVisibleLine())
        local lastline = math.min(editor:GetLineCount(),
          editor:DocLineFromVisible(editor:GetFirstVisibleLine() + editor:LinesOnScreen()))
        -- if the line is already on the screen, then don't enforce policy
        if line <= firstline or line >= lastline then
          editor:EnsureVisibleEnforcePolicy(line)
        end
      end

      document:SetActive()
      ide:RequestAttention()
      editor:GotoLine(line-1)
      debugger:UpdateVariables(editor, document)
    if content then
        -- it's possible that the current editor tab already has
        -- breakpoints that have been set based on its filepath;
        -- if the content has been matched, then existing breakpoints
        -- need to be removed and new ones set, based on the content.
        if not debugger.editormap[editor] and filePath then
          local line = editor:MarkerNext(0, BREAKPOINT_MARKER_VALUE)
          while filePath and line ~= -1 do
            debugger:handle("delb " .. filePath .. " " .. (line+1))
            debugger:handle("setb " .. file .. " " .. (line+1))
            line = editor:MarkerNext(line + 1, BREAKPOINT_MARKER_VALUE)
          end
        end

        -- keep track of those editors that have been activated based on
        -- content rather than file names as their breakpoints have to be
        -- specified in a different way
        debugger.editormap[editor] = file
      end
      activated = editor
      break
    end
  end

  if not (activated or indebugger or debugger.loop or activatehow == activate.CHECKONLY)
  and (ide.config.editor.autoactivate or content and activatehow == activate.NOREPORT) then
    -- found file, but can't activate yet (because this part may be executed
    -- in a different coroutine), so schedule pending activation.
    if content or wx.wxFileName(file):FileExists() then
      debugger.activate = {file, line, content}
      return true -- report successful activation, even though it's pending
    end

    -- only report files once per session and if not asked to skip
    if not debugger.missing[file] and activatehow ~= activate.NOREPORT then
      debugger.missing[file] = true
      displayError(TR("Couldn't activate file '%s' for debugging; continuing without it.")
        :format(file))
    end
  end
  PackageEventHandle("onDebuggerActivate", debugger, file, line, activated)
  return activated
end

function debugger:reSetBreakpoints()
  local debugger = self
  -- remove all breakpoints that may still be present from the last session
  -- this only matters for those remote clients that reload scripts
  -- without resetting their breakpoints
  debugger:handle("delallb")

  -- go over all windows and find all breakpoints
  if (not debugger.scratchpad) then
    for _, document in pairs(ide:GetDocuments()) do
      local editor = document:GetEditor()
      local filePath = document:GetFilePath()
      local line = editor:MarkerNext(0, BREAKPOINT_MARKER_VALUE)
      while filePath and line ~= -1 do
        debugger:handle("setb " .. filePath .. " " .. (line+1))
        line = editor:MarkerNext(line + 1, BREAKPOINT_MARKER_VALUE)
      end
    end
  end
end

function debugger:shell(expression, isstatement)
  local debugger = self
  local loadstring = loadstring or load
  -- check if the debugger is running and may be waiting for a response.
  -- allow that request to finish, otherwise this function does nothing.
  if debugger.running then debugger:Update() end
  if debugger.server and not debugger.running
  and (not debugger.scratchpad or debugger.scratchpad.paused) then
    -- default options for shell commands
    local params = debugger:GetDataOptions({
        comment=true, maxlength=false, maxlevel=1, numformat=false})
    -- any explicit options for this command
    for k, v in pairs(loadstring("return"..(expression:match("--%s*(%b{})%s*$") or "{}"))()) do
      params[k] = v
    end
    copas.addthread(function()
        local debugger = debugger
        -- exec command is not expected to return anything.
        -- eval command returns 0 or more results.
        -- 'values' has a list of serialized results returned.
        -- as it is not possible to distinguish between 0 results and one
        -- 'nil' value returned, 'nil' is always returned in this case.
        -- the first value returned by eval command is not used;
        -- this may need to be taken into account by other debuggers.
        local addedret, forceexpression = true, expression:match("^%s*=%s*")
        expression = expression:gsub("^%s*=%s*","")
        local _, values, err = debugger:evaluate(expression, params)
        if not forceexpression and err then
          local _, values2, err2 = debugger:execute(expression, params)
          -- since the remote execution may fail during compilation- and run-time,
          -- and some expressions may fail in both cases, try to report the "best" error.
          -- for example, `x[1]` fails as statement, and may also fail if `x` is `nil`.
          -- in this case, the first (expression) error is returned if it's not a
          -- statement and compiles as an expression without errors.
          -- the order of statement and expression checks can't be reversed as errors from
          -- code fragments that fail with both, will be always reported as expressions.
          if not (err2 and not isstatement and loadstring("return "..expression)) then
            addedret, values, err = false, values2, err2
          end
        end

        if err then
          if addedret then err = err:gsub('^%[string "return ', '[string "') end
          ide:GetConsole():Error(err)
        elseif addedret or #values > 0 then
          if forceexpression then -- display elements as multi-line
            for i,v in pairs(values) do -- stringify each of the returned values
              local func = loadstring('return '..v) -- deserialize the value first
              if func then -- if it's deserialized correctly
                values[i] = (forceexpression and i > 1 and '\n' or '') ..
                  serialize(func(), {nocode = true, comment = 0,
                    -- if '=' is used, then use multi-line serialized output
                    indent = forceexpression and '  ' or nil})
              end
            end
          end

          -- if empty table is returned, then show nil if this was an expression
          if #values == 0 and (forceexpression or not isstatement) then
            values = {'nil'}
          end
          ide:GetConsole():Print(unpack(values))
        end

        -- refresh Stack and Watch windows if executed a statement (and no err)
        if isstatement and not err and not addedret and #values == 0 then
          debugger:updateStackSync()
          debugger:updateWatchesSync()
        end
      end)
  elseif debugger.server then
    ide:GetConsole():Error(TR("Can't evaluate the expression while the application is running."))
  end
end

function debugger:stoppedAtBreakpoint(file, line)
  -- if this document can be activated and the current line has a breakpoint
  local editor = self:ActivateDocument(file, line, activate.CHECKONLY)
  if not editor then return false end

  local current = editor:MarkerNext(0, CURRENT_LINE_MARKER_VALUE)
  local breakpoint = editor:MarkerNext(current, BREAKPOINT_MARKER_VALUE)
  return breakpoint ~= wx.wxNOT_FOUND and breakpoint == current
end

function debugger:mapRemotePath(basedir, file, line, method)
  local debugger = self
  if not file then return end

  -- file is /foo/bar/my.lua; basedir is d:\local\path\
  -- check for d:\local\path\my.lua, d:\local\path\bar\my.lua, ...
  -- wxwidgets on Windows handles \\ and / as separators, but on OSX
  -- and Linux it only handles 'native' separator;
  -- need to translate for GetDirs to work.
  local file = file:gsub("\\", "/")
  local parts = wx.wxFileName(file):GetDirs()
  local name = wx.wxFileName(file):GetFullName()

  -- find the longest remote path that can be mapped locally
  local longestpath, remotedir
  while true do
    local mapped = GetFullPathIfExists(basedir, name)
    if mapped then
      longestpath = mapped
      remotedir = file:gsub(q(name):gsub("/", ".").."$", "")
    end
    if #parts == 0 then break end
    name = table.remove(parts, #parts) .. "/" .. name
  end
  -- if the mapped directory empty or the same as the basedir, nothing to do
  if not remotedir or remotedir == "" or wx.wxFileName(remotedir):SameAs(wx.wxFileName(debugger.basedir)) then return end

  -- if found a local mapping under basedir
  local activated = longestpath and (debugger:ActivateDocument(longestpath, line, method or activate.NOREPORT)
    -- local file may exist, but not activated when not (auto-)opened, still need to remap
    or wx.wxFileName(longestpath):FileExists())
  if activated then
    -- find remote basedir by removing the tail from remote file
    debugger:handle("basedir " .. debugger.basedir .. "\t" .. remotedir)
    -- reset breakpoints again as remote basedir has changed
    debugger:reSetBreakpoints()
    ide:Print(TR("Mapped remote request for '%s' to '%s'."):format(remotedir, debugger.basedir))

    return longestpath
  end

  return nil
end

function debugger:Listen(start)
  local debugger = ide:GetDebugger()
  if start == false then
    if debugger.listening then
      debugger:terminate() -- terminate if running
      copas.removeserver(debugger.listening)
      ide:Print(TR("Debugger server stopped at %s:%d.")
        :format(debugger.hostname, debugger.portnumber))
      debugger.listening = false
    else
      displayError(TR("Can't stop debugger server as it is not started."))
    end
    return
  end

  if debugger.listening then return end

  local server, err = socket.bind("*", debugger.portnumber)
  if not server then
    displayError(TR("Can't start debugger server at %s:%d: %s.")
      :format(debugger.hostname, debugger.portnumber, err or TR("unknown error")))
    return
  end
  ide:Print(TR("Debugger server started at %s:%d."):format(debugger.hostname, debugger.portnumber))

  copas.autoclose = false
  copas.addserver(server, function (skt)
      local debugger = ide:GetDebugger()
      local options = debugger.options or {}
      if options.refuseonconflict == nil then options.refuseonconflict = ide.config.debugger.refuseonconflict end

      -- pull any pending data not processed yet
      if debugger.running then debugger:Update() end
      if debugger.server and options.refuseonconflict then
        displayError(TR("Refused a request to start a new debugging session as there is one in progress already."))
        return
      end

      -- error handler is set per-copas-thread
      copas.setErrorHandler(function(error)
        -- ignore errors that happen because debugging session is
        -- terminated during handshake (server == nil in this case).
        if debugger.server then
          displayError(TR("Can't start debugging session due to internal error '%s'."):format(error))
        end
        debugger:terminate()
      end)

      -- this may be a remote call without using an interpreter and as such
      -- debugger.options may not be set, but runonstart is still configured.
      local runstart = options.runstart
      if runstart == nil then runstart = ide.config.debugger.runonstart end

      -- support allowediting as set in the interpreter or config
      if options.allowediting == nil then options.allowediting = ide.config.debugger.allowediting end

      if not debugger.scratchpad and not options.allowediting then
        SetAllEditorsReadOnly(true)
      end

      debugger = ide:SetDebugger(debugger:init({
          server = copas.wrap(skt),
          socket = skt,
          loop = false,
          scratchable = false,
          stats = {line = 0},
          missing = {},
          editormap = {},
          runtocursor = nil,
      }))

      if PackageEventHandle("onDebuggerPreLoad", debugger, options) == false then return end

      local editor = ide:GetEditor()
      local startfile = ide:GetProjectStartFile() or options.startwith
        or (editor and SaveIfModified(editor) and ide:GetDocument(editor):GetFilePath())

      if not startfile then
        displayError(TR("Can't start debugging without an opened file or with the current file not being saved."))
        return debugger:terminate()
      end

      local startpath = wx.wxFileName(startfile):GetPath(wx.wxPATH_GET_VOLUME + wx.wxPATH_GET_SEPARATOR)
      local basedir = options.basedir or ide:GetProject() or startpath
      -- guarantee that the path has a trailing separator
      debugger.basedir = wx.wxFileName.DirName(basedir):GetFullPath()

      -- load the remote file into the debugger
      -- set basedir first, before loading to make sure that the path is correct
      debugger:handle("basedir " .. debugger.basedir)

      local init = options.init or ide.config.debugger.init
      if init then
        local _, _, err = debugger:execute(init)
        if err then displayError(TR("Ignored error in debugger initialization code: %s."):format(err)) end
      end

      debugger:reSetBreakpoints()

      local redirect = ide.config.debugger.redirect or options.redirect
      if redirect then
        debugger:handle("output stdout " .. redirect, nil,
          { handler = function(m)
              -- if it's an error returned, then handle the error
              if m and m:find("stack traceback:", 1, true) then
                -- this is an error message sent remotely
                local ok, res = LoadSafe("return "..m)
                if ok then
                  ide:Print(res)
                  return
                end
              end

              if ide.config.debugger.outputfilter then
                local ok, res = pcall(ide.config.debugger.outputfilter, m)
                if ok then
                  m = res
                else
                  displayError("Output filter failed: "..res)
                  return
                end
              end
              if m then ide:GetOutput():Write(m) end
            end})
      end

      if (options.startwith) then
        local file, line, err = debugger:loadfile(options.startwith)
        if err then
          displayError(TR("Can't run the entry point script ('%s').")
            :format(options.startwith)
            .." "..TR("Compilation error")
            ..":\n"..err)
          return debugger:terminate()
        elseif runstart and not debugger.scratchpad then
          if debugger:stoppedAtBreakpoint(file, line) then
            debugger:ActivateDocument(file, line)
            runstart = false
          end
        elseif file and line and not debugger:ActivateDocument(file, line) then
          displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).")
            :format(file, line))
        end
      elseif not debugger.scratchpad then
        local file, line, err = debugger:loadfile(startfile)
        -- "load" can work in two ways: (1) it can load the requested file
        -- OR (2) it can "refuse" to load it if the client was started
        -- with start() method, which can't load new files
        -- if file and line are set, this indicates option #2
        if err then
          displayError(TR("Can't start debugging for '%s'."):format(startfile)
            .." "..TR("Compilation error")
            ..":\n"..err)
          return debugger:terminate()
        elseif runstart then
          local file = (debugger:mapRemotePath(basedir, file, line or 0, activate.CHECKONLY)
            or file or startfile)

          if debugger:stoppedAtBreakpoint(file, line or 0) then
            debugger:ActivateDocument(file, line or 0)
            runstart = false
          end
        elseif file and line then
          local activated = debugger:ActivateDocument(file, line, activate.NOREPORT)

          -- if not found, check using full file path and reset basedir
          if not activated and not wx.wxIsAbsolutePath(file) then
            activated = debugger:ActivateDocument(startpath..file, line, activate.NOREPORT)
            if activated then
              debugger.basedir = startpath
              debugger:handle("basedir " .. debugger.basedir)
              -- reset breakpoints again as basedir has changed
              debugger:reSetBreakpoints()
            end
          end

          -- if not found and the files doesn't exist, it may be
          -- a remote call; try to map it to the project folder.
          -- also check for absolute path as it may need to be remapped
          -- when autoactivation is disabled.
          if not activated and (not wx.wxFileName(file):FileExists()
                                or wx.wxIsAbsolutePath(file)) then
            if debugger:mapRemotePath(basedir, file, line, activate.NOREPORT) then
              activated = true
            end
          end

          if not activated then
            displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).")
              :format(file, line))
          end

          -- debugger may still be available for scratchpad,
          -- if the interpreter signals scratchpad support, so enable it.
          debugger.scratchable = ide.interpreter.scratchextloop ~= nil
        else
          debugger.scratchable = true
          local activated = debugger:ActivateDocument(startfile, 0) -- find the appropriate line
          if not activated then
            displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).")
              :format(startfile, '?'))
          end
        end
      end

      if (not options.noshell and not debugger.scratchpad) then
        ide:GetConsole():SetRemote(debugger:GetConsole())
      end

      debugger:toggleViews(true)
      debugger:updateStackSync()
      debugger:updateWatchesSync()

      ide:Print(TR("Debugging session started in '%s'."):format(debugger.basedir))

      if debugger.scratchpad then
        debugger.scratchpad.updated = true
      elseif runstart then
        ClearAllCurrentLineMarkers()
        debugger:Run()
      end

      -- request attention if the debugging is stopped
      if not debugger.running then ide:RequestAttention() end
      -- refresh toolbar and menus in case the main app is not active
      ide:GetMainFrame():UpdateWindowUI(wx.wxUPDATE_UI_FROMIDLE)
      ide:GetToolBar():UpdateWindowUI(wx.wxUPDATE_UI_FROMIDLE)

      PackageEventHandle("onDebuggerLoad", debugger, options)
    end)
  debugger.listening = server
end

local function nameOutputTab(name)
  local nbk = ide.frame.bottomnotebook
  local index = nbk:GetPageIndex(ide:GetOutput())
  if index ~= wx.wxNOT_FOUND then nbk:SetPageText(index, name) end
end

local ok, winapi = pcall(require, 'winapi')
if not ok then winapi = nil end

function debugger:handle(command, server, options)
  local debugger = self
  local verbose = ide.config.debugger.verbose
  options = options or {}
  options.verbose = verbose and (function(...) ide:Print(...) end) or false

  local ip, port = debugger.socket:getpeername()
  PackageEventHandle("onDebuggerCommand", debugger, command, server or debugger.server, options)
  debugger.running = true
  debugger:UpdateStatus("running")
  if verbose then ide:Print(("[%s:%s] Debugger sent (command):"):format(ip, port), command) end
  local file, line, err = mobdebug.handle(command, server or debugger.server, options)
  if verbose then ide:Print(("[%s:%s] Debugger received (file, line, err):"):format(ip, port), file, line, err) end
  debugger.running = false
  -- only set suspended if the debugging hasn't been terminated
  debugger:UpdateStatus(debugger.server and "suspended" or "stopped")

  -- some filenames may be represented in a different code page; check and re-encode as UTF8
  local codepage = ide:GetCodePage()
  if codepage and type(file) == "string" and FixUTF8(file) == nil and winapi then
    file = winapi.encode(codepage, winapi.CP_UTF8, file)
  end
  return file, line, err
end

function debugger:exec(command, func)
  local debugger = self
  if debugger.server and not debugger.running then
    copas.addthread(function()
        local debugger = debugger
        -- execute a custom function (if any) in the context of this thread
        if type(func) == 'function' then func() end
        local out
        local attempts = 0
        while true do
          -- clear markers before running the command
          -- don't clear if running trace as the marker is then invisible,
          -- and it needs to be visible during tracing
          if not debugger.loop then ClearAllCurrentLineMarkers() end
          debugger.breaking = false
          local file, line, err = debugger:handle(out or command)
          if out then out = nil end
          if line == nil then
            if err then displayError(err) end
            debugger:teardown()
            return
          elseif not debugger.server then
            -- it is possible that while debugger.handle call was executing
            -- the debugging was terminated; simply return in this case.
            return
          else
            local activated = debugger:ActivateDocument(file, line)
            -- activation has been canceled; nothing else needs to be done
            if activated == nil then return end
            if activated then
              -- move cursor to the activated line if it's a breakpoint
              if ide.config.debugger.linetobreakpoint
              and command ~= "step" and debugger:stoppedAtBreakpoint(file, line)
              and not debugger.breaking and ide:IsValidCtrl(activated) then
                activated:GotoLine(line-1)
              end
              debugger.stats.line = debugger.stats.line + 1
              if debugger.loop then
                debugger:updateStackSync()
                debugger:updateWatchesSync()
              else
                debugger:updateStackAndWatches()
                return
              end
            else
              -- clear the marker as it wasn't cleared earlier
              if debugger.loop then ClearAllCurrentLineMarkers() end
              -- we may be in some unknown location at this point;
              -- If this happens, stop and report allowing users to set
              -- breakpoints and step through.
              if debugger.breaking then
                displayError(TR("Debugging suspended at '%s:%s' (couldn't activate the file).")
                  :format(file, line))
                debugger:updateStackAndWatches()
                return
              end
              -- redo now; if the call is from the debugger, then repeat
              -- the same command, except when it was "run" (switch to 'step');
              -- this is needed to "break" execution that happens in on() call.
              -- in all other cases get out of this file.
              -- don't get out of "mobdebug", because it may happen with
              -- start() or on() call, which will get us out of the current
              -- file, which is not what we want.
              -- Some engines (Corona SDK) report =?:0 as the current location.
              -- repeat the same command, but check if this has been tried
              -- too many times already; if so, get "out"
              out = ((tonumber(line) == 0 and attempts < 10) and command
                or (file:find('mobdebug%.lua$')
                  and (command == 'run' and 'step' or command) or "out"))
              attempts = attempts + 1
            end
          end
        end
      end)
  end
end

function debugger:handleAsync(command)
  local debugger = self
  if debugger.server and not debugger.running then
    copas.addthread(function()
        local debugger = debugger
        debugger:handle(command)
      end)
  end
end
function debugger:handleDirect(command)
  local debugger = self
  local sock = debugger.socket
  if debugger.server and sock then
    local running = debugger.running
    -- this needs to be short as it will block the UI
    sock:settimeout(0.25)
    debugger:handle(command, sock)
    sock:settimeout(0)
    -- restore running status
    debugger.running = running
  end
end

function debugger:loadfile(file)
  local debugger = self
  local f, l, err = debugger:handle("load " .. file)
  if not f and wx.wxFileExists(file) and err and err:find("Cannot open file") then
    local content = FileRead(file)
    if content then return debugger:loadstring(file, content) end
  end
  return f, l, err
end
function debugger:loadstring(file, string)
  local debugger = self
  return debugger:handle("loadstring '" .. file .. "' " .. string)
end

do
  local nextupdatedelta = 0.250
  local nextupdate = ide:GetTime() + nextupdatedelta
  local function forceUpdateOnWrap(editor)
    -- http://www.scintilla.org/ScintillaDoc.html#LineWrapping
    -- Scintilla doesn't perform wrapping immediately after a content change
    -- for performance reasons, so the activation calculations can be wrong
    -- if there is wrapping that pushes the current line out of the screen.
    -- force editor update that performs wrapping recalculation.
    if ide.config.editor.usewrap then editor:Update(); editor:Refresh() end
  end
  function debugger:Update()
    local debugger = self
    local smth = false
    if debugger.server or debugger.listening and ide:GetTime() > nextupdate then
      smth = copas.step(0)
      nextupdate = ide:GetTime() + nextupdatedelta
    end

    -- if there is any pending activation
    if debugger.activate then
      local file, line, content = unpack(debugger.activate)
      debugger.activate = nil
      if content then
        local editor = NewFile()
        editor:SetTextDyn(content)
        if not ide.config.debugger.allowediting
        and not (debugger.options or {}).allowediting then
          editor:SetReadOnly(true)
        end
        forceUpdateOnWrap(editor)
        debugger:ActivateDocument(file, line)
      else
        local editor = LoadFile(file)
        if editor then
          forceUpdateOnWrap(editor)
          debugger:ActivateDocument(file, line)
        end
      end
    end
    return smth
  end
end

function debugger:UpdateVariables(editor, document)
  local wroot = self.watchCtrl:GetRootItem()
  self.watchCtrl:DeleteChildren(wroot)
  local ctrl = ide:GetOutlineTree()
  local edpos = editor:GetCurrentPos() + #editor:GetCurLine():gsub("(%s*)$", "")
  local cache = ide:GetOutline().caches[editor]
  local item
  local depth = 0
  for n, func in ipairs(cache.funcs) do
    if edpos >= func.pos and (edpos <= func.poe) then      
      if func.depth > depth then
        item = func.item
        depth = func.depth
      end
    end
  end  
  edpos = editor:GetCurrentPos()  
  item = ctrl:GetFirstChild(item or ctrl:GetRootItem())
  while true do
    if not item:IsOk() then break end
    local n = -ctrl:GetItemData(item):GetData()
    if n > 0 then
      local t = ctrl:GetItemImage(item)
      if  t == 7 or (t == 6 and cache.vars[n].pos < edpos) then
        local var = ctrl:GetItemText(item)
        self.watchCtrl:SetItemExpression(self.watchCtrl:AppendItem(wroot, var, -1), var)
      end
    end
    item = ctrl:GetNextSibling(item)          
  end  

end

function debugger:terminate()
  local debugger = self
  if debugger.server then
    if killProcess(ide:GetLaunchedProcess()) then -- if there is PID, try local kill
      ide:SetLaunchedProcess(nil)
    else -- otherwise, try graceful exit for the remote process
      debugger:detach("exit")
    end
    debugger:teardown()
  end
end
function debugger:Step() return self:exec("step") end
function debugger:trace()
  local debugger = self
  debugger.loop = true
  debugger:exec("step")
end
function debugger:RunTo(editor, line)
  local debugger = self

  -- check if the location is valid for a breakpoint
  if editor:IsLineEmpty(line-1) then return end

  local ed, ln = unpack(debugger.runtocursor or {})
  local same = ed and ln and ed:GetId() == editor:GetId() and ln == line

  -- check if there is already a breakpoint in the "run to" location;
  -- if so, don't mark the location as "run to" as it will stop there anyway
  if bit.band(editor:MarkerGet(line-1), BREAKPOINT_MARKER_VALUE) > 0
  and not same then
    debugger.runtocursor = nil
    debugger:Run()
    return
  end

  -- save the location of the breakpoint
  debugger.runtocursor = {editor, line}
  -- set breakpoint and execute run
  debugger:exec("run", function()
      -- if run-to-cursor location is already set, then remove the breakpoint,
      -- but only if this location is different
      if ed and ln and not same then
        debugger:breakpointToggle(ed, ln, false) -- remove earlier run-to breakpoint
        debugger:Wait()
      end
      if not same then
        debugger:breakpointToggle(editor, line, true) -- set new run-to breakpoint
        debugger:Wait()
      end
    end)
end

function debugger:Wait()
  local debugger = self
  -- wait for all results to come back
  while debugger.running do debugger:Update() end
end
function debugger:Over() return self:exec("over") end
function debugger:Out() return self:exec("out") end
function debugger:Run()  
  ide:GetProjectNotebook():SetSelection(1)
  local result = self:exec("run")
  return result
end
function debugger:detach(cmd)
  local debugger = self
  if not debugger.server then return end
  if debugger.running then
    debugger:handleDirect(cmd or "done")
    debugger:teardown()
  else
    debugger:exec(cmd or "done")
  end
end
local function todeb(params) return params and " -- "..mobdebug.line(params, {comment = false}) or "" end
function debugger:evaluate(exp, params) return self:handle('eval ' .. exp .. todeb(params)) end
function debugger:execute(exp, params) return self:handle('exec '.. exp .. todeb(params)) end
function debugger:stack(params) return self:handle('stack' .. todeb(params)) end
function debugger:Break(command)
  local debugger = self
  -- stop if we're running a "trace" command
  debugger.loop = false

  -- force suspend command; don't use copas interface as it checks
  -- for the other side "reading" and the other side is not reading anything.
  -- use the "original" socket to send "suspend" command.
  -- this will only break on the next Lua command.
  if debugger.socket then
    local running = debugger.running
    -- this needs to be short as it will block the UI
    debugger.socket:settimeout(0.25)
    local file, line, err = debugger:handle(command or "suspend", debugger.socket)
    debugger.socket:settimeout(0)
    -- restore running status
    debugger.running = running
    debugger.breaking = true
    -- don't need to do anything else as the earlier call (run, step, etc.)
    -- will get the results (file, line) back and will update the UI
    return file, line, err
  end
end
function debugger:breakpoint(file, line, state)
  local debugger = self
  if debugger.running then
    return debugger:handleDirect((state and "asetb " or "adelb ") .. file .. " " .. line)
  end
  return debugger:handleAsync((state and "setb " or "delb ") .. file .. " " .. line)
end
function debugger:EvalAsync(var, callback, params)
  local debugger = self
  if debugger.server and not debugger.running and callback
  and not debugger.scratchpad and not (debugger.options or {}).noeval then
    copas.addthread(function()
      local debugger = debugger
      local _, values, err = debugger:evaluate(var, params)
      if err then
        callback(nil, (err:gsub("%[.-%]:%d+:%s*","error: ")))
      else
        callback(#values > 0 and values[1] or 'nil')
      end
    end)
  end
end

local width, height = 360, 200

local keyword = {}
for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
  'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
  'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end

local function stringifyKeyIntoPrefix(name, num)
  return (type(name) == "number"
    and (num and num == name and '' or ("[%s] = "):format(name))
    or type(name) == "string" and (name:match("^[%l%u_][%w_]*$") and not keyword[name]
      and ("%s = "):format(name)
      or ("[%q] = "):format(name))
    or ("[%s] = "):format(tostring(name)))
end

local function debuggerCreateStackWindow()
  local stackCtrl = ide:CreateTreeCtrl(ide.frame, wx.wxID_ANY,
    wx.wxDefaultPosition, wx.wxSize(width, height),
    wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS + wx.wxTR_SINGLE
    + wx.wxTR_HIDE_ROOT + wx.wxNO_BORDER)

  local debugger = ide:GetDebugger()
  debugger.stackCtrl = stackCtrl

  stackCtrl:SetImageList(debugger.imglist)

  local names = {}
  function stackCtrl:SetItemName(item, name)
    local nametype = type(name)
    names[item:GetValue()] = (
      (nametype == 'string' or nametype == 'number' or nametype == 'boolean')
      and name or nil
    )
  end

  function stackCtrl:GetItemName(item)
    return names[item:GetValue()]
  end

  local expandable = {} -- special value
  local valuecache = {}
  function stackCtrl:SetItemValueIfExpandable(item, value, delayed)
    local maxlvl = tonumber(ide.config.debugger.maxdatalevel)
    -- don't make empty tables expandable if expansion is disabled (`maxdatalevel` is false)
    local isexpandable = type(value) == 'table' and (next(value) ~= nil or delayed and maxlvl ~= nil)
    if isexpandable then -- cache table value to expand when requested
      valuecache[item:GetValue()] = next(value) == nil and expandable or value
    elseif type(value) ~= 'table' then
      valuecache[item:GetValue()] = nil
    end
    self:SetItemHasChildren(item, isexpandable)
  end

  function stackCtrl:IsExpandable(item) return valuecache[item:GetValue()] == expandable end

  local stacktype_img = { ["table"] = 3, ["object"] = 5, ["function"] = 4, ["string"] = 6, ["number"] = 7}
  function stackCtrl:GetImageFromValue(value)
    -- local value = value or self:GetItemChildren(self:GetItemParent(item))[self:GetItemText(item)]
    local image
    if value ~= nil then
      local t = type(value)
      if t == "table" then
        if value.constructor ~= nil then
          image = 5
        elseif getmetatable(value) ~= nil and getmetatable(value).__type ~= nil then
          image = 8
        else
          image = 3
        end
      else
        image = stacktype_img[t]
      end
    else
      image = -1
    end
    return image
  end

  function stackCtrl:DeleteAll()
    self:DeleteAllItems()
    valuecache = {}
    names = {}
  end

  function stackCtrl:GetItemChildren(item)
    return valuecache[item:GetValue()] or {}
  end

  function stackCtrl:IsFrame(item)
    return (item and item:IsOk() and self:GetItemParent(item):IsOk()
      and self:GetItemParent(item):GetValue() == self:GetRootItem():GetValue())
  end

  function stackCtrl:GetItemFullExpression(item)
    local expr = ''
    while item:IsOk() and not self:IsFrame(item) do
      local name = self:GetItemName(item)
      -- check if it's a top item, as it needs to be used as is;
      -- convert `(*vararg num)` to `select(num, ...)`
      expr = (self:IsFrame(self:GetItemParent(item))
        and name:gsub("^%(%*vararg (%d+)%)$", "select(%1, ...)")
        or (type(name) == 'string' and '[%q]' or '[%s]'):format(tostring(name)))
      ..expr
      item = self:GetItemParent(item)
    end
    return expr, item:IsOk() and item or nil
  end

  function stackCtrl:GetItemPos(item)
    if not item:IsOk() then return end
    local pos = 0
    repeat
      pos = pos + 1
      item = self:GetPrevSibling(item)
    until not item:IsOk()
    return pos
  end

  function stackCtrl:ExpandItemValue(item)
    local expr, itemframe = self:GetItemFullExpression(item)
    local stack = self:GetItemPos(itemframe)

    local debugger = ide:GetDebugger()
    if debugger.running then debugger:Update() end
    if debugger.server and not debugger.running
    and (not debugger.scratchpad or debugger.scratchpad.paused) then
      copas.addthread(function()
        local debugger = debugger
        local value, _, err = debugger:evaluate(expr, {maxlevel = 1, stack = stack})
        if err then
          err = err:gsub("%[.-%]:%d+:%s+","")
          -- this may happen when attempting to expand a sub-element referenced by a key
          -- that can't be evaluated, like a table, function, or userdata
          if err ~= "attempt to index a nil value" then
            self:SetItemText(item, 'error: '..err)
          else
            local name = self:GetItemName(item)
            local text = stringifyKeyIntoPrefix(name, self:GetItemPos(item)).."{}"
            self:SetItemText(item, text)
            self:SetItemValueIfExpandable(item, {})
            self:Expand(item)
          end
        else
          local ok, res = LoadSafe("return "..tostring(value))
          if ok then
            self:SetItemValueIfExpandable(item, res)
            self:Expand(item)

            local name = self:GetItemName(item)
            if not name then
              -- this is an empty table, so replace MORE indicator with the empty table
              self:SetItemText(item, (self:GetItemText(item):gsub(q(MORE), "{}")))
              return
            end

            -- update cache in the parent
            local parent = self:GetItemParent(item)
            valuecache[parent:GetValue()][name] = res

            local params = debugger:GetDataOptions({maxlevel=1})

            -- now update all serialized values in the tree starting from the expanded item
            while item:IsOk() and not self:IsFrame(item) do
              local value = valuecache[item:GetValue()]
              local strval = fixUTF8(serialize(value, params))
              local name = self:GetItemName(item)
              local text = (self:IsFrame(self:GetItemParent(item))
                and name.." = "
                or stringifyKeyIntoPrefix(name, self:GetItemPos(item)))
              ..strval
              self:SetItemText(item, text)
              item = self:GetItemParent(item)
            end
          end
        end
      end)
    end
  end

  stackCtrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_EXPANDING,
    function (event)
      local item_id = event:GetItem()
      local count = stackCtrl:GetChildrenCount(item_id, false)
      if count > 0 then return true end

      if stackCtrl:IsExpandable(item_id) then return stackCtrl:ExpandItemValue(item_id) end

      local image = stackCtrl:GetItemImage(item_id)
      local num, maxnum = 1, ide.config.debugger.maxdatanum
      local params = debugger:GetDataOptions({maxlevel = 1})

      stackCtrl:Freeze()
      for name,value in pairs(stackCtrl:GetItemChildren(item_id)) do
        local image = stackCtrl:GetImageFromValue(value)
        item = stackCtrl:AppendItem(item_id, "")
        if image ~= nil then
          stackCtrl:SetItemImage(item, image)
        end
        stackCtrl:SetItemValueIfExpandable(item, value, true)

        local text = name
        if image ~= nil and image > 5 then
          text = name..' = '..fixUTF8(serialize(value, params))
        end
        stackCtrl:SetItemText(item, text) --stringifyKeyIntoPrefix(name, num)..strval)
        stackCtrl:SetItemName(item, name)
        num = num + 1
        if num > maxnum then break end
      end
      stackCtrl:Thaw()
      return true
    end)

    stackCtrl:Connect(wx.wxEVT_TREE_ITEM_GETTOOLTIP,
    function (event)
      stackCtrl:Freeze()
      local item_id = event:GetItem() 
      local val = stackCtrl:GetItemChildren(stackCtrl:GetItemParent(item_id))[stackCtrl:GetItemText(item_id)]
      if val ~= nil then
        local t
        local image = stackCtrl:GetItemImage(item_id, wx.wxTreeItemIcon_Normal)
        for k, v in pairs(stacktype_img) do
          if v == image then
              t = k
            break
          end
        end
        event:SetToolTip(t or tostring(val):match("^(%w+)"))
      end
      stackCtrl:Thaw()
      return true
    end)


  stackCtrl:Connect(wx.wxEVT_SET_FOCUS, function(event)
      local debugger = ide:GetDebugger()
      if debugger.needrefresh.stack then
        debugger:updateStack()
        debugger.needrefresh.stack = false
      end
    end)

  -- register navigation callback
  stackCtrl:Connect(wx.wxEVT_LEFT_DCLICK, function (event)
    local item_id = stackCtrl:HitTest(event:GetPosition())
    if not item_id or not item_id:IsOk() then event:Skip() return end

    local coords = callData[item_id:GetValue()]
    if not coords then event:Skip() return end

    local file, line = coords[1], coords[2]
    if file:match("@") then file = string.sub(file, 2) end
    file = GetFullPathIfExists(ide:GetDebugger().basedir, file)
    if file then
      local editor = LoadFile(file,nil,true)
      editor:SetFocus()
      if line then
        editor:GotoLine(line-1)
        editor:EnsureVisibleEnforcePolicy(line-1) -- make sure the line is visible (unfolded)
      end
    end
  end)

  local layout = ide:GetSetting("/view", "uimgrlayout")
  local iconsize = ide:GetBestIconSize()
  if layout and not layout:find("stackpanel") then
    ide:AddPanelDocked(ide.frame.bottomnotebook, stackCtrl, "stackpanel", TR("Stack"), nil, ide:GetBitmap("DEBUG", "CALLSTACK", wx.wxSize(iconsize, iconsize)))--wx.wxBitmap("studio/res/16/DEBUG-CALLSTACK.png"))
  else
    ide:AddPanel(stackCtrl, "stackpanel", TR("Stack"), nil, ide:GetBitmap("DEBUG", "CALLSTACK", wx.wxSize(iconsize, iconsize)))--wx.wxBitmap("studio/res/16/DEBUG-CALLSTACK.png"))
  end
end

local function debuggerCreateWatchWindow()
  local watchCtrl = ide:CreateTreeCtrl(ide.frame, wx.wxID_ANY,
    wx.wxDefaultPosition, wx.wxSize(width, height),
    wx.wxTR_SINGLE + wx.wxTR_HAS_BUTTONS + wx.wxTR_NO_LINES + wx.wxTR_TWIST_BUTTONS + wx.wxTR_HIDE_ROOT + wx.wxNO_BORDER)

  local debugger = ide:GetDebugger()
  debugger.watchCtrl = watchCtrl

  local root = watchCtrl:AddRoot("Watch")
  watchCtrl:SetImageList(debugger.imglist)

  local defaultExpr = "watch expression"
  local expressions = {} -- table to keep track of expressions

  function watchCtrl:SetItemExpression(item, expr, value)
    expressions[item:GetValue()] = expr
    if value and value:sub(1,1) == "{" then
      self:SetItemText(item, expr)
    else
      self:SetItemText(item, expr.." = "..tostring(value))
    end
    self:SelectItem(item, true)
    local debugger = ide:GetDebugger()
    if not value then debugger:updateWatches(item) end
  end

  function watchCtrl:GetItemExpression(item)
    return expressions[item:GetValue()]
  end

  local names = {}
  function watchCtrl:SetItemName(item, name)
    local nametype = type(name)
    names[item:GetValue()] = (
      (nametype == 'string' or nametype == 'number' or nametype == 'boolean')
      and name or nil
    )
  end

  function watchCtrl:GetItemName(item)
    return names[item:GetValue()]
  end

  local expandable = {} -- special value
  local valuecache = {}
  function watchCtrl:SetItemValueIfExpandable(item, value, delayed)
    local maxlvl = tonumber(ide.config.debugger.maxdatalevel)
    -- don't make empty tables expandable if expansion is disabled (`maxdatalevel` is false)
    local isexpandable = type(value) == 'table' and (next(value) ~= nil or delayed and maxlvl ~= nil)
    if isexpandable then -- cache table value to expand when requested
      valuecache[item:GetValue()] = next(value) == nil and expandable or value
    elseif type(value) ~= 'table' then
      valuecache[item:GetValue()] = nil
    end
    self:SetItemHasChildren(item, isexpandable)
  end

  function watchCtrl:IsExpandable(item) return valuecache[item:GetValue()] == expandable end

  function watchCtrl:GetItemChildren(item)
    return valuecache[item:GetValue()] or {}
  end

  function watchCtrl:IsWatch(item)
    return (item and item:IsOk() and self:GetItemParent(item):IsOk()
      and self:GetItemParent(item):GetValue() == root:GetValue())
  end

  function watchCtrl:IsEditable(item)
    return (item and item:IsOk()
      and (self:IsWatch(item) or self:GetItemName(item) ~= nil))
  end

  function watchCtrl:GetItemFullExpression(item)
    local expr = ''
    while true do
      local name = self:GetItemName(item)
      expr = (self:IsWatch(item)
        and ('({%s})[1]'):format(self:GetItemExpression(item))
        or (type(name) == 'string' and '[%q]' or '[%s]'):format(tostring(name))
      )..expr
      if self:IsWatch(item) then break end
      item = self:GetItemParent(item)
      if not item:IsOk() then break end
    end
    return expr, item:IsOk() and item or nil
  end

  function watchCtrl:CopyItemValue(item)
    local expr = self:GetItemFullExpression(item)

    local debugger = ide:GetDebugger()
    if debugger.running then debugger:Update() end
    if debugger.server and not debugger.running
    and (not debugger.scratchpad or debugger.scratchpad.paused) then
      copas.addthread(function()
        local debugger = debugger
        local _, values, error = debugger:evaluate(expr)
        ide:CopyToClipboard(error and error:gsub("%[.-%]:%d+:%s+","")
          or (#values == 0 and 'nil' or fixUTF8(values[1])))
      end)
    end
  end

  function watchCtrl:UpdateItemValue(item, value)
    local expr, itemupd = self:GetItemFullExpression(item)

    local debugger = ide:GetDebugger()
    if debugger.running then debugger:Update() end
    if debugger.server and not debugger.running
    and (not debugger.scratchpad or debugger.scratchpad.paused) then
      copas.addthread(function()
        local debugger = debugger
        local _, _, err = debugger:execute(expr..'='..value)
        if err then
          watchCtrl:SetItemText(item, 'error: '..err:gsub("%[.-%]:%d+:%s+",""))
        elseif itemupd then
          debugger:updateWatchesSync(itemupd)
        end
        debugger:updateStackSync()
      end)
    end
  end

  function watchCtrl:GetItemPos(item)
    if not item:IsOk() then return end
    local pos = 0
    repeat
      pos = pos + 1
      item = self:GetPrevSibling(item)
    until not item:IsOk()
    return pos
  end

  function watchCtrl:ExpandItemValue(item)
    local expr = self:GetItemFullExpression(item)

    local debugger = ide:GetDebugger()
    if debugger.running then debugger:Update() end
    if debugger.server and not debugger.running
    and (not debugger.scratchpad or debugger.scratchpad.paused) then
      copas.addthread(function()
        local debugger = debugger
        local value, _, err = debugger:evaluate(expr, {maxlevel = 1})
        if err then
          self:SetItemText(item, 'error: '..err:gsub("%[.-%]:%d+:%s+",""))
        else
          local ok, res = LoadSafe("return "..tostring(value))
          if ok then
            self:SetItemValueIfExpandable(item, res)
            self:Expand(item)
            local name = self:GetItemName(item)
            if not name then
              self:SetItemText(item, (self:GetItemText(item):gsub(q(MORE), "{}")))
              return
            end

            local params = debugger:GetDataOptions({maxlevel=1})
            -- now update all serialized values in the tree starting from the expanded item
            while item:IsOk() do
              local value = valuecache[item:GetValue()]
              local name = self:GetItemName(item)
              self:SetItemText(item, name..strval)
              if _type and type_img[_type] ~= nil then
                self:SetItemImage(item, type_img[_type])
              end
              if self:IsWatch(item) then break end
              item = self:GetItemParent(item)
            end
          end
        end
      end)
    end
  end

  watchCtrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_EXPANDING,
    function (event)
      local item_id = event:GetItem()
      local count = watchCtrl:GetChildrenCount(item_id, false)
      if count > 0 then return true end

      if watchCtrl:IsExpandable(item_id) then return watchCtrl:ExpandItemValue(item_id) end

      local num, maxnum = 1, ide.config.debugger.maxdatanum
      local params = debugger:GetDataOptions({maxlevel = 1})

      watchCtrl:Freeze()
      local properties = {}
      for name,value in pairs(watchCtrl:GetItemChildren(item_id)) do
        local image
        local complex = false
        local _type = type(value):match("(%w+)")
        if _type ~= nil then
          if _type == "table" then
            _type = (getmetatable(value) or {}).__name
            if _type == nil then
              image = "table"
              _type = image
            else
              image = "Object"
              complex = true
            end
          elseif _type == "Object" then
            _type = getmetatable(value).__name
            image = "instance"
            complex = true
          end   
	      end         
        _type = _type ~= nil and _type:match('%w+') or _type
        local newval
        if complex then
          newval = fixUTF8(name..' <'.._type..'>')
        elseif _type == "function" then
          local i = watchCtrl:GetItemImage(item_id)
          newval = name.."()"
        elseif _type == "table" or _type == "module" then
          newval = name
        elseif _type == "string" and value:match("^Object: ") then
          newval = name
          image = "Object"        
        else
          newval = fixUTF8(name .. ' = '..serialize(value))
        end
        if type(name) == "string" and name:find("^property_get_") then
          newval = fixUTF8(name:gsub("^property_get_", "") .. ' = '..serialize(value):gsub("%-%-%[%[.-%]%]", ""))
          image = "property"
        end
        local item = watchCtrl:AppendItem(item_id, "", -1)
        watchCtrl:SetItemValueIfExpandable(item, value, true)     
        watchCtrl:SetItemText(item, tostring(newval))
        image = image or _type or "nil"
        watchCtrl:SetItemImage(item, type_img[image])
        num = num + 1
        if num > maxnum then break end
	::done_property::
      end
      watchCtrl:Thaw()
      return true
    end)

  watchCtrl:Connect(wx.wxEVT_COMMAND_TREE_DELETE_ITEM,
    function (event)
      local value = event:GetItem():GetValue()
      expressions[value] = nil
      valuecache[value] = nil
      names[value] = nil
    end)

  watchCtrl:Connect(wx.wxEVT_SET_FOCUS, function(event)
      local debugger = ide:GetDebugger()
      if debugger.needrefresh.watches then
        debugger:updateWatches()
        debugger.needrefresh.watches = false
      end
    end)

  local item
  -- wx.wxEVT_CONTEXT_MENU is only triggered over tree items on OSX,
  -- but it needs to be also triggered below any item to add a watch,
  -- so use RIGHT_DOWN instead
  watchCtrl:Connect(wx.wxEVT_RIGHT_DOWN,
    function (event)
      -- store the item to be used in edit/delete actions
      item = watchCtrl:HitTest(watchCtrl:ScreenToClient(wx.wxGetMousePosition()))
      local menu = ide:MakeMenu {
        { ID.COPYWATCHVALUE, TR("&Copy Value")..KSC(ID.COPYWATCHVALUE) },
      }
      PackageEventHandle("onMenuWatch", menu, watchCtrl, event)
      watchCtrl:PopupMenu(menu)
      item = nil
    end)

  watchCtrl:Connect(ID.COPYWATCHVALUE, wx.wxEVT_COMMAND_MENU_SELECTED,
    function (event) watchCtrl:CopyItemValue(item or watchCtrl:GetSelection()) end)
  watchCtrl:Connect(ID.COPYWATCHVALUE, wx.wxEVT_UPDATE_UI, function (event)
      -- allow copying only when the debugger is available
      local debugger = ide:GetDebugger()
      event:Enable(item:IsOk() and debugger.server and not debugger.running
        and (not debugger.scratchpad or debugger.scratchpad.paused))
    end)

  local layout = ide:GetSetting("/view", "uimgrlayout")
  local iconsize = ide:GetBestIconSize()
  if layout and not layout:find("watchpanel") then
    ide:AddPanelDocked(ide.frame.bottomnotebook, watchCtrl, "watchpanel", "Variables", nil, false, ide:GetBitmap("DEBUG", "WATCH", wx.wxSize(iconsize, iconsize)))
  else
    ide:AddPanel(watchCtrl, "watchpanel", TR("Variables"), nil, ide:GetBitmap("DEBUG", "WATCH", wx.wxSize(iconsize, iconsize)))
    ide:GetUIManager():GetPane("watchpanel"):MinSize(260, -1):Bottom():Dock():Layer(1):Position(2):BestSize(300, 200)
  end
end

debuggerCreateStackWindow()
debuggerCreateWatchWindow()

----------------------------------------------
-- public api

function debugger:RefreshPanels() return self:updateStackAndWatches() end

function debugger:BreakpointSet(...) return self:breakpoint(...) end

local statuses = {
  running = TR("Output (running)"),
  suspended = TR("Output (suspended)"),
  stopped = TR("Output"),
}
function debugger:UpdateStatus(status)
  local debugger = self
  if not status then
    status = debugger.running and "running" or debugger.server and "suspended" or "stopped"
  end
  if PackageEventHandle("onDebuggerStatusUpdate", debugger, status) == false then return end
  nameOutputTab(statuses[status] or statuses.stopped)
end

function debugger:OutputSet(stream, mode, options)
  return self:handle(("output %s %s"):format(stream, mode), nil, options)
end

function DebuggerAttachDefault(options) ide:GetDebugger():SetOptions(options) end
function debugger:SetOptions(options) self.options = options end

function debugger:Stop()
  local debugger = self
  -- terminate the local session (if still active)
  if killProcess(ide:GetLaunchedProcess()) then ide:SetLaunchedProcess(nil) end
  debugger:terminate()
  debugger.stackCtrl:Freeze()
  debugger.stackCtrl:DeleteAll()
  debugger.stackCtrl:Thaw()
  debugger:RefreshPanels()
end

function debugger:Shutdown()
  self:Stop()
  PackageEventHandle("onDebuggerShutdown", self)
end

function debugger:teardown()
  local debugger = self
  local uimgr = ide:GetUIManager()
  uimgr:GetPane("watchpanel"):Hide()
  debugger.watchCtrl:DeleteChildren(debugger.watchCtrl:GetRootItem())  
  uimgr:Update()
  if debugger.server then
    local lines = TR("traced %d instruction", debugger.stats.line):format(debugger.stats.line)
    ide:Print(TR("Debugging session completed (%s)."):format(lines))
    debugger:UpdateStatus(ide:GetLaunchedProcess() and "running" or "stopped")
    if debugger.runtocursor then
      local ed, ln = unpack(debugger.runtocursor)
      debugger:breakpointToggle(ed, ln, false) -- remove current run-to breakpoint
    end
    if PackageEventHandle("onDebuggerPreClose", debugger) ~= false then
      SetAllEditorsReadOnly(false)
      ide:GetConsole():SetRemote(nil)
      ClearAllCurrentLineMarkers()
      debugger:toggleViews(false)
      PackageEventHandle("onDebuggerClose", debugger)
    end
    debugger.server = nil
    debugger:ScratchpadOff()
  else
    -- it's possible that the application couldn't start, or that the
    -- debugger in the application didn't start, which means there is
    -- no debugger.server, but scratchpad may still be on. Turn it off.
    debugger:ScratchpadOff()
  end
end

local function debuggerMakeFileName(editor)
  return ide:GetDocument(editor):GetFilePath()
  or ide:GetDocument(editor):GetFileName()
  or ide:GetDefaultFileName()
end

function debugger:breakpointToggle(editor, line, value)
  local debugger = self
  local file = debugger.editormap and debugger.editormap[editor] or debuggerMakeFileName(editor)
  debugger:BreakpointSet(file, line, value)
end

-- scratchpad functions

function debugger:ScratchpadRefresh()
  local debugger = self
  if debugger.scratchpad and debugger.scratchpad.updated and not debugger.scratchpad.paused then
    local scratchpadEditor = debugger.scratchpad.editor
    if scratchpadEditor.spec.apitype
    and scratchpadEditor.spec.apitype == "lua"
    and not ide.interpreter.skipcompile
    and not CompileProgram(scratchpadEditor, { jumponerror = false, reportstats = false })
    then
      debugger.scratchpad.updated = false
      return
    end

    local code = StripShebang(scratchpadEditor:GetTextDyn())
    if debugger.scratchpad.running then
      -- break the current execution first
      -- don't try too frequently to avoid overwhelming the debugger
      local now = ide:GetTime()
      if now - debugger.scratchpad.running > 0.250 then
        debugger:Break()
        debugger.scratchpad.running = now
      end
    else
      local filePath = debuggerMakeFileName(scratchpadEditor)

      -- wrap into a function call to make "return" to work with scratchpad
      code = "(function(...)"..code.."\nend)(...)"

      -- this is a special error message that is generated at the very end
      -- of each script to avoid exiting the (debugee) scratchpad process.
      -- these errors are handled and not reported to the user
      local errormsg = 'execution suspended at ' .. ide:GetTime()
      local stopper = "error('" .. errormsg .. "')"
      -- store if interpreter requires a special handling for external loop
      local extloop = ide.interpreter.scratchextloop

      local function reloadScratchpadCode()
        local debugger = debugger
        debugger.scratchpad.running = ide:GetTime()
        debugger.scratchpad.updated = false
        debugger.scratchpad.runs = (debugger.scratchpad.runs or 0) + 1

        ide:GetOutput():Erase()

        -- the code can be running in two ways under scratchpad:
        -- 1. controlled by the application, requires stopper (most apps)
        -- 2. controlled by some external loop (for example, love2d).
        -- in the first case we need to reload the app after each change
        -- in the second case, we need to load the app once and then
        -- "execute" new code to reflect the changes (with some limitations).
        local _, _, err
        if extloop then -- if the execution is controlled by an external loop
          if debugger.scratchpad.runs == 1
          then _, _, err = debugger:loadstring(filePath, code)
          else _, _, err = debugger:execute(code) end
        else   _, _, err = debugger:loadstring(filePath, code .. stopper) end

        -- when execute() is used, it's not possible to distinguish between
        -- compilation and run-time error, so just report as "Scratchpad error"
        local prefix = extloop and TR("Scratchpad error") or TR("Compilation error")

        if not err then
          _, _, err = debugger:handle("run")
          prefix = TR("Execution error")
        end
        if err and not err:find(errormsg) then
          local fragment, line = err:match('.-%[string "([^\010\013]+)"%]:(%d+)%s*:')
          -- make the code shorter to better see the error message
          if prefix == TR("Scratchpad error") and fragment and #fragment > 30 then
            err = err:gsub(q(fragment), function(s) return s:sub(1,30)..'...' end)
          end
          displayError(prefix
            ..(line and (" "..TR("on line %d"):format(line)) or "")
            ..":\n"..err:gsub('stack traceback:.+', ''):gsub('\n+$', ''))
        end
        debugger.scratchpad.running = false
      end

      copas.addthread(reloadScratchpadCode)
    end
  end
end

function debugger:ScratchpadOn(editor)
  local debugger = self

  -- first check if there is already scratchpad editor.
  -- this may happen when more than one editor is being added...
  if debugger.scratchpad and debugger.scratchpad.editors then
    debugger.scratchpad.editors[editor] = true
  else
    debugger.scratchpad = {editor = editor, editors = {[editor] = true}}

    -- check if the debugger is already running; this happens when
    -- scratchpad is turned on after external script has connected
    if debugger.server then
      debugger.scratchpad.updated = true
      ClearAllCurrentLineMarkers()
      SetAllEditorsReadOnly(false)
      ide:GetConsole():SetRemote(nil) -- disable remote shell
      debugger:ScratchpadRefresh()
    elseif not ProjectDebug(true, "scratchpad") then
      debugger.scratchpad = nil
      return
    end
  end

  local scratchpadEditor = editor
  for _, numberStyle in ipairs(scratchpadEditor.spec.isnumber) do
    scratchpadEditor:StyleSetUnderline(numberStyle, true)
  end
  debugger.scratchpad.margin = scratchpadEditor:GetAllMarginWidth()

  scratchpadEditor:Connect(wxstc.wxEVT_STC_MODIFIED, function(event)
    local evtype = event:GetModificationType()
    if (bit.band(evtype,wxstc.wxSTC_MOD_INSERTTEXT) ~= 0 or
        bit.band(evtype,wxstc.wxSTC_MOD_DELETETEXT) ~= 0 or
        bit.band(evtype,wxstc.wxSTC_PERFORMED_UNDO) ~= 0 or
        bit.band(evtype,wxstc.wxSTC_PERFORMED_REDO) ~= 0) then
      debugger.scratchpad.updated = true
      debugger.scratchpad.editor = scratchpadEditor
    end
    event:Skip()
  end)

  scratchpadEditor:Connect(wx.wxEVT_LEFT_DOWN, function(event)
    local scratchpad = debugger.scratchpad

    local point = event:GetPosition()
    local pos = scratchpadEditor:PositionFromPoint(point)
    local isnumber = scratchpadEditor.spec.isnumber

    -- are we over a number in the scratchpad? if not, it's not our event
    if not (scratchpad and isnumber[bit.band(scratchpadEditor:GetStyleAt(pos),ide.STYLEMASK)]) then
      event:Skip()
      return
    end

    -- find start position and length of the number
    local text = scratchpadEditor:GetTextDyn()

    local nstart = pos
    while nstart >= 0 and isnumber[bit.band(scratchpadEditor:GetStyleAt(nstart),ide.STYLEMASK)] do
      nstart = nstart - 1
    end

    local nend = pos
    while nend < string.len(text) and isnumber[bit.band(scratchpadEditor:GetStyleAt(nend),ide.STYLEMASK)] do
      nend = nend + 1
    end

    -- check if there is minus sign right before the number and include it
    if nstart >= 0 and scratchpadEditor:GetTextRangeDyn(nstart,nstart+1) == '-' then 
      nstart = nstart - 1
    end
    scratchpad.start = nstart + 1
    scratchpad.length = nend - nstart - 1
    scratchpad.origin = scratchpadEditor:GetTextRangeDyn(nstart+1,nend)
    if tonumber(scratchpad.origin) then
      scratchpad.point = point
      scratchpadEditor:BeginUndoAction()
      scratchpadEditor:CaptureMouse()
    end
  end)

  scratchpadEditor:Connect(wx.wxEVT_LEFT_UP, function(event)
    if debugger.scratchpad and debugger.scratchpad.point then
      debugger.scratchpad.point = nil
      scratchpadEditor:EndUndoAction()
      scratchpadEditor:ReleaseMouse()
      wx.wxSetCursor(wx.wxNullCursor) -- restore cursor
    else event:Skip() end
  end)

  scratchpadEditor:Connect(wx.wxEVT_MOTION, function(event)
    local point = event:GetPosition()
    local pos = scratchpadEditor:PositionFromPoint(point)
    local scratchpad = debugger.scratchpad
    local ipoint = scratchpad and scratchpad.point

    -- record the fact that we are over a number or dragging slider
    scratchpad.over = scratchpad and
      (ipoint ~= nil or scratchpadEditor.spec.isnumber[bit.band(scratchpadEditor:GetStyleAt(pos),ide.STYLEMASK)])

    if ipoint then
      local startpos = scratchpad.start
      local endpos = scratchpad.start+scratchpad.length

      -- calculate difference in point position
      local dx = point.x - ipoint.x

      -- calculate the number of decimal digits after the decimal point
      local origin = scratchpad.origin
      local decdigits = #(origin:match('%.(%d+)') or '')

      -- calculate new value
      local value = tonumber(origin) + dx * 10^-decdigits

      -- convert new value back to string to check the number of decimal points
      -- this is needed because the rate of change is determined by the
      -- current value. For example, for number 1, the next value is 2,
      -- but for number 1.1, the next is 1.2 and for 1.01 it is 1.02.
      -- But if 1.01 becomes 1.00, the both zeros after the decimal point
      -- need to be preserved to keep the increment ratio the same when
      -- the user wants to release the slider and start again.
      origin = tostring(value)
      local newdigits = #(origin:match('%.(%d+)') or '')
      if decdigits ~= newdigits then
        origin = origin .. (origin:find('%.') and '' or '.') .. ("0"):rep(decdigits-newdigits)
      end

      -- update length
      scratchpad.length = #origin

      -- update the value in the document
      scratchpadEditor:SetTargetStart(startpos)
      scratchpadEditor:SetTargetEnd(endpos)
      scratchpadEditor:ReplaceTarget(origin)
    else event:Skip() end
  end)

  scratchpadEditor:Connect(wx.wxEVT_SET_CURSOR, function(event)
    if (debugger.scratchpad and debugger.scratchpad.over) then
      event:SetCursor(wx.wxCursor(wx.wxCURSOR_SIZEWE))
    elseif debugger.scratchpad and ide.osname == 'Unix' then
      -- restore the cursor manually on Linux since event:Skip() doesn't reset it
      local ibeam = event:GetX() > debugger.scratchpad.margin
      event:SetCursor(wx.wxCursor(ibeam and wx.wxCURSOR_IBEAM or wx.wxCURSOR_RIGHT_ARROW))
    else event:Skip() end
  end)

  return true
end

function debugger:ScratchpadOff()
  local debugger = self
  if not debugger.scratchpad then return end

  for scratchpadEditor in pairs(debugger.scratchpad.editors) do
    for _, numberStyle in ipairs(scratchpadEditor.spec.isnumber) do
      scratchpadEditor:StyleSetUnderline(numberStyle, false)
    end
    scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wxstc.wxEVT_STC_MODIFIED)
    scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_MOTION)
    scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_LEFT_DOWN)
    scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_LEFT_UP)
    scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_SET_CURSOR)
  end

  wx.wxSetCursor(wx.wxNullCursor) -- restore cursor

  debugger.scratchpad = nil
  debugger:terminate()

  -- disable menu if it is still enabled
  -- (as this may be called when the debugger is being shut down)
  local menuBar = ide.frame.menuBar
  if menuBar:IsChecked(ID.RUNNOW) then menuBar:Check(ID.RUNNOW, false) end

  return true
end

debugger = ide:SetDebugger(setmetatable({}, {__index = protodeb}))

ide:AddPackage('core.debugger', {
    onEditorMarkerUpdate = function(self, editor, marker, line, value)
      if marker ~= BREAKPOINT_MARKER then return end

      local debugger = ide:GetDebugger()
      if value == false then
        -- if there is pending "run-to-cursor" call at this location, remove it
        local ed, ln = unpack(debugger.runtocursor or {})
        local same = ed and ln and ed:GetId() == editor:GetId() and ln == line
        if same then debugger.runtocursor = nil end
      elseif editor:IsLineEmpty(line-1) then
        return false -- don't set marker here
      end

      return debugger:breakpointToggle(editor, line, value)
    end,
  })
