-- Copyright 2015-18 Paul Kulchenko, ZeroBrane LLC

local ide = ide
ide.markers = {
  markersCtrl = nil,
  imglist = ide:CreateImageList("MARKERS", "FILE-NORMAL", "DEBUG-BREAKPOINT-TOGGLE", "BOOKMARK-TOGGLE"),
  needrefresh = {},
  settings = {markers = {}},
}

local unpack = table.unpack or unpack
local markers = ide.markers
local caches = {}
local image = { FILE = 0, BREAKPOINT = 1, BOOKMARK = 2 }
local markertypes = {breakpoint = 0, bookmark = 0}
local maskall = 0
for markertype in pairs(markertypes) do
  markertypes[markertype] = 2^ide:GetMarker(markertype)
  maskall = maskall + markertypes[markertype]
end

-- make these two IDs local to this menu,
-- as their status differs from bookmarks/breakpoints in the editor
local BOOKMARKTOGGLE = ID("markers.bookmarktoggle")
local BREAKPOINTTOGGLE = ID("markers.breakpointtoggle")

ide.config.toolbar.iconmap[BOOKMARKTOGGLE] = ide.config.toolbar.iconmap[ID.BOOKMARKTOGGLE]
ide.config.toolbar.iconmap[BREAKPOINTTOGGLE] = ide.config.toolbar.iconmap[ID.BREAKPOINTTOGGLE]

local function resetMarkersTimer()
  if ide.config.markersinactivity then
    ide.timers.markers:Start(ide.config.markersinactivity*1000, wx.wxTIMER_ONE_SHOT)
  end
end

local function needRefresh(editor)
  ide.markers.needrefresh[editor] = true
  resetMarkersTimer()
end

local function getMarkers(editor, mtype)
  local edmarkers = {}
  local line = editor:MarkerNext(0, maskall)
  while line ~= wx.wxNOT_FOUND do
    local markerval = editor:MarkerGet(line)
    for markertype, val in pairs(markertypes) do
      if bit.band(markerval, val) > 0 and (not mtype or markertype == mtype) then
        table.insert(edmarkers, {line, markertype})
      end
    end
    line = editor:MarkerNext(line + 1, maskall)
  end
  return edmarkers
end

local function markersRefresh()
  local ctrl = ide.markers.markersCtrl
  local win = ide:GetMainFrame():FindFocus()
  ctrl:Freeze()

  for editor in pairs(ide.markers.needrefresh) do
    local cache = caches[editor]
    if cache then
      local fileitem = cache.fileitem
      if not fileitem then
        local filename = ide:GetDocument(editor):GetTabText()
        local root = ctrl:GetRootItem()
        if not root or not root:IsOk() then return end
        fileitem = ctrl:AppendItem(root, filename, image.FILE)
        ctrl:SortChildren(root)
        cache.fileitem = fileitem
      end

      -- disabling event handlers is not strictly necessary, but it's expected
      -- to fix a crash on Windows that had DeleteChildren in the trace (#442).
      ctrl:SetEvtHandlerEnabled(false)
      ctrl:DeleteChildren(fileitem)
      ctrl:SetEvtHandlerEnabled(true)

      for _, edmarker in ipairs(getMarkers(editor)) do
        local line, markertype = unpack(edmarker)
        local text = ("%d: %s"):format(line+1,
          FixUTF8(editor:GetLineDyn(line), function(s) return '\\'..string.byte(s) end))
        ctrl:AppendItem(fileitem, text:gsub("[\r\n]+$",""), image[markertype:upper()])
      end

      -- if no markers added, then remove the file from the markers list
      ctrl:Expand(fileitem)
      if not ctrl:ItemHasChildren(fileitem) then
        ctrl:Delete(fileitem)
        cache.fileitem = nil
      end
    end
  end

  ctrl:Thaw()
  if win and win ~= ide:GetMainFrame():FindFocus() then win:SetFocus() end
end

local function item2editor(item_id)
  for editor, cache in pairs(caches) do
    if cache.fileitem and cache.fileitem:GetValue() == item_id:GetValue() then return editor end
  end
end

local function clearAllEditorMarkers(mtype, editor)
  for _, edmarker in ipairs(getMarkers(editor, mtype)) do
    local line = unpack(edmarker)
    editor:MarkerToggle(mtype, line, false)
  end
end

local function clearAllProjectMarkers(mtype)
  for filepath, markers in pairs(markers.settings.markers) do
    if ide:IsProjectSubDirectory(filepath) then
      local doc = ide:FindDocument(filepath)
      local editor = doc and doc:GetEditor()
      for m = #markers, 1, -1 do
        local line, markertype = unpack(markers[m])
        if markertype == mtype then
          if editor then
            editor:MarkerToggle(markertype, line, false)
          else
            table.remove(markers, m)
          end
        end
      end
    end
  end
end

local function createMarkersWindow()
  local width, height = 360, 200
  local ctrl = ide:CreateTreeCtrl(ide.frame, wx.wxID_ANY,
    wx.wxDefaultPosition, wx.wxSize(width, height),
    wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS + wx.wxTR_HIDE_ROOT + wx.wxNO_BORDER)

  markers.markersCtrl = ctrl
  ide.timers.markers = ide:AddTimer(ctrl, function() markersRefresh() end)

  ctrl:AddRoot("Markers")
  ctrl:SetImageList(markers.imglist)
  ctrl:SetFont(ide.font.tree)

  function ctrl:ActivateItem(item_id, marker)
    local itemimage = ctrl:GetItemImage(item_id)
    if itemimage == image.FILE then
      -- activate editor tab
      local editor = item2editor(item_id)
      if editor then ide:GetDocument(editor):SetActive() end
    else -- clicked on the marker item
      local parent = ctrl:GetItemParent(item_id)
      if parent:IsOk() and ctrl:GetItemImage(parent) == image.FILE then
        local editor = item2editor(parent)
        if editor then
          local line = tonumber(ctrl:GetItemText(item_id):match("^(%d+):"))
          if line then
            if marker then
              editor:MarkerToggle(marker, line-1, false)
              ctrl:Delete(item_id)
              return -- don't activate the editor when the breakpoint is toggled
            end
            editor:GotoLine(line-1)
            editor:EnsureVisibleEnforcePolicy(line-1)
          end
          ide:GetDocument(editor):SetActive()
        end
      end
    end
  end

  local function activateByPosition(event)
    local mask = (wx.wxTREE_HITTEST_ONITEMINDENT + wx.wxTREE_HITTEST_ONITEMLABEL
      + wx.wxTREE_HITTEST_ONITEMICON + wx.wxTREE_HITTEST_ONITEMRIGHT)
    local item_id, flags = ctrl:HitTest(event:GetPosition())

    if item_id and item_id:IsOk() and bit.band(flags, mask) > 0 then
      local marker
      local itemimage = ctrl:GetItemImage(item_id)
      if bit.band(flags, wx.wxTREE_HITTEST_ONITEMICON) > 0 then
        for iname, itype in pairs(image) do
          if itemimage == itype then marker = iname:lower() end
        end
      end
      ctrl:ActivateItem(item_id, marker)
    else
      event:Skip()
    end
    return true
  end

  local function clearMarkersInFile(item_id, marker)
    local editor = item2editor(item_id)
    local itemimage = ctrl:GetItemImage(item_id)
    if itemimage ~= image.FILE then
      local parent = ctrl:GetItemParent(item_id)
      if parent:IsOk() and ctrl:GetItemImage(parent) == image.FILE then
        editor = item2editor(parent)
      end
    end
    if editor then clearAllEditorMarkers(marker, editor) end
  end

  ctrl:Connect(wx.wxEVT_LEFT_DOWN, activateByPosition)
  ctrl:Connect(wx.wxEVT_LEFT_DCLICK, activateByPosition)
  ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, function(event)
      ctrl:ActivateItem(event:GetItem())
    end)

  ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU,
    function (event)
      local item_id = event:GetItem()
      local menu = ide:MakeMenu {
        { BOOKMARKTOGGLE, TR("Toggle Bookmark"), TR("Toggle bookmark") },
        { BREAKPOINTTOGGLE, TR("Toggle Breakpoint"), TR("Toggle breakpoint") },
        { },
        { ID.BOOKMARKFILECLEAR, TR("Clear Bookmarks In File")..KSC(ID.BOOKMARKFILECLEAR) },
        { ID.BREAKPOINTFILECLEAR, TR("Clear Breakpoints In File")..KSC(ID.BREAKPOINTFILECLEAR) },
        { },
        { ID.BOOKMARKPROJECTCLEAR, TR("Clear Bookmarks In Project")..KSC(ID.BOOKMARKPROJECTCLEAR) },
        { ID.BREAKPOINTPROJECTCLEAR, TR("Clear Breakpoints In Project")..KSC(ID.BREAKPOINTPROJECTCLEAR) },
      }
      local itemimage = ctrl:GetItemImage(item_id)

      menu:Enable(BOOKMARKTOGGLE, itemimage == image.BOOKMARK)
      menu:Connect(BOOKMARKTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED,
        function() ctrl:ActivateItem(item_id, "bookmark") end)

      menu:Enable(BREAKPOINTTOGGLE, itemimage == image.BREAKPOINT)
      menu:Connect(BREAKPOINTTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED,
        function() ctrl:ActivateItem(item_id, "breakpoint") end)

      menu:Enable(ID.BOOKMARKFILECLEAR, itemimage == image.BOOKMARK or itemimage == image.FILE)
      menu:Connect(ID.BOOKMARKFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
        function() clearMarkersInFile(item_id, "bookmark") end)

      menu:Enable(ID.BREAKPOINTFILECLEAR, itemimage == image.BREAKPOINT or itemimage == image.FILE)
      menu:Connect(ID.BREAKPOINTFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
        function() clearMarkersInFile(item_id, "breakpoint") end)

      PackageEventHandle("onMenuMarkers", menu, ctrl, event)

      ctrl:PopupMenu(menu)
    end)

  local function reconfigure(pane)
    pane:TopDockable(false):BottomDockable(false)
        :MinSize(150,-1):BestSize(300,-1):FloatingSize(200,300)
  end

  local layout = ide:GetSetting("/view", "uimgrlayout")
  local iconsize = ide:GetBestIconSize()
  if not layout or not layout:find("markerspanel") then
    ide:AddPanelDocked(ide:GetOutputNotebook(), ctrl, "markerspanel", TR("Markers"), reconfigure, false, ide:GetBitmap("OUTPUT", "MARKERS", wx.wxSize(iconsize, iconsize)))
  else
    ide:AddPanel(ctrl, "markerspanel", TR("Markers"), reconfigure, ide:GetBitmap("OUTPUT", "MARKERS", wx.wxSize(iconsize, iconsize)))
  end
end

local package = ide:AddPackage('core.markers', {
    onRegister = function(self)
      if not ide.config.markersinactivity then return end

      createMarkersWindow()

      local bmmenu = ide:FindMenuItem(ID.BOOKMARK):GetSubMenu()
      bmmenu:AppendSeparator()
      bmmenu:Append(ID.BOOKMARKFILECLEAR, TR("Clear Bookmarks In File")..KSC(ID.BOOKMARKFILECLEAR))
      bmmenu:Append(ID.BOOKMARKPROJECTCLEAR, TR("Clear Bookmarks In Project")..KSC(ID.BOOKMARKPROJECTCLEAR))

      local bpmenu = ide:FindMenuItem(ID.BREAKPOINT):GetSubMenu()
      bpmenu:AppendSeparator()
      bpmenu:Append(ID.BREAKPOINTFILECLEAR, TR("Clear Breakpoints In File")..KSC(ID.BREAKPOINTFILECLEAR))
      bpmenu:Append(ID.BREAKPOINTPROJECTCLEAR, TR("Clear Breakpoints In Project")..KSC(ID.BREAKPOINTPROJECTCLEAR))

      ide:GetMainFrame():Connect(ID.BOOKMARKFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
        function()
          local editor = ide:GetEditor()
          if editor then clearAllEditorMarkers("bookmark", editor) end
        end)
      ide:GetMainFrame():Connect(ID.BOOKMARKPROJECTCLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
        function() clearAllProjectMarkers("bookmark") end)

      ide:GetMainFrame():Connect(ID.BREAKPOINTFILECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
        function()
          local editor = ide:GetEditor()
          if editor then clearAllEditorMarkers("breakpoint", editor) end
        end)
      ide:GetMainFrame():Connect(ID.BREAKPOINTPROJECTCLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
        function() clearAllProjectMarkers("breakpoint") end)
    end,

    -- save markers; remove tab from the list
    onEditorClose = function(self, editor)
      local cache = caches[editor]
      if not cache then return end
      if cache.fileitem then markers.markersCtrl:Delete(cache.fileitem) end
      caches[editor] = nil
    end,

    -- schedule marker update if the change is for one of the editors with markers
    onEditorUpdateUI = function(self, editor, event)
      if not caches[editor] then return end
      if bit.band(event:GetUpdated(), wxstc.wxSTC_UPDATE_CONTENT) == 0 then return end
      needRefresh(editor)
    end,

    onEditorMarkerUpdate = function(self, editor)
      -- if no marker, then all markers in a file need to be refreshed
      if not caches[editor] then caches[editor] = {} end
      needRefresh(editor)
      -- delay saving markers as other EditorMarkerUpdate handlers may still modify them,
      -- but check to make sure that the editor is still valid
      ide:DoWhenIdle(function()
          if ide:IsValidCtrl(editor) then markers:SaveMarkers(editor) end
        end)
    end,

    onEditorSave = function(self, editor) markers:SaveMarkers(editor) end,
    onEditorLoad = function(self, editor) markers:LoadMarkers(editor) end,
  })

function markers:SaveSettings() package:SetSettings(self.settings) end

function markers:SaveMarkers(editor, force)
  -- if the file has the name and has not been modified, save the breakpoints
  -- this also works when the file is saved as the modified flag is already set to `false`
  local doc = ide:GetDocument(editor)
  local filepath = doc and doc:GetFilePath()
  if filepath and (force or not doc:IsModified()) then
    -- remove it from the list if it has no breakpoints
    local edmarkers = getMarkers(editor)
    self.settings.markers[filepath] = #edmarkers > 0 and edmarkers or nil
    self:SaveSettings()
  end
end

function markers:LoadMarkers(editor)
  local doc = ide:GetDocument(editor)
  local filepath = doc:GetFilePath()
  if filepath then
    for _, edmarker in ipairs(self.settings.markers[filepath] or {}) do
      local line, markertype = unpack(edmarker)
      editor:MarkerToggle(markertype, line, true)
    end
  end
end

MergeSettings(markers.settings, package:GetSettings())
