-- mod-version:3 --priority:10
local core = require "core"
local common = require "core.common"
local DocView = require "core.docview"
local Doc = require "core.doc"
local style = require "core.style"
local config = require "core.config"
local command = require "core.command"
local keymap = require "core.keymap"
local translate = require "core.doc.translate"

local RESIZE_REBUILD_DEBOUNCE_SECONDS = 0.15

local function get_font(default_font, type, indent_size)
  local font = style.syntax_fonts[type] or default_font
  if font ~= default_font then font:set_tab_size(indent_size) end
  return font
end

local function get_width(font, text, tab_offset)
  return font:get_width(text, { tab_offset = tab_offset })
end

---Configuration options for `linewrapping` plugin.
---@class config.plugins.linewrapping
---The type of wrapping to perform. Can be "letter" or "word".
---@field mode "letter" | "word"
---If nil, uses the DocView's size, otherwise, uses this exact width. Can be a function.
---@field width_override? number | function():number
---Whether or not to draw a guide
---@field guide boolean
---Whether or not we should indent ourselves like the first line of a wrapped block.
---@field indent boolean
---Whether or not to enable wrapping by default when opening files.
---@field enable_by_default boolean
---Requires tokenization
---@field require_tokenization boolean
config.plugins.linewrapping = common.merge({
	-- The type of wrapping to perform. Can be "letter" or "word".
  mode = "letter",
	-- If nil, uses the DocView's size, otherwise, uses this exact width. Can be a function.
  width_override = nil,
	-- Whether or not to draw a guide
  guide = true,
  -- Whether or not we should indent ourselves like the first line of a wrapped block.
  indent = true,
  -- Whether or not to enable wrapping by default when opening files.
  enable_by_default = false,
  -- Requires tokenization
  require_tokenization = false,
  -- The config specification used by gui generators
  config_spec = {
    name = "Line Wrapping",
    {
      label = "Mode",
      description = "The type of wrapping to perform.",
      path = "mode",
      type = "selection",
      default = "letter",
      values = {
        {"Letters", "letter"},
        {"Words", "word"}
      }
    },
    {
      label = "Guide",
      description = "Whether or not to draw a guide.",
      path = "guide",
      type = "toggle",
      default = true
    },
    {
      label = "Indent",
      description = "Whether or not to follow the indentation of wrapped line.",
      path = "indent",
      type = "toggle",
      default = true
    },
    {
      label = "Enable by Default",
      description = "Whether or not to enable wrapping by default when opening files.",
      path = "enable_by_default",
      type = "toggle",
      default = false
    },
    {
      label = "Require Tokenization",
      description = "Use tokenization when applying wrapping.",
      path = "require_tokenization",
      type = "toggle",
      default = false
    }
  }
}, config.plugins.linewrapping)

---@class plugins.linewrapping
local LineWrapping = {}

-- Optimzation function. The tokenizer is relatively slow (at present), and
-- so if we don't need to run it, should be run sparingly.
local function spew_tokens(doc, line) if line < math.huge then return math.huge, "normal", doc:get_utf8_line(line) end end
local function get_tokens(doc, line)
  if config.plugins.linewrapping.require_tokenization then
    return doc.highlighter:each_token(line)
  end
  return spew_tokens, doc, line
end

-- Computes the breaks for a given line, width and mode. Returns a list of columns
-- at which the line should be broken.
function LineWrapping.compute_line_breaks(doc, default_font, line, width, mode)
  local xoffset, last_i, i, last_space, last_width, begin_width = 0, 1, 1, nil, 0, 0
  local splits = { 1 }
  local _, indent_size = doc:get_indent_info()
  default_font:set_tab_size(indent_size)
  for idx, type, text in get_tokens(doc, line) do
    local font = get_font(default_font, type, indent_size)
    if idx == 1 or idx == math.huge and config.plugins.linewrapping.indent then
      local _, indent_end = text:find("^%s+")
      if indent_end then
        begin_width = get_width(font, text:sub(1, indent_end), 0)
      end
    end
    local w = get_width(font, text, xoffset)
    if xoffset + w > width then
      for char in common.utf8_chars(text) do
        w = get_width(font, char, xoffset)
        xoffset = xoffset + w
        if xoffset > width then
          if mode == "word" and last_space then
            table.insert(splits, last_space + 1)
            xoffset = w + begin_width + (xoffset - last_width)
          else
            table.insert(splits, i)
            xoffset = begin_width
            w = get_width(font, char, xoffset)
            xoffset = xoffset + w
          end
          last_space = nil
        elseif char == ' ' then
          last_space = i
          last_width = xoffset
        end
        i = i + #char
      end
    else
      xoffset = xoffset + w
      i = i + #text
    end
  end
  return splits, begin_width
end

local function rebuild_line_to_idx(docview)
  local line_to_idx = {}
  local last_line = nil
  for i = 1, #docview.wrapped_lines, 2 do
    local line = docview.wrapped_lines[i]
    if line ~= last_line then
      line_to_idx[line] = (i + 1) / 2
      last_line = line
    end
  end
  docview.wrapped_line_to_idx = line_to_idx
end

local function compute_line_rows(docview, line, width)
  local breaks, begin_width = LineWrapping.compute_line_breaks(
    docview.doc,
    docview.wrapped_settings.font,
    line,
    width,
    config.plugins.linewrapping.mode
  )
  local rows = {}
  for i = 1, #breaks do
    rows[#rows + 1] = line
    rows[#rows + 1] = breaks[i]
  end
  return rows, begin_width
end

local function clear_pending_resize(docview)
  docview.wrapped_pending_width = nil
  docview.wrapped_pending_font = nil
  docview.wrapped_rebuild_at = nil
end

-- breaks are held in a single table that contains n*2 elements, where n is the
-- amount of line breaks. Each element represents line and column of the break.
function LineWrapping.reconstruct_breaks(docview, default_font, width)
  clear_pending_resize(docview)
  if width ~= math.huge then
    local doc = docview.doc
    -- two elements per wrapped line; first maps to original line number, second to column number.
    docview.wrapped_lines = { }
    -- one element per actual line; maps to the first index of in wrapped_lines for this line
    docview.wrapped_line_to_idx = { }
    -- one element per actual line; gives the indent width for the acutal line
    docview.wrapped_line_offsets = { }
    docview.wrapped_settings = { ["width"] = width, ["font"] = default_font }
    for i = 1, #doc.lines do
      local breaks, offset = LineWrapping.compute_line_breaks(doc, default_font, i, width, config.plugins.linewrapping.mode)
      docview.wrapped_line_offsets[#docview.wrapped_line_offsets + 1] = offset
      for k, col in ipairs(breaks) do
        docview.wrapped_lines[#docview.wrapped_lines + 1] = i
        docview.wrapped_lines[#docview.wrapped_lines + 1] = col
      end
    end
    rebuild_line_to_idx(docview)
  else
    docview.wrapped_lines = nil
    docview.wrapped_line_to_idx = nil
    docview.wrapped_line_offsets = nil
    docview.wrapped_settings = nil
  end
  docview:invalidate_visual_lines()
end

-- When we have an insertion or deletion, we have four sections of text.
-- 1. The unaffected section, located prior to the cursor. This is completely ignored.
-- 2. The beginning of the affected line prior to the insertion or deletion. Begins on column 1 of the selection.
-- 3. The removed/pasted lines.
-- 4. Every line after the modification, begins one line after the selection in the initial document.
function LineWrapping.update_breaks(docview, old_line1, old_line2, net_lines)
  if not docview.wrapped_settings
  or not docview.wrapped_lines
  or not docview.wrapped_line_to_idx
  or not docview.wrapped_line_offsets then
    LineWrapping.update_docview_breaks(docview)
    return
  end

  clear_pending_resize(docview)

  local old_rows = docview.wrapped_lines
  local old_offsets = docview.wrapped_line_offsets
  local total_rows = #old_rows / 2
  local old_idx1 = docview.wrapped_line_to_idx[old_line1] or (total_rows + 1)
  local old_idx2 = (docview.wrapped_line_to_idx[old_line2 + 1] or (total_rows + 1)) - 1
  local old_row_start = (old_idx1 - 1) * 2 + 1
  local old_row_stop = old_idx2 * 2

  local new_line1 = old_line1
  local new_line2 = math.min(old_line2 + net_lines, #docview.doc.lines)
  local inserted_rows = {}
  local inserted_offsets = {}
  if new_line1 <= new_line2 then
    for line = new_line1, new_line2 do
      local rows, begin_width = compute_line_rows(docview, line, docview.wrapped_settings.width)
      inserted_offsets[#inserted_offsets + 1] = begin_width
      for i = 1, #rows do
        inserted_rows[#inserted_rows + 1] = rows[i]
      end
    end
  end

  local wrapped_lines = {}
  for i = 1, old_row_start - 1 do
    wrapped_lines[#wrapped_lines + 1] = old_rows[i]
  end
  for i = 1, #inserted_rows do
    wrapped_lines[#wrapped_lines + 1] = inserted_rows[i]
  end
  for i = old_row_stop + 1, #old_rows, 2 do
    wrapped_lines[#wrapped_lines + 1] = old_rows[i] + net_lines
    wrapped_lines[#wrapped_lines + 1] = old_rows[i + 1]
  end

  local wrapped_line_offsets = {}
  for line = 1, old_line1 - 1 do
    wrapped_line_offsets[#wrapped_line_offsets + 1] = old_offsets[line]
  end
  for i = 1, #inserted_offsets do
    wrapped_line_offsets[#wrapped_line_offsets + 1] = inserted_offsets[i]
  end
  for line = old_line2 + 1, #old_offsets do
    wrapped_line_offsets[#wrapped_line_offsets + 1] = old_offsets[line]
  end

  docview.wrapped_lines = wrapped_lines
  docview.wrapped_line_offsets = wrapped_line_offsets
  rebuild_line_to_idx(docview)
  docview:invalidate_visual_lines(old_line1)
end

local function get_wrap_width(docview)
  local scrollbar_width = docview.v_scrollbar.expanded_size or style.expanded_scrollbar_size
  local width_override = config.plugins.linewrapping.width_override
  return (type(width_override) == "function" and width_override(docview))
    or width_override
    or (docview.size.x - docview:get_gutter_width() - scrollbar_width)
end

local function schedule_resize_rebuild(docview, width)
  local now = system.get_time()
  if docview.wrapped_pending_width ~= width then
    docview.wrapped_pending_width = width
    docview.wrapped_pending_font = docview:get_font()
    docview.wrapped_rebuild_at = now + RESIZE_REBUILD_DEBOUNCE_SECONDS
  end
  core.redraw = true
  return docview.wrapped_rebuild_at and now >= docview.wrapped_rebuild_at
end

function LineWrapping.update_docview_breaks(docview)
  local width = get_wrap_width(docview)
  if width == math.huge then
    if docview.wrapped_settings then
      LineWrapping.reconstruct_breaks(docview, docview:get_font(), width)
    end
    return
  end

  if not docview.wrapped_settings or docview.wrapped_settings.width == nil then
    docview.scroll.to.x = 0
    LineWrapping.reconstruct_breaks(docview, docview:get_font(), width)
    return
  end

  if width ~= docview.wrapped_settings.width then
    docview.scroll.to.x = 0
    if not docview.wrapped_lines then
      LineWrapping.reconstruct_breaks(docview, docview:get_font(), width)
    elseif schedule_resize_rebuild(docview, width) then
      LineWrapping.reconstruct_breaks(
        docview,
        docview.wrapped_pending_font or docview:get_font(),
        width
      )
    end
    return
  end

  if docview.wrapped_pending_width then
    clear_pending_resize(docview)
  end
end

-- Draws a guide if applicable to show where wrapping is occurring.
function LineWrapping.draw_guide(docview)
  if config.plugins.linewrapping.guide and docview.wrapped_settings.width ~= math.huge then
    local x, y = docview:get_content_offset()
    local gw = docview:get_gutter_width()
    local width = docview.wrapped_pending_width or docview.wrapped_settings.width
    renderer.draw_rect(x + gw + width, y, 1, core.root_view.size.y, style.selection)
  end
end

local function get_idx_line_col(docview, idx)
  local doc = docview.doc
  if not docview.wrapped_settings then
    if idx > #doc.lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end
    return idx, 1
  end
  if idx < 1 then return 1, 1 end
  local offset = (idx - 1) * 2 + 1
  if offset > #docview.wrapped_lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end
  return docview.wrapped_lines[offset], docview.wrapped_lines[offset + 1]
end

local function get_idx_line_length(docview, idx)
  local doc = docview.doc
  if not docview.wrapped_settings then
    if idx > #doc.lines then return #doc.lines[#doc.lines] + 1 end
    return #doc.lines[idx]
  end
  local offset = (idx - 1) * 2 + 1
  local start = docview.wrapped_lines[offset + 1]
  if docview.wrapped_lines[offset + 2] and docview.wrapped_lines[offset + 2] == docview.wrapped_lines[offset] then
    return docview.wrapped_lines[offset + 3] - docview.wrapped_lines[offset + 1]
  else
    return #doc.lines[docview.wrapped_lines[offset]] - docview.wrapped_lines[offset + 1] + 1
  end
end

local function get_total_wrapped_lines(docview)
  if not docview.wrapped_settings then return docview.doc and #docview.doc.lines end
  return #docview.wrapped_lines / 2
end

-- If line end, gives the end of an index line, rather than the first character of the next line.
local function get_line_idx_col_count(docview, line, col, line_end, ndoc)
  local doc = docview.doc
  if not docview.wrapped_settings then return common.clamp(line, 1, #doc.lines), col, 1, 1 end
  if line > #doc.lines then return get_line_idx_col_count(docview, #doc.lines, #doc.lines[#doc.lines] + 1) end
  line = math.max(line, 1)
  local idx = docview.wrapped_line_to_idx[line] or 1
  local ncol, scol = 1, 1
  if col then
    local i = idx + 1
    while line == docview.wrapped_lines[(i - 1) * 2 + 1] and col >= docview.wrapped_lines[(i - 1) * 2 + 2] do
      local nscol = docview.wrapped_lines[(i - 1) * 2 + 2]
      if line_end and col == nscol then
        break
      end
      scol = nscol
      i = i + 1
      idx = idx + 1
    end
    ncol = (col - scol) + 1
  end
  local count = (docview.wrapped_line_to_idx[line + 1] or (get_total_wrapped_lines(docview) + 1)) - (docview.wrapped_line_to_idx[line] or get_total_wrapped_lines(docview))
  return idx, ncol, count, scol
end

local get_line_col_from_index_and_x

local old_get_line_wraps = DocView.get_line_wraps
function DocView:get_line_wraps(line)
  if not self.wrapped_settings then
    return old_get_line_wraps(self, line)
  end
  local idx = self.wrapped_line_to_idx[line]
  if not idx then return nil end
  local next_idx = self.wrapped_line_to_idx[line + 1] or (get_total_wrapped_lines(self) + 1)
  local starts = {}
  for i = idx, next_idx - 1 do
    local _, col = get_idx_line_col(self, i)
    starts[#starts + 1] = col
  end
  return starts
end

local old_has_variable_visual_lines = DocView.has_variable_visual_lines
function DocView:has_variable_visual_lines()
  return self.wrapped_settings ~= nil or old_has_variable_visual_lines(self)
end

function DocView:get_visual_line_col_from_x(row, x)
  if not self.wrapped_settings then
    return self:get_x_offset_col(self:visual_position_from_row(row), x)
  end
  local _, col = get_line_col_from_index_and_x(self, row, x)
  return col
end

function get_line_col_from_index_and_x(docview, idx, x)
  local doc = docview.doc
  local line, col = get_idx_line_col(docview, idx)
  if idx < 1 then return 1, 1 end
  local next_line, next_col = get_idx_line_col(docview, idx + 1)
  local row_end_col = #doc.lines[line]
  if next_line == line then
    local _, previous_col = translate.previous_char(doc, line, next_col)
    row_end_col = previous_col
  end
  local xoffset, last_i, i = (col ~= 1 and docview.wrapped_line_offsets[line] or 0), col, 1
  if x < xoffset then return line, col end
  local default_font = docview:get_font()
  local _, indent_size = doc:get_indent_info()
  default_font:set_tab_size(indent_size)
  for _, type, text in doc.highlighter:each_token(line) do
    local font, w = get_font(default_font, type, indent_size), 0
    for char in common.utf8_chars(text) do
      if i >= col then
        if i > row_end_col then return line, row_end_col end
        if xoffset >= x then
          return line, (xoffset - x > (w / 2) and last_i or i)
        end
        w = get_width(font, char, xoffset)
        xoffset = xoffset + w
      end
      last_i = i
      i = i + #char
    end
  end
  return line, row_end_col
end


local open_files = setmetatable({ }, { __mode = "k" })

local old_doc_insert = Doc.raw_insert
function Doc:raw_insert(line, col, text, undo_stack, time)
  local old_lines = #self.lines
  old_doc_insert(self, line, col, text, undo_stack, time)
  if open_files[self] then
    for i,docview in ipairs(open_files[self]) do
      if docview.wrapped_settings then
        local lines = #self.lines - old_lines
        LineWrapping.update_breaks(docview, line, line, lines)
      end
    end
  end
end

local old_doc_remove = Doc.raw_remove
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
  local old_lines = #self.lines
  old_doc_remove(self, line1, col1, line2, col2, undo_stack, time)
  if open_files[self] then
    for i,docview in ipairs(open_files[self]) do
      if docview.wrapped_settings then
        local lines = #self.lines - old_lines
        LineWrapping.update_breaks(docview, line1, line2, lines)
      end
    end
  end
end

local old_doc_on_close = Doc.on_close
function Doc:on_close()
  old_doc_on_close(self)
  if open_files[self] then open_files[self] = nil end
end

local old_doc_update = DocView.update
function DocView:update()
  old_doc_update(self)
  if self.wrapped_settings and self.size.x > 0 then
    LineWrapping.update_docview_breaks(self)
  end
end

local old_get_h_scrollable_size = DocView.get_h_scrollable_size
function DocView:get_h_scrollable_size(...)
  if self.wrapping_enabled then return 0 end
  return old_get_h_scrollable_size(self, ...)
end

local old_new = DocView.new
function DocView:new(doc)
  old_new(self, doc)
  if not open_files[doc] then open_files[doc] = {} end
  table.insert(open_files[doc], self)
  if config.plugins.linewrapping.enable_by_default then
    self.wrapping_enabled = true
    LineWrapping.update_docview_breaks(self)
  else
    self.wrapping_enabled = false
  end
end

local old_scroll_to_line = DocView.scroll_to_line
function DocView:scroll_to_line(...)
  if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end
  old_scroll_to_line(self, ...)
end

local old_scroll_to_make_visible = DocView.scroll_to_make_visible
function DocView:scroll_to_make_visible(line, col, instant)
  if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end
  old_scroll_to_make_visible(self, line, col, instant)
  if self.wrapped_settings then self.scroll.to.x = 0 end
end

local old_get_visible_cols_range = DocView.get_visible_cols_range
function DocView:get_visible_cols_range(line, extra_cols)
  if not self.wrapped_settings then
    return old_get_visible_cols_range(self, line, extra_cols)
  end
  extra_cols = extra_cols or 100

  local first_line_idx = self.wrapped_line_to_idx[line]
  if not first_line_idx then return 0, 0, 0, 0 end

  local _, y, _, y2 = self:get_content_bounds()
  local lh = self:get_line_height()
  local first_visible_idx = math.max(1, math.floor(y / lh))
  local last_visible_idx = math.min(get_total_wrapped_lines(self), math.floor(y2 / lh) + 1)
  local next_line_idx = self.wrapped_line_to_idx[line + 1] or (get_total_wrapped_lines(self) + 1)
  local first_idx = math.max(first_visible_idx, first_line_idx)
  local last_idx = math.min(last_visible_idx, next_line_idx - 1)
  if first_idx > last_idx then return 0, 0, 0, 0 end

  local text = self.doc:get_utf8_line(line)
  local line_len = #text
  local _, col1 = get_idx_line_col(self, first_idx)
  local next_line, next_col = get_idx_line_col(self, last_idx + 1)
  local col2 = next_line == line and next_col - 1 or line_len

  col1 = math.max(1, col1 - extra_cols)
  col2 = math.min(line_len, col2 + extra_cols)

  local ucol1, ucol2 = col1, col2
  local cache = self.doc.cache.ulen
  local ulen = cache[line]
  if not ulen then
    ulen = text:ulen(nil, nil, true)
    cache[line] = ulen
  end
  if ulen < line_len then
    ucol1 = text:ulen(1, col1, true)
    ucol2 = text:ulen(1, col2, true)
    col1 = text:ucharpos(ucol1)
    col2 = text:ucharpos(ucol2)
  end

  return col1, col2, ucol1, ucol2
end

local old_get_x_offset_col = DocView.get_x_offset_col
function DocView:get_x_offset_col(line, x)
  if not self.wrapped_settings then return old_get_x_offset_col(self, line, x) end
  local idx = get_line_idx_col_count(self, line)
  return get_line_col_from_index_and_x(self, idx, x)
end

-- If line end is true, returns the end of the previous line, in a multi-line break.
local old_get_col_x_offset = DocView.get_col_x_offset
function DocView:get_col_x_offset(line, col, line_end)
  if not self.wrapped_settings then return old_get_col_x_offset(self, line, col) end
  local idx, ncol, count, scol = get_line_idx_col_count(self, line, col, line_end)
  local xoffset, i = (scol ~= 1 and self.wrapped_line_offsets[line] or 0), 1
  local default_font = self:get_font()
  local _, indent_size = self.doc:get_indent_info()
  default_font:set_tab_size(indent_size)
  for _, type, text in self.doc.highlighter:each_token(line) do
    if i + #text >= scol then
      if i < scol then
        text = text:sub(scol - i + 1)
        i = scol
      end
      local font = get_font(default_font, type, indent_size)
      for char in common.utf8_chars(text) do
        if i >= col then
          return xoffset
        end
        xoffset = xoffset + get_width(font, char, xoffset)
        i = i + #char
      end
    else
     i = i + #text
    end
  end
  return xoffset
end

local old_draw_line_text = DocView.draw_line_text
local function get_search_selections(doc, line)
  local selections = {}
  for _, line1, col1, line2, col2 in doc:get_selections(true) do
    if line == line1 and line <= line2
      and doc:is_search_selection(line1, col1, line2, col2)
    then
      selections[#selections + 1] = { start = col1, stop = col2 }
    end
  end
  return selections
end

local function is_search_selected(selections, col)
  for _, selection in ipairs(selections) do
    if col >= selection.start and col < selection.stop then
      return true
    end
  end
  return false
end

function DocView:draw_line_text(line, x, y)
  if not self.wrapped_settings then return old_draw_line_text(self, line, x, y) end
  local default_font = self:get_font()
  local tx, ty, begin_width = x, y + self:get_line_text_y_offset(), self.wrapped_line_offsets[line]
  local lh = self:get_line_height()
  local idx, _, count = get_line_idx_col_count(self, line)
  local total_offset = 1
  local _, indent_size = self.doc:get_indent_info()
  local search_selections = get_search_selections(self.doc, line)
  default_font:set_tab_size(indent_size)
  for _, type, text in self.doc.highlighter:each_token(line) do
    local color = style.syntax[type] or style.syntax["normal"]
    local font = get_font(default_font, type, indent_size)
    if text:sub(-1) == "\n" then text = text:sub(1, -2) end
    local token_offset = 1
    -- Split tokens if we're at the end of the document.
    while text ~= nil and token_offset <= #text do
      local next_line, next_line_start_col = get_idx_line_col(self, idx + 1)
      if next_line ~= line then
        next_line_start_col = #self.doc.lines[line] + 1
      end
      local max_length = next_line_start_col - total_offset
      local rendered_text = text:sub(token_offset, token_offset + max_length - 1)
      if #search_selections == 0 then
        tx = renderer.draw_text(font, rendered_text, tx, ty, color, { tab_offset = tx - x })
      else
        local i = 1
        local rendered_col = total_offset
        while i <= #rendered_text do
          local chunk_start = i
          local selected = is_search_selected(search_selections, rendered_col)
          while i <= #rendered_text
            and is_search_selected(search_selections, rendered_col) == selected
          do
            i = i + 1
            rendered_col = rendered_col + 1
          end
          local chunk = rendered_text:sub(chunk_start, i - 1)
          local chunk_color = selected and (style.search_selection_text or style.background) or color
          tx = renderer.draw_text(font, chunk, tx, ty, chunk_color, { tab_offset = tx - x })
        end
      end
      total_offset = total_offset + #rendered_text
      if total_offset ~= next_line_start_col or max_length == 0 then break end
      token_offset = token_offset + #rendered_text
      idx = idx + 1
      tx, ty = x + begin_width, ty + lh
    end
  end
  return lh * count
end

local old_draw_line_body = DocView.draw_line_body
function DocView:draw_line_body(line, x, y)
  if not self.wrapped_settings then return old_draw_line_body(self, line, x, y) end
  local lh = self:get_line_height()
  local idx0, _, count = get_line_idx_col_count(self, line)
  for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
    if line >= line1 and line <= line2 then
      local selection_color = style.selection
      if self.doc:is_search_selection(line1, col1, line2, col2) then
        selection_color = style.search_selection or style.caret
      end
      if line1 ~= line then col1 = 1 end
      if line2 ~= line then col2 = #self.doc.lines[line] + 1 end
      if col1 ~= col2 then
        local idx1 = get_line_idx_col_count(self, line, col1)
        local idx2 = get_line_idx_col_count(self, line, col2, true)
        for i = idx1, idx2 do
          local row_line, row_col = get_idx_line_col(self, i)
          if row_line ~= line then goto continue end

          local next_line, next_col = get_idx_line_col(self, i + 1)
          local row_end_col = next_line == line and next_col or #self.doc.lines[line] + 1
          local start_col = math.max(col1, row_col)
          local end_col = math.min(col2, row_end_col)
          if start_col < end_col then
            local x1 = x + self:get_col_x_offset(line, start_col)
            local x2 = x + self:get_col_x_offset(line, end_col, end_col == row_end_col)
            renderer.draw_rect(x1, y + (i - idx0) * lh, x2 - x1, lh, selection_color)
          elseif start_col == end_col and col1 == col2 then
            local x1 = x + self:get_col_x_offset(line, start_col)
            renderer.draw_rect(x1, y + (i - idx0) * lh, 0, lh, selection_color)
          end
          ::continue::
        end
      end
    end
  end
  local draw_highlight = nil
  for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do
    -- draw line highlight if caret is on this line
    if draw_highlight ~= false and config.highlight_current_line
    and line1 == line and core.active_view == self then
      draw_highlight = (line1 == line2 and col1 == col2)
    end
  end
  if draw_highlight then
    for i=1,count do
      self:draw_line_highlight(x + self.scroll.x, y + lh * (i - 1))
    end
  end
  -- draw line's text
  return self:draw_line_text(line, x, y)
end

local old_draw = DocView.draw
function DocView:draw()
  old_draw(self)
  if self.wrapped_settings then
    LineWrapping.draw_guide(self)
  end
end

local old_draw_line_gutter = DocView.draw_line_gutter
function DocView:draw_line_gutter(line, x, y, width)
  local lh = self:get_line_height()
  local _, _, count = get_line_idx_col_count(self, line)
  return (old_draw_line_gutter(self, line, x, y, width) or lh) * count
end

local old_translate_end_of_line = translate.end_of_line
function translate.end_of_line(doc, line, col)
  if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then
    return old_translate_end_of_line(doc, line, col)
  end
  local idx, ncol = get_line_idx_col_count(core.active_view, line, col)
  local nline, ncol2 = get_idx_line_col(core.active_view, idx + 1)
  if nline ~= line then return line, math.huge end
  return line, ncol2 - 1
end

local old_translate_start_of_line = translate.start_of_line
function translate.start_of_line(doc, line, col)
  if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then
    return old_translate_start_of_line(doc, line, col)
  end
  local idx, ncol = get_line_idx_col_count(core.active_view, line, col)
  local nline, ncol2 = get_idx_line_col(core.active_view, idx - 1)
  if nline ~= line then return line, 1 end
  return line, ncol2 + 1
end

---Enable or disable wrapping for a document view.
---@param docview core.docview
---@param enabled boolean
function LineWrapping.set_enabled(docview, enabled)
  if not docview or not docview.doc then return end
  docview.wrapping_enabled = enabled
  if enabled then
    LineWrapping.update_docview_breaks(docview)
  else
    LineWrapping.reconstruct_breaks(docview, docview:get_font(), math.huge)
  end
end

local function each_command_view(fn)
  local view = core.active_view
  if not view or not view.doc then return end
  local parent = view.diff_view_parent
  if parent then
    fn(parent.doc_view_a)
    fn(parent.doc_view_b)
  else
    fn(view)
  end
end

command.add(nil, {
  ["line-wrapping:enable"] = function()
    each_command_view(function(view) LineWrapping.set_enabled(view, true) end)
  end,
  ["line-wrapping:disable"] = function()
    each_command_view(function(view) LineWrapping.set_enabled(view, false) end)
  end,
  ["line-wrapping:toggle"] = function()
    local view = core.active_view
    if not view or not view.doc then return end
    local parent = view.diff_view_parent
    local enabled
    if parent then
      enabled = parent.doc_view_a.wrapping_enabled
        and parent.doc_view_b.wrapping_enabled
    else
      enabled = view.wrapping_enabled
    end
    each_command_view(function(item) LineWrapping.set_enabled(item, not enabled) end)
  end
})

keymap.add {
  ["f10"] = "line-wrapping:toggle",
}

return LineWrapping
