--
-- TextBox widget re-using code from pragtical's DocView.
--

local core = require "core"
local style = require "core.style"
local translate = require "core.doc.translate"
local Doc = require "core.doc"
local DocView = require "core.docview"
local Highlighter = require "core.doc.highlighter"
local View = require "core.view"
local Widget = require "widget"

local PASSWORD_CHAR = "•"

---@param text string
---@return string
local function password_visible_text(text)
  return (text:gsub("[\r\n]", ""))
end

---@param text string
---@return string
local function password_text(text)
  return password_visible_text(text):ugsub(".", PASSWORD_CHAR)
end

---@param text string
---@param col integer
---@return integer
local function password_chars_before_col(text, col)
  local count = 0
  local pos = 1
  for char in text:ugmatch(".") do
    if pos >= col then break end
    if char ~= "\n" and char ~= "\r" then
      count = count + 1
    end
    pos = pos + #char
  end
  return count
end

---Customized Highlighter to disable the coroutine.
---@class widget.textbox.SingleLineHighlighter
local SingleLineHighlighter = Highlighter:extend()

function SingleLineHighlighter:get_line(idx)
  return {text=self.doc.lines[1], tokens={"normal", self.doc.lines[1]}}
end

function SingleLineHighlighter:start() end

---@class widget.textbox.SingleLineDoc : core.doc
---@overload fun():widget.textbox.SingleLineDoc
---@field super core.doc
local SingleLineDoc = Doc:extend()

function SingleLineDoc:reset()
  SingleLineDoc.super.reset(self)
  self.highlighter = SingleLineHighlighter(self)
  self:reset_syntax()
end

function SingleLineDoc:insert(line, col, text)
  SingleLineDoc.super.insert(self, line, col, text:gsub("\n", ""))
end

---@class widget.textbox.TextView : core.docview
---@overload fun(parent:widget,subparent:widget):widget.textbox.TextView
---@field super core.docview
local TextView = DocView:extend()

function TextView:new(parent, subparent)
  TextView.super.new(self, SingleLineDoc())
  self.parent = parent
  self.subparent = subparent
  self.gutter_width = 0
  self.hide_lines_gutter = true
  self.gutter_text_brightness = 0
  self.scrollable = true
  self.font = "font"
  self.name = View.get_name(self)

  self.size.y = 0
  self.label = ""
end

function TextView:get_name()
  return self.name
end

function TextView:get_scrollable_size()
  return 0
end

function TextView:get_text()
  return self.doc:get_text(1, 1, 1, math.huge)
end

function TextView:set_text(text, select)
  self.doc:remove(1, 1, math.huge, math.huge)
  self.doc:text_input(text)
  if select then
    self.doc:set_selection(math.huge, math.huge, 1, 1)
  end
end

function TextView:get_gutter_width()
  return self.gutter_width or 0
end

function TextView:get_line_height()
  return math.floor(self:get_font():get_height() * 1.2)
end

function TextView:draw_line_gutter(idx, x, y)
  if self.hide_lines_gutter then
    return
  end
  TextView.super.draw_line_gutter(self, idx, x, y)
end

function TextView:draw_line_highlight()
  -- no-op function to disable this functionality
end

function TextView:draw_line_text(line, x, y)
  if self.subparent.password then
    local text = self.doc.lines[line] or ""
    local ty = y + self:get_line_text_y_offset()
    renderer.draw_text(
      self:get_font(),
      password_text(text),
      x,
      ty,
      style.syntax["normal"]
    )
    local placeholder = self.subparent.placeholder or ""
    if placeholder ~= "" and #text < 2 then
      renderer.draw_text(self:get_font(), placeholder, x, ty, style.dim)
    end
    return self:get_line_height()
  end

  TextView.super.draw_line_text(self, line, x, y)
  local placeholder = self.subparent.placeholder or ""
  if placeholder ~= "" and #self.doc.lines[line] < 2 then
    renderer.draw_text(self:get_font(), placeholder, x, y, style.dim)
  end
end

function TextView:get_col_x_offset(line, col)
  if self.subparent.password then
    local text = self.doc.lines[line] or ""
    return self:get_font():get_width(PASSWORD_CHAR)
      * password_chars_before_col(text, col)
  end
  return TextView.super.get_col_x_offset(self, line, col)
end

function TextView:get_x_offset_col(line, x)
  if self.subparent.password then
    local text = password_visible_text(self.doc.lines[line] or "")
    local char_width = self:get_font():get_width(PASSWORD_CHAR)
    if char_width <= 0 then return 1 end
    local offset = 0
    local col = 1
    for char in text:ugmatch(".") do
      if x <= offset + (char_width / 2) then
        return col
      end
      offset = offset + char_width
      col = col + #char
    end
    return #text
  end
  return TextView.super.get_x_offset_col(self, line, x)
end

-- Overwrite this function just to disable the core.push_clip_rect
function TextView:draw()
  self:draw_background(style.background)
  local _, indent_size = self.doc:get_indent_info()
  self:get_font():set_tab_size(indent_size)

  local minline, maxline = self:get_visible_line_range()
  local lh = self:get_line_height()

  local x, y = self:get_line_screen_position(minline)
  for i = minline, maxline do
    self:draw_line_gutter(i, self.position.x, y)
    y = y + lh
  end

  x, y = self:get_line_screen_position(minline)
  for i = minline, maxline do
    self:draw_line_body(i, x, y)
    y = y + lh
  end
  self:draw_overlay()

  self:draw_scrollbar()
end

---@class widget.textbox : widget
---@overload fun(parent?:widget, text?:string, placeholder?:string, options?:table):widget.textbox
---@field textview widget.textbox.TextView
---@field placeholder string
---@field password boolean
local TextBox = Widget:extend()

function TextBox:new(parent, text, placeholder, options)
  TextBox.super.new(self, parent)
  self.type_name = "widget.textbox"
  self.textview = TextView(parent, self) -- allow finding containing parent
  self.textview.name = self:get_name()
  self.size.x = 200 + (style.padding.x * 2)
  self.textview.size.x = self.size.x
  self.size.y = self:get_font():get_height() + (style.padding.y * 2)
  self.placeholder = placeholder or ""
  -- this widget is for text input
  self.input_text = true
  self.cursor = "ibeam"
  self.active = false
  self.drag_select = false
  self.password = options and options.password or false

  if text ~= "" then
    self.textview:set_text(text)
  end

  local this = self

  function self.textview.doc:on_text_change()
    this:on_change(this:get_text())
  end

  -- more granular listening of text changing events
  local doc_raw_insert = self.textview.doc.raw_insert
  function self.textview.doc:raw_insert(...)
    doc_raw_insert(self, ...)
    this:on_text_change("insert", ...)
  end

  local doc_raw_remove = self.textview.doc.raw_remove
  function self.textview.doc:raw_remove(...)
    doc_raw_remove(self, ...)
    this:on_text_change("remove", ...)
  end
end

---@param width integer
function TextBox:set_size(width)
  TextBox.super.set_size(
    self,
    width,
    self:get_font():get_height() + (style.padding.y * 2)
  )
  self.textview.size.x = self.size.x
end

--- Get the text displayed on the textbox.
---@return string
function TextBox:get_text()
  return self.textview:get_text()
end

--- Set the text displayed on the textbox.
---@param text string
---@param select? boolean
function TextBox:set_text(text, select)
  self.textview:set_text(text, select)
end

---Enable or disable password display mode.
---@param enabled boolean
function TextBox:set_password_mode(enabled)
  self.password = enabled
end

---Get whether password display mode is enabled.
---@return boolean
function TextBox:get_password_mode()
  return self.password
end

--
-- Events
--

function TextBox:on_mouse_pressed(button, x, y, clicks)
  if TextBox.super.on_mouse_pressed(self, button, x, y, clicks) then
    self.textview:on_mouse_pressed(button, x, y, clicks)
    local line, col = self.textview:resolve_screen_position(x, y)
    self.drag_select = { line = line, col = col }
    self.textview.doc:set_selection(line, col, line, col)
    if clicks == 2 then
      local line1, col1 = translate.start_of_word(self.textview.doc, line, col)
      local line2, col2 = translate.end_of_word(self.textview.doc, line1, col1)
      self.textview.doc:set_selection(line2, col2, line1, col1)
    elseif clicks == 3 then
      self.textview.doc:set_selection(1, 1, 1, math.huge)
    end
    if core.active_view ~= self.textview then
      self.textview:on_mouse_released(button, x, y)
    end
    return true
  end
  return false
end

function TextBox:on_mouse_released(button, x, y)
  if TextBox.super.on_mouse_released(self, button, x, y) then
    self.drag_select = false
    self.textview:on_mouse_released(button, x, y)
    return true
  end
  return false
end

function TextBox:on_mouse_moved(x, y, dx, dy)
  if self.drag_select then
    local line, col = self.textview:resolve_screen_position(x, y)
    self.textview.doc:set_selection(
      self.drag_select.line, self.drag_select.col, line, col
    )
  end
  if TextBox.super.on_mouse_moved(self, x, y, dx, dy) then
    if self.active or core.active_view == self.textview then
      self.textview:on_mouse_moved(x, y, dx, dy)
    end
    return true
  end
  return false
end

function TextBox:activate()
  self.hover_border = style.caret
  self.active = true
  core.request_cursor("ibeam")
end

function TextBox:deactivate()
  self.hover_border = nil
  self.drag_select = false
  self.active = false
  core.request_cursor("arrow")
end

function TextBox:on_text_input(text)
  TextBox.super.on_text_input(self, text)
  self.textview:on_text_input(text)
end

---Event fired on any text change event.
---@param action string Can be "insert" or "remove",
---insert arguments (see Doc:raw_insert):
---  line, col, text, undo_stack, time
---remove arguments (see Doc:raw_remove):
---  line1, col1, line2, col2, undo_stack, time
---@diagnostic disable-next-line
function TextBox:on_text_change(action, ...) end

function TextBox:update()
  if not TextBox.super.update(self) then return false end

  self.border.color = self.hover_border or style.text

  if
    self.drag_select
    or
    (self.active and self:mouse_on_top(self.mouse.x, self.mouse.y))
  then
    core.request_cursor("ibeam")
  end

  self.textview.position.x = self.position.x + (style.padding.x / 2)
  self.textview.position.y = self.position.y - (style.padding.y/2.5)
  self.textview.size.x = self.size.x
  self.textview.size.y = self.size.y - (style.padding.y * 2)

  self.textview:update()

  self:set_size(nil, self:get_font():get_height() + (style.padding.y * 2))

  return true
end

function TextBox:draw()
  if not TextBox.super.draw(self) then return false end

  core.push_clip_rect(
    self.position.x,
    self.position.y,
    self.size.x,
    self.size.y
  )
  self.textview:draw()
  core.pop_clip_rect()

  return true
end


return TextBox
