-- Copyright 2011-16 Paul Kulchenko, ZeroBrane LLC
-- styles for comment markup
---------------------------------------------------------

local ide = ide
local MD_MARK_ITAL = '_' -- italic
local MD_MARK_BOLD = '**' -- bold
local MD_MARK_LINK = '[' -- link description start
local MD_MARK_LINZ = ']' -- link description end
local MD_MARK_LINA = '(' -- link URL start
local MD_MARK_LINT = ')' -- link URL end
local MD_MARK_HEAD = '#' -- header
local MD_MARK_CODE = '`' -- code
local MD_MARK_BOXD = '|' -- highlight
local MD_MARK_MARK = ' ' -- separator
local MD_LINK_NEWWINDOW = '+' -- indicator to open a new window for links
-- old versions of Scintilla had only 5-bit styles, so assign styles manually in those cases
local markup = {
  [MD_MARK_BOXD] = {st=ide:AddStyle("markup.boxd", ide.STYLEMASK == 31 and 25 or nil), fg={127,0,127}, b=true},
  [MD_MARK_CODE] = {st=ide:AddStyle("markup.code", ide.STYLEMASK == 31 and 26 or nil), fg={127,127,127}, fs=10},
  [MD_MARK_HEAD] = {st=ide:AddStyle("markup.head", ide.STYLEMASK == 31 and 27 or nil), fn="Lucida Console", b=true},
  [MD_MARK_LINK] = {st=ide:AddStyle("markup.link", ide.STYLEMASK == 31 and 28 or nil), u=true, hs={32,32,127}},
  [MD_MARK_BOLD] = {st=ide:AddStyle("markup.bold", ide.STYLEMASK == 31 and 29 or nil), b=true},
  [MD_MARK_ITAL] = {st=ide:AddStyle("markup.ital", ide.STYLEMASK == 31 and 30 or nil), i=true},
  [MD_MARK_MARK] = {st=ide:AddStyle("markup.mark", ide.STYLEMASK == 31 and 31 or nil), v=false},
}

-- allow other editor features to recognize this special markup
function MarkupIsSpecial(style) return style == markup[MD_MARK_MARK].st end
function MarkupIsAny(style)
  for _, mark in pairs(markup) do
    if style == mark.st then return true end
  end
  return false
end
function MarkupAddStyles(styles)
  local comment = styles.comment or {}
  for key,value in pairs(markup) do
    local style = styles[key] or {}
    -- copy all style features by value
    for feature in pairs(value) do
      style[feature] = style[feature] or value[feature]
    end
    style.fg = style.fg or comment.fg
    style.bg = style.bg or comment.bg
    styles[key] = style
  end
end

local q = EscapeMagic

local MD_MARK_PTRN = ''  -- combination of all markup marks that can start styling
for key in pairs(markup) do
  if key ~= MD_MARK_MARK then MD_MARK_PTRN = MD_MARK_PTRN .. q(key) end
end
MarkupAddStyles(ide.config.styles)

function MarkupHotspotClick(pos, editor)
  -- check if this is "our" hotspot event
  if bit.band(editor:GetStyleAt(pos),ide.STYLEMASK) ~= markup[MD_MARK_LINK].st then
    -- not "our" style, so nothing to do for us here
    return
  end
  local line = editor:LineFromPosition(pos)
  local tx = editor:GetLineDyn(line)
  pos = pos + #MD_MARK_LINK - editor:PositionFromLine(line) -- turn into relative position

  -- extract the URL/command on the right side of the separator
  local _,_,text = string.find(tx, q(MD_MARK_LINZ).."(%b"..MD_MARK_LINA..MD_MARK_LINT..")", pos)
  if text then
    text = text:gsub("^"..q(MD_MARK_LINA), ""):gsub(q(MD_MARK_LINT).."$", "")
    local doc = ide:GetDocument(editor)
    local filepath = doc and doc.filePath or ide:GetProject()
    local _,_,http = string.find(text, [[^(https?:%S+)$]])
    local _,_,command,code = string.find(text, [[^macro:(%w+)%((.*%S)%)$]])
    if not command then _,_,command = string.find(text, [[^macro:(%w+)$]]) end

    if command == 'shell' then
      ShellExecuteCode(code, true)
    elseif command == 'inline' then
      ShellExecuteInline(code, true)
    elseif command == 'run' then -- run the current file
      ProjectRun()
    elseif command == 'debug' then -- debug the current file
      ProjectDebug()
    elseif http then -- open the URL in a new browser window
      wx.wxLaunchDefaultBrowser(http, 0)
    elseif filepath then -- only check for saved files
      -- check if requested to open in a new window
      local newwindow = not doc or string.find(text, MD_LINK_NEWWINDOW, 1, true)
      if newwindow then text = string.gsub(text, "^%" .. MD_LINK_NEWWINDOW, "") end
      local filename = GetFullPathIfExists(
        wx.wxFileName(filepath):GetPath(wx.wxPATH_GET_VOLUME), text)
      if filename and
        (newwindow or SaveModifiedDialog(editor, true) ~= wx.wxID_CANCEL) then
        if not newwindow and ide.osname == 'Macintosh' then editor:GotoPos(0) end
        LoadFile(filename,not newwindow and editor or nil,true)
      end
    end
  end
  return true
end

local function ismarkup (tx)
  local start = 1
  local marksep = "[%s!%?%.,;:%(%)]"
  while true do
    -- find a separator first
    local st,_,sep,more = string.find(tx, "(["..MD_MARK_PTRN.."])(.)", start)
    if not st then return end

    -- check if this is a first character of a multi-character separator
    if not markup[sep] then sep = sep .. (more or '') end

    local s,e,cap
    local qsep = q(sep)
    local nonspace = "[^%s]"
    if sep == MD_MARK_HEAD then
      -- always search from the start of the line
      -- [%w%p] set is needed to avoid continuing this markup to the next line
      s,e,cap = string.find(tx,"^("..q(MD_MARK_HEAD)..".+[%w%p])")
    elseif sep == MD_MARK_LINK then
      -- allow everything based on balanced link separators
      s,e,cap = string.find(tx,
        "^(%b"..MD_MARK_LINK..MD_MARK_LINZ
        .."%b"..MD_MARK_LINA..MD_MARK_LINT..")", st)
      -- if either part of the link is empty `[]` or `()`, skip the match
      if cap and cap:find("^"..q(MD_MARK_LINK..MD_MARK_LINZ))
      or cap and cap:find(q(MD_MARK_LINA..MD_MARK_LINT).."$") then s = nil end
    elseif markup[sep] then
      -- try a single character first, then 2+ characters between separators;
      -- this is to handle "`5` `6`" as two sequences, not one.
      s,e,cap = string.find(tx,"^("..qsep..nonspace..qsep..")".."%f"..marksep, st)
      if not s then s,e,cap = string.find(tx,"^("..qsep..nonspace..".-"..nonspace..qsep..")".."%f"..marksep, st) end
    end
    if s and -- selected markup is surrounded by spaces or punctuation marks
      (s == 1   or tx:sub(s-1, s-1):match(marksep)) and
      (e == #tx or tx:sub(e+1, e+1):match(marksep))
      then return s,e,cap,sep end
    start = st+1
  end
end

function MarkupStyle(editor, lines, linee)
  local lines = lines or 0
  if (lines < 0) then return end

  -- if the current spec doesn't have any comments, nothing to style
  if not next(editor.spec.iscomment) then return end

  -- always style to the end as there may be comments that need re-styling
  -- technically, this should be GetLineCount()-1, but we want to style
  -- beyond the last line to make sure it is styled correctly
  local linec = editor:GetLineCount()
  local linee = linee or linec

  local linecomment = editor.spec.linecomment
  local iscomment = {}
  for i,v in pairs(editor.spec.iscomment) do
    iscomment[i] = v
  end

  local es = editor:GetEndStyled()
  local needfix = false

  for line=lines,linee do
    local tx = editor:GetLineDyn(line)
    local ls = editor:PositionFromLine(line)

    local from = 1
    local off = -1

    -- doing WrapCount(line) when line == linec (which may be beyond
    -- the last line) occasionally crashes the application on OSX.
    local wrapped = line < linec and editor:WrapCount(line) or 0

    while from do
      tx = string.sub(tx,from)
      local f,t,w,mark = ismarkup(tx)

      if (f) then
        local p = ls+f+off
        local s = bit.band(editor:GetStyleAt(p), ide.STYLEMASK)
        -- only style comments and only those that are not at the beginning
        -- of the file to avoid styling shebang (#!) lines
        -- also ignore matches for line comments (as defined in the spec)
        if iscomment[s] and p > 0 and mark ~= linecomment then
          local smark = #mark
          local emark = #mark -- assumes end mark is the same length as start mark
          if mark == MD_MARK_HEAD then
            -- grab multiple MD_MARK_HEAD if present
            local _,_,full = string.find(w,"^("..q(MD_MARK_HEAD).."+)")
            smark,emark = #full,0
          elseif mark == MD_MARK_LINK then
            local lsep = w:find(q(MD_MARK_LINZ)..q(MD_MARK_LINA))
            if lsep then emark = #w-lsep+#MD_MARK_LINT end
          end
          local sp = bit.band(editor:GetStyleAt(p-1), ide.STYLEMASK) -- previous position style
          if mark == MD_MARK_HEAD and not iscomment[sp] then
            p = p + 1
            smark = smark - 1
          end
          -- StartStyling deprecated the second parameter, but since a version check
          -- is not available for Scintilla, we check for INDIC0_MASK, which was
          -- removed in the same wxwidgets commit
          editor:StartStyling(p, wxstc.wxSTC_INDIC0_MASK and ide.STYLEMASK or 0)
          editor:SetStyling(smark, markup[MD_MARK_MARK].st)
          editor:SetStyling(t-f+1-smark-emark, markup[mark].st or markup[MD_MARK_MARK].st)
          editor:SetStyling(emark, markup[MD_MARK_MARK].st)
        end

        off = off + t
      end
      from = t and (t+1)
    end

    -- has this line changed its wrapping because of invisible styling?
    if wrapped > 1 and editor:WrapCount(line) < wrapped then needfix = true end
  end
  editor:StartStyling(es, wxstc.wxSTC_INDIC0_MASK and ide.STYLEMASK or 0)

  -- if any wrapped lines have changed, then reset WrapMode to fix the drawing
  if needfix then
    -- this fixes an issue with duplicate lines in Scintilla when
    -- invisible styles hide some of the content that would be wrapped.
    local wrapmode = editor:GetWrapMode()
    if wrapmode ~= wxstc.wxSTC_WRAP_NONE then
      -- change the wrap mode to force recalculation
      editor:SetWrapMode(wxstc.wxSTC_WRAP_NONE)
      editor:SetWrapMode(wrapmode)
    end
    -- if some of the lines have folded, this can make not styled lines visible
    MarkupStyle(editor, linee+1) -- style to the end in this case
  end
end
