--- @brief ---
help
---:[range]TOhtml {file}                                                *:TOhtml*
---Converts the buffer shown in the current window to HTML, opens the generated
---HTML in a new split window, and saves its contents to {file}. If {file} is not
---given, a temporary file (created by |tempname()|) is used.
---
-- The HTML conversion script is different from Vim's one. If you want to use
-- Vim's TOhtml converter, download it from the vim GitHub repo.
-- Here are the Vim files related to this functionality:
-- - https://github.com/vim/vim/blob/master/runtime/syntax/2html.vim
-- - https://github.com/vim/vim/blob/master/runtime/autoload/tohtml.vim
-- - https://github.com/vim/vim/blob/master/runtime/plugin/tohtml.vim
--
-- Main differences between this and the vim version:
-- - No "ignore some visual thing" settings (just set the right Vim option)
-- - No support for legacy web engines
-- - No support for legacy encoding (supports only UTF-8)
-- - No interactive webpage
-- - No specifying the internal HTML (no XHTML, no use_css=false)
-- - No multiwindow diffs
-- - No ranges
--
-- Remarks:
-- - Not all visuals are supported, so it may differ.
--- @class (private) vim.tohtml.state.global
--- @field background string
--- @field foreground string
--- @field title string|false
--- @field font string
--- @field highlights_name table')
  local out_start = #out
  local hide_count = 0
  --- @type integer[]
  local stack = {}
  local before = ''
  local after = ''
  local function loop(row)
    local inside = row <= state.end_ and row >= state.start
    local style_line = styletable[row]
    if style_line.hide and (styletable[row - 1] or {}).hide then
      return
    end
    if inside then
      _extend_virt_lines(out, state, row)
    end
    --Possible improvement (altermo):
    --Instead of looping over all the buffer characters per line,
    --why not loop over all the style_line cells,
    --and then calculating the amount of text.
    if style_line.hide then
      return
    end
    local line = vim.api.nvim_buf_get_lines(state.bufnr, row - 1, row, false)[1] or ''
    local s = ''
    if inside then
      s = s .. _pre_text_to_html(state, row)
    end
    local true_line_len = #line + 1
    for k in pairs(style_line) do
      if type(k) == 'number' and k > true_line_len then
        true_line_len = k
      end
    end
    for col = 1, true_line_len do
      local cell = style_line[col]
      --- @type table?
      local char
      if cell then
        for i = #cell[2], 1, -1 do
          local hlid = cell[2][i]
          if hlid < 0 then
            if hlid == HIDE_ID then
              hide_count = hide_count - 1
            end
          else
            --- @type integer?
            local index
            for idx = #stack, 1, -1 do
              s = s .. (name_to_closetag(state.highlights_name[stack[idx]]))
              if stack[idx] == hlid then
                index = idx
                break
              end
            end
            assert(index, 'a coles tag which has no corresponding open tag')
            for idx = index + 1, #stack do
              s = s .. (name_to_tag(state.highlights_name[stack[idx]]))
            end
            table.remove(stack, index)
          end
        end
        for _, hlid in ipairs(cell[1]) do
          if hlid < 0 then
            if hlid == HIDE_ID then
              hide_count = hide_count + 1
            end
          else
            table.insert(stack, hlid)
            s = s .. (name_to_tag(state.highlights_name[hlid]))
          end
        end
        if cell[3] and inside then
          s = s .. _virt_text_to_html(state, cell)
        end
        char = cell[4][#cell[4]]
      end
      if col == true_line_len and not char then
        break
      end
      if hide_count == 0 and inside then
        s = s
          .. _char_to_html(
            state,
            char
              or { vim.api.nvim_buf_get_text(state.bufnr, row - 1, col - 1, row - 1, col, {})[1] }
          )
      end
    end
    if row > state.end_ + 1 then
      after = after .. s
    elseif row < state.start then
      before = s .. before
    else
      table.insert(out, s)
    end
  end
  for row = 1, vim.api.nvim_buf_line_count(state.bufnr) + 1 do
    loop(row)
  end
  out[out_start] = out[out_start] .. before
  out[#out] = out[#out] .. after
  assert(#stack == 0, 'an open HTML tag was never closed')
  table.insert(out, '')
end
--- @param out string[]
--- @param fn fun()
local function extend_body(out, fn)
  table.insert(out, '')
  fn()
  table.insert(out, '')
end
--- @param out string[]
--- @param fn fun()
local function extend_html(out, fn)
  table.insert(out, '')
  table.insert(out, '')
  fn()
  table.insert(out, '')
end
--- @param winid integer
--- @param global_state vim.tohtml.state.global
--- @return vim.tohtml.state
local function global_state_to_state(winid, global_state)
  local bufnr = vim.api.nvim_win_get_buf(winid)
  local opt = global_state.conf
  local width = opt.width or vim.bo[bufnr].textwidth
  if not width or width < 1 then
    width = vim.api.nvim_win_get_width(winid)
  end
  local range = opt.range or { 1, vim.api.nvim_buf_line_count(bufnr) }
  local state = setmetatable({
    winid = winid == 0 and vim.api.nvim_get_current_win() or winid,
    opt = vim.wo[winid],
    style = generate_styletable(bufnr),
    bufnr = bufnr,
    tabstop = (' '):rep(vim.bo[bufnr].tabstop),
    width = width,
    start = range[1],
    end_ = range[2],
  }, { __index = global_state })
  return state --[[@as vim.tohtml.state]]
end
--- @param opt vim.tohtml.opt
--- @param title? string
--- @return vim.tohtml.state.global
local function opt_to_global_state(opt, title)
  local fonts = {}
  if opt.font then
    fonts = type(opt.font) == 'string' and { opt.font } or opt.font --[[@as (string[])]]
    for i, v in pairs(fonts) do
      fonts[i] = ('"%s"'):format(v)
    end
  elseif vim.o.guifont:match('^[^:]+') then
    -- Example:
    -- Input: "Font,Escape\,comma, Ignore space after comma"
    -- Output: { "Font","Escape,comma","Ignore space after comma" }
    local prev = ''
    for name in vim.gsplit(vim.o.guifont:match('^[^:]+'), ',', { trimempty = true }) do
      if vim.endswith(name, '\\') then
        prev = prev .. vim.trim(name:sub(1, -2) .. ',')
      elseif vim.trim(name) ~= '' then
        table.insert(fonts, ('"%s%s"'):format(prev, vim.trim(name)))
        prev = ''
      end
    end
  end
  -- Generic family names (monospace here) must not be quoted
  -- because the browser recognizes them as font families.
  table.insert(fonts, 'monospace')
  --- @type vim.tohtml.state.global
  local state = {
    background = get_background_color(),
    foreground = get_foreground_color(),
    title = opt.title or title or false,
    font = table.concat(fonts, ','),
    highlights_name = {},
    conf = opt,
  }
  return state
end
--- @type fun(state: vim.tohtml.state)[]
local styletable_funcs = {
  styletable_syntax,
  styletable_diff,
  styletable_treesitter,
  styletable_match,
  styletable_extmarks,
  styletable_conceal,
  styletable_listchars,
  styletable_folds,
  styletable_statuscolumn,
}
--- @param state vim.tohtml.state
local function state_generate_style(state)
  vim._with({ win = state.winid }, function()
    for _, fn in ipairs(styletable_funcs) do
      --- @type string?
      local cond
      if type(fn) == 'table' then
        cond = fn[2] --[[@as string]]
        --- @type function
        fn = fn[1]
      end
      if not cond or cond(state) then
        fn(state)
      end
    end
  end)
end
--- @param winid integer
--- @param opt? vim.tohtml.opt
--- @return string[]
local function win_to_html(winid, opt)
  opt = opt or {}
  local title = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(winid))
  local global_state = opt_to_global_state(opt, title)
  local state = global_state_to_state(winid, global_state)
  state_generate_style(state)
  local html = {}
  extend_html(html, function()
    extend_head(html, global_state)
    extend_body(html, function()
      extend_pre(html, state)
    end)
  end)
  return html
end
local M = {}
--- @class vim.tohtml.opt
--- @inlinedoc
---
--- Title tag to set in the generated HTML code.
--- (default: buffer name)
--- @field title? string|false
---
--- Show line numbers.
--- (default: `false`)
--- @field number_lines? boolean
---
--- Fonts to use.
--- (default: `guifont`)
--- @field font? string[]|string
---
--- Width used for items which are either right aligned or repeat a character
--- infinitely.
--- (default: 'textwidth' if non-zero or window width otherwise)
--- @field width? integer
---
--- Range of rows to use.
--- (default: entire buffer)
--- @field range? integer[]
--- Converts the buffer shown in the window {winid} to HTML and returns the output as a list of string.
--- @param winid? integer Window to convert (defaults to current window)
--- @param opt? vim.tohtml.opt Optional parameters.
--- @return string[]
function M.tohtml(winid, opt)
  return win_to_html(winid or 0, opt)
end
return M