--
-- Base widget implementation for pragtical.
-- @copyright Jefferson Gonzalez
-- @license MIT
--

local core = require "core"
local command = require "core.command"
local config = require "core.config"
local style = require "core.style"
local keymap = require "core.keymap"
local View = require "core.view"
local ScrollBar = require "widget.scrollbar"
local RootView

---Represents the border of a widget.
---@class widget.border
---@field public width number
---@field public color renderer.color | nil

---Represents the position of a widget.
---@class widget.position
---@field public x number Real X
---@field public y number Real y
---@field public rx number Relative X
---@field public ry number Relative Y
---@field public dx number Dragging initial x position
---@field public dy number Dragging initial y position

---Represents a padding attribute.
---@class widget.padding
---@field public x number
---@field public y number

---@class widget.animation.options
---Prevents duplicated animations from getting added.
---@field name? string
---Speed of the animation, defaults to 0.5
---@field rate? number
---Called each time the value of a property changes.
---@field on_step? fun(target:table, property:string, value:number, animation:widget.animation)
---Called when the animation finishes.
---@field on_complete? fun(widget:widget)

---@class widget.animation
---@field target table
---@field properties table<string,number>
---@field options? widget.animation.options

---Represents a reference to a color stored elsewhere.
---@class widget.colorreference
---@field public type "color"
---@field public container table<string, renderer.color>
---@field public name string

---Represents a reference to a font stored elsewhere.
---@class widget.fontreference
---@field public type "font"
---@field public container table<string, renderer.font>
---@field public name string

---@alias widget.font widget.fontreference | renderer.font | string

---@alias widget.clicktype
---| "left"
---| "right"

---@alias widget.styledtext table<integer, renderer.font|widget.fontreference|renderer.color|widget.colorreference|integer|string>

---A base widget
---@class widget : core.view
---@overload fun(parent?:widget, floating?:boolean):widget
---@field public super widget
---@field public parent widget | nil
---@field public name string
---@field public position widget.position
---Modifying this property directly is not advised, use set_size() instead.
---@field public size widget.position
---@field public childs table<integer,widget>
---@field public child_active widget | nil
---@field public zindex integer
---@field public border widget.border
---@field public clickable boolean
---@field public draggable boolean
---@field public scrollable boolean
---@field public font widget.font
---@field public foreground_color renderer.color
---@field public background_color renderer.color
---@field public render_background boolean
---@field public type_name string
---@field protected visible boolean
---@field protected has_focus boolean
---@field protected dragged boolean
---@field protected tooltip? string | widget.styledtext
---@field protected tooltip_command? string
---@field protected label string | widget.styledtext
---@field protected input_text boolean
---@field protected textview widget
---@field protected next_zindex integer
---@field protected mouse widget.position
---@field protected prev_size widget.position
---@field protected mouse_is_pressed boolean
---@field protected mouse_is_hovering boolean
---@field protected mouse_pressed_outside boolean
---@field protected is_scrolling boolean
---By default is set to true to allow ctrl+wheel or cmd+wheel on mac to scale
---the interface, you can set it to false on your parent widget to allow
---manually intercepting ctrl+wheel.
---@field protected skip_scroll_ctrl boolean
---@field protected captured_widget widget Widget that captured mouse events
---@field protected animations widget.animation[]
local Widget = View:extend()

---Indicates on a widget.styledtext that a new line follows.
---@type integer
Widget.NEWLINE = 1

---Keep track of last hovered widget to properly trigger on_mouse_leave
---@type widget | nil
local last_hovered_child = nil

---A list of floating widgets that need to receive events.
---@type table<integer, widget>
local floating_widgets = {}

---Flag that indicates if the tooltip is been shown
---@type boolean
local widget_showing_tooltip = false

---When no parent is given to the widget constructor it will automatically
---overwrite RootView methods to intercept system events.
---@param parent? widget
---@param floating? boolean
function Widget:new(parent, floating)
  Widget.super.new(self)

  self.updated = false -- flag to ensure we updated before drawing.

  self.v_scrollbar = ScrollBar(self, {direction = "v", alignment = "e"})
  self.h_scrollbar = ScrollBar(self, {direction = "h", alignment = "e"})

  self.type_name = "widget"
  self.parent = parent
  self.name = "---" -- defaults to the application name
  if type(floating) == "boolean" then
    self.defer_draw =  floating
  else
    self.defer_draw = true
  end
  self.childs = {}
  self.child_active = nil
  self.zindex = nil
  self.next_zindex = 1
  self.border = {
    width = 1,
    color = nil
  }
  self.foreground_color = nil
  self.background_color = nil
  self.render_background = true
  self.visible = parent and true or false
  self.has_focus = false
  self.clickable = true
  self.draggable = false
  self.dragged = false
  self.font = "font"
  self.force_events = {}
  self.label = ""
  self.input_text = false
  self.textview = nil
  self.mouse = {x = 0, y = 0}
  self.prev_size = {x = 0, y = 0}
  self.is_scrolling = false
  self.skip_scroll_ctrl = true

  self.mouse_is_pressed = false
  self.mouse_is_hovering = false

  -- used to allow proper node resizing
  self.mouse_pressed_outside = false

  self.animations = {}

  if parent then
    parent:add_child(self)
  elseif self.defer_draw then
    table.insert(floating_widgets, self)
    Widget.override_rootview()
  end

  self:set_position(0, 0)
end

---Useful for debugging.
function Widget:__tostring()
  return self.type_name
end

---Add a child widget, automatically assign a zindex if non set and sorts
---them in reverse order for better events matching.
---@param child widget
function Widget:add_child(child)
  if not child.zindex then
    child.zindex = self.next_zindex
    self.next_zindex = self.next_zindex + 1
  end

  table.insert(self.childs, child)
  table.sort(self.childs, function(t1, t2) return t1.zindex > t2.zindex end)
end

---Remove a child widget.
---@param child widget
function Widget:remove_child(child)
  for position, element in ipairs(self.childs) do
    if child == element then
      child:destroy_childs()
      table.remove(self.childs, position)
      break
    end
  end
end

---Show the widget.
function Widget:show()
  if not self.parent then
    if self.size.x <= 0 or self.size.y <= 0 then
      self.size.x = self.prev_size.x
      self.size.y = self.prev_size.y
    end
    self.prev_size.x = 0
    self.prev_size.y = 0
  end
  self.visible = true
  -- re-triggers update to make sure everything was properly calculated
  -- and redraw the interface once, maybe something else can be changed
  -- to not require this action, but for now lets do this.
  self.perform_update_size_position = true
end

---Perform an animated show.
---@param lock_x? boolean Do not resize width while animating
---@param lock_y? boolean Do not resize height while animating
---@param options? widget.animation.options
function Widget:show_animated(lock_x, lock_y, options)
  if not self.parent then
    if self.prev_size.x > 0 or self.prev_size.y > 0 then
      self.size.x = self.prev_size.x
      self.size.y = self.prev_size.y
    end
    self.prev_size.x = 0
    self.prev_size.y = 0
  end

  local target_x, target_y = self.size.x, self.size.y
  self.size.x = lock_x and target_x or 0
  self.size.y = lock_y and target_y or 0
  local properties = {}
  if not lock_x then properties["x"] = target_x end
  if not lock_y then properties["y"] = target_y end
  options = options or {}
  self:animate(self.size, properties, {
    name = options.name or "show_animated",
    rate = options.rate,
    on_step = options.on_step,
    on_complete = options.on_complete
  })

  self.visible = true
end

---Hide the widget.
function Widget:hide()
  self.visible = false
  -- we need to force size to zero on parent widget to properly hide it
  -- when used as a pragtical node, otherwise the reserved space of the node
  -- will stay visible and dragging will reveal empty space.
  if not self.parent then
    if self.size.x > 0 or self.size.y > 0 then
      -- we only do this once to prevent issues of consecutive hide calls
      if self.prev_size.x == 0 and self.prev_size.y == 0 then
        self.prev_size.x = self.size.x
        self.prev_size.y = self.size.y
      end
      self.size.x = 0
      self.size.y = 0
    end
  end
end

---Perform an animated hide.
---@param lock_x? boolean Do not resize width while animating
---@param lock_y? boolean Do not resize height while animating
---@param options? widget.animation.options
function Widget:hide_animated(lock_x, lock_y, options)
  local x, y = self.size.x, self.size.y
  local properties = {}
  if not lock_x then properties["x"] = 0 end
  if not lock_y then properties["y"] = 0 end
  options = options or {}
  self:animate(self.size, properties, {
    name = options.name or "hide_animated",
    rate = options.rate,
    on_step = options.on_step,
    on_complete = function()
      self.size.x, self.size.y = x, y
      self:hide()
      if options.on_complete then
        options.on_complete(self)
      end
    end
  })
end

---When set to false the background rendering is disabled.
---@param enable? boolean | nil
function Widget:toggle_background(enable)
  if type(enable) == "boolean" then
    self.render_background = enable
  else
    self.render_background = not self.render_background
  end
end

---Toggle visibility of widget.
---@param animated? boolean
---@param lock_x? boolean
---@param lock_y? boolean
---@param options? widget.animation.options
function Widget:toggle_visible(animated, lock_x, lock_y, options)
  if not self.visible then
    if not animated then
      self:show()
    else
      self:show_animated(lock_x, lock_y, options)
    end
  else
    if not animated then
      self:hide()
    else
      self:hide_animated(lock_x, lock_y, options)
    end
  end
end

---Check if the widget is visible also recursing to the parent visibility.
---@return boolean
function Widget:is_visible()
  if
    not self.explicit_update
    and
    (not self.visible or (self.parent and not self.parent:is_visible()))
  then
    return false
  end
  return true
end

---Taken from the logview and modified it a tiny bit.
---TODO: something similar should be on pragtical core.
---@param font widget.font
---@param text string
---@param x integer
---@param y integer
---@param color renderer.color
---@param only_calc boolean
---@return integer resx
---@return integer resy
---@return integer width
---@return integer height
function Widget:draw_text_multiline(font, text, x, y, color, only_calc)
  font = self:get_font(font)
  local th = font:get_height()
  local resx, resy = x, y
  local width, height = 0, 0
  for line in (text .. "\n"):gmatch("(.-)\n") do
    resy = y
    if only_calc then
      resx = x + font:get_width(line)
    else
      resx = renderer.draw_text(font, line, x, y, color)
    end
    y = y + th
    width = math.max(width, resx - x)
    height = height + th
  end
  return resx, resy, width, height
end

---Render or calculate the size of the specified range of elements
---in a styled text elemet.
---@param text widget.styledtext
---@param x integer
---@param y integer
---@param only_calc? boolean
---@param start_idx? integer
---@param end_idx? integer
---@return integer width
---@return integer height
function Widget:draw_styled_text(text, x, y, only_calc, start_idx, end_idx)
  local font = self:get_font()
  local color = self.foreground_color or style.text
  local width = 0
  local height = font:get_height()
  local new_line = false
  local nx = x

  start_idx = start_idx or 1
  end_idx = end_idx or #text

  for pos=start_idx, end_idx, 1 do
    local element = text[pos]
    local ele_type = type(element)
    if
      ele_type == "userdata"
      or
      (
        ele_type == "table" and (not element.type or element.type == "font")
        and
        (element.container or type(element[1]) == "userdata")
      )
    then
      if ele_type == "table" and element.container then
        font = element.container[element.name]
      else
        font = element
      end
    elseif ele_type == "table" and (not element.type or element.type == "color") then
      if element.container then
        color = element.container[element.name]
      else
        color = element
      end
    elseif element == Widget.NEWLINE then
      y = y + font:get_height()
      nx = x
      new_line = true
    elseif ele_type == "string" then
      local rx, ry, w, h = self:draw_text_multiline(
        font, element, nx, y, color, only_calc
      )
      y = ry
      nx = rx
      if new_line then
        height = height + h
        width = math.max(width, w)
        new_line = false
      else
        height = math.max(height, h)
        width = width + w
      end
    end
  end

  return width, height
end

---Draw the widget configured border or custom one.
---@param x? number
---@param y? number
---@param w? number
---@param h? number
function Widget:draw_border(x, y, w, h)
  if self.border.width <= 0 then return end

  x = x or self.position.x
  y = y or self.position.y
  w = w or self.size.x
  h = h or self.size.y

  x = x - self.border.width
  y = y - self.border.width
  w = w + (self.border.width * 2)
  h = h + (self.border.width * 2)

  -- Draw lines instead of full rectangle to be able to draw on top

  --top
  renderer.draw_rect(
    x, y, w + x % 1 - self.border.width, self.border.width,
    self.border.color or style.text
  )
  --bottom
  renderer.draw_rect(
    x, y+h - self.border.width, w + x % 1 - self.border.width, self.border.width,
    self.border.color or style.text
  )
  --right
  renderer.draw_rect(
    x+w - self.border.width, y, self.border.width, h,
    self.border.color or style.text
  )
  --left
  renderer.draw_rect(
    x, y, self.border.width, h,
    self.border.color or style.text
  )
end

---Called by pragtical node system to properly resize the widget.
---@param axis string | "'x'" | "'y'"
---@param value number
function Widget:set_target_size(axis, value)
  if not self.visible then
    return false
  end
  if axis == "x" then
    self:set_size(value)
  else
    self:set_size(nil, value)
  end
  return true
end

---@param width? integer
---@param height? integer
function Widget:set_size(width, height)
  -- take into consideration the border as part of size
  if width then
    if width > (self.border.width * 2) then
      width = width - (self.border.width * 2)
    else
      width = 0
    end
  end
  if height then
    if height > (self.border.width * 2) then
      height = height - (self.border.width * 2)
    else
      height = 0
    end
  end

  if not self.parent and not self.visible then
    if width then self.prev_size.x = width end
    if height then self.prev_size.y = height end
  else
    if width then self.size.x = width end
    if height then self.size.y = height end
  end

  if
    not self.prev_width or not self.prev_height
  then
    self.prev_width, self.prev_height = width, height
    self.perform_update_size_position = true
  end
end

---@return widget.position
function Widget:get_size()
  if not self.parent and not self.visible then
    return self.prev_size
  end
  return self.size
end

---Set the widget border size and appropriately re-set the widget size.
---@param width integer
function Widget:set_border_width(width)
  local wwidth, wheight = 0, 0;
  if self.border.width > 0 then
    local prev_width = self.border.width * 2
    local size = self:get_size()
    wwidth = size.x + prev_width
    wheight = size.y + prev_width
  end
  self.border.width = width
  self:set_size(wwidth, wheight)
end

---Called on the update function to be able to scroll the child widgets.
function Widget:update_position()
  if self.parent then
    self.position.x = self.position.rx + self.border.width
    self.position.y = self.position.ry + self.border.width

    -- add offset to properly scroll
    local ox, oy = self.parent:get_content_offset()
    self.position.x = ox + self.position.x
    self.position.y = oy + self.position.y
  end

  for _, child in pairs(self.childs) do
    child:update_position()
  end
end

---Set the position of the widget and updates the child absolute coordinates
---@param x? integer
---@param y? integer
function Widget:set_position(x, y)
  if x then self.position.x = x + self.border.width end
  if y then self.position.y = y + self.border.width end

  if self.parent then
    -- add offset to properly scroll
    local ox, oy = self.parent:get_content_offset()

    if x then
      self.position.rx = x
      self.position.x = ox + self.position.x
    end

    if y then
      self.position.ry = y
      self.position.y = oy + self.position.y
    end
  end

  if x or y then
    for _, child in pairs(self.childs) do
      child:set_position(child.position.rx, child.position.ry)
    end
  end
end

---Get the real renderer.font associated with a widget.font.
---@param font? widget.font
---@return renderer.font
function Widget:get_font(font)
  if not font then font = self.font end
  local font_type = type(font)
  if font_type == "userdata" then
    return font
  elseif font_type == "string" then
    return style[font]
  elseif font and font.container then
    return font.container[font.name]
  end
  if not font then
    return style.font
  end
  return font
end

---Get the relative position in relation to parent
---@return widget.position
function Widget:get_position()
  self:update_if_scaled()
  local position = { x = self.position.x, y = self.position.y }
  if self.parent then
    position.x = self.position.rx
    position.y = self.position.ry
  end
  return position
end

---Get width including borders.
---@return number
function Widget:get_width()
  local size = self:get_size()
  return size.x + (self.border.width * 2)
end

---Get height including borders.
---@return number
function Widget:get_height()
  local size = self:get_size()
  return size.y + (self.border.width * 2)
end

---Get the right x coordinate relative to parent
---@return number
function Widget:get_right()
  return self:get_position().x + self:get_width()
end

---Get the bottom y coordinate relative to parent
---@return number
function Widget:get_bottom()
  return self:get_position().y + self:get_height()
end

---Overall height of the widget accounting all visible child widgets.
---@return number
function Widget:get_real_height()
  local size = 0
  for _, child in pairs(self.childs) do
    if child.visible then
      size = math.max(size, child:get_bottom())
    end
  end
  return size
end

---Overall width of the widget accounting all visible child widgets.
---@return number
function Widget:get_real_width()
  local size = 0
  for _, child in pairs(self.childs) do
    if child.visible then
      size = math.max(size, child:get_right())
    end
  end
  return size
end

---Check if the given mouse coordinate is hovering the widget
---@param x number
---@param y number
---@return boolean
function Widget:mouse_on_top(x, y)
  return
    self.visible
    and
    x >= self.position.x - self.border.width
    and
    x <= self.position.x - self.border.width + self:get_width()
    and
    y >= self.position.y - self.border.width
    and
    y <= self.position.y - self.border.width + self:get_height()
end

---Mark a widget as having focus.
---TODO: Implement set focus system by pressing a key like tab?
function Widget:set_focus(has_focus)
  self.set_focus = has_focus
end

---Text displayed when the widget is hovered.
---If a command name is also given its associated binding will be displayed
---as part of the tooltip.
---@param tooltip? string | widget.styledtext
---@param command? string
function Widget:set_tooltip(tooltip, command)
  self.tooltip = tooltip
  self.tooltip_command = command
end

---A text label for the widget, not all widgets support this.
---@param text string | widget.styledtext
function Widget:set_label(text)
  self.label = text
end

---Used internally when dragging is activated.
---@param x number
---@param y number
function Widget:drag(x, y)
  if self.position.dx and self.position.dy then
    self:set_position(x - self.position.dx, y - self.position.dy)
  end
end

---Center the widget horizontally and vertically to the screen or parent widget.
function Widget:centered()
  local w, h = system.get_window_size(core.window);
  if self.parent then
    w = self.parent:get_width()
    h = self.parent:get_height()
  end
  self:set_position(
    (w / 2) - (self:get_width() / 2),
    (h / 2) - (self:get_height() / 2)
  )
end

---Replaces current active child with a new one and calls the
---activate/deactivate events of the child. This is especially
---used to send text input events to widgets with input_text support.
---@param child? widget If nil deactivates current child
function Widget:swap_active_child(child)
  if self.parent then
    self.parent:swap_active_child(child)
    return
  end

  if child and child == self.child_active then return end

  local active_child = self.child_active

  if self.child_active then
    self.child_active:deactivate()
  end

  self.child_active = child

  if child then
    if not self.prev_view then
      self.prev_view = core.active_view
    end
    core.set_active_view(child.input_text and child.textview or child)
    self.child_active:activate()
  elseif self.prev_view then
    -- return focus to previous view
    if self.prev_view ~= active_child then
      core.set_active_view(self.prev_view)
    else
      core.set_active_view(self)
    end
    self.prev_view = nil
  end
end

---Calculates the y scrollable size taking into account the bottom most
---widget or the size of the widget it self if greater.
---@return number
function Widget:get_scrollable_size()
  return math.max(self:get_size().y, self:get_real_height())
end

---Calculates the x scrollable size taking into account the right most
---widget or the size of the widget it self if greater.
---@return number
function Widget:get_h_scrollable_size()
  return math.max(self:get_size().x, self:get_real_width())
end

---The name that is displayed on pragtical tabs.
function Widget:get_name()
  return self.parent and self.parent:get_name() or self.name
end

--
-- Events
--

---Send file drop event to hovered child.
---@param filename string
---@param x number
---@param y number
---@return boolean processed
function Widget:on_file_dropped(filename, x, y)
  if not self.visible then return false end

  for _, child in pairs(self.childs) do
    if child:mouse_on_top(x, y) then
      return child:on_file_dropped(filename, x, y)
    end
  end

  return false
end

---Redirects any text input to active child with the input_text flag.
---@param text string
---@return boolean processed
function Widget:on_text_input(text)
  if not self.visible then return false end

  Widget.super.on_text_input(self, text)

  if self.child_active then
    self.child_active:on_text_input(text)
    return true
  end

  return false
end

---All mouse events will be directly sent to the widget even if mouse moves
---outside the widget region.
---@param scrolling? boolean Capture for scrolling
function Widget:capture_mouse(scrolling)
  local parent = self.parent
  while parent do
    -- propagate to parents so if mouse is not on top still
    -- reach the childrens when the mouse is released
    if scrolling then parent.is_scrolling = true end
    parent.captured_widget = self
    parent = parent.parent
  end
  if scrolling then self.is_scrolling = true end
end

---Undo capture_mouse()
function Widget:release_mouse()
  local parent = self.parent
  while parent do
    -- propagate to parents so if mouse is not on top still
    -- reach the childrens when the mouse is released
    parent.is_scrolling = false
    parent.captured_widget = nil
    parent = parent.parent
  end
  self.is_scrolling = false
end

---Send mouse pressed events to hovered child or starts dragging if enabled.
---@param button widget.clicktype
---@param x number
---@param y number
---@param clicks integer
---@return boolean processed
function Widget:on_mouse_pressed(button, x, y, clicks)
  if not self.visible then return false end

  -- Capture when scrollbar is pressed
  if Widget.super.on_mouse_pressed(self, button, x, y, clicks) then
    self:capture_mouse(true)
    return true
  end

  for _, child in pairs(self.childs) do
    if child:mouse_on_top(x, y) and child.clickable then
      child:on_mouse_pressed(button, x, y, clicks)
      return true
    end
  end

  if self:mouse_on_top(x, y) then
    self.mouse_is_pressed = true

    if self.parent then
      -- propagate to parents so if mouse is not on top still
      -- reach the childrens when the mouse is released
      self.parent.mouse_is_pressed = true
    end

    if self.draggable and not self.child_active then
      self.position.dx = x - self.position.x
      self.position.dy = y - self.position.y
      system.set_cursor("hand")
    end
  else
    self:swap_active_child()
    return false
  end

  return true
end

---Send mouse released events to hovered child, ends dragging if enabled and
---emits on click events if applicable.
---@param button widget.clicktype
---@param x number
---@param y number
---@return boolean processed
function Widget:on_mouse_released(button, x, y)
  if not self.visible then return false end

  if widget_showing_tooltip then
    widget_showing_tooltip = false
    core.status_view:remove_tooltip()
  end

  if self.captured_widget then
    self.captured_widget:on_mouse_released(button, x, y)
    if self.is_scrolling then
      self.captured_widget:release_mouse()
    end
    return true
  end

  Widget.super.on_mouse_released(self, button, x, y)

  self:swap_active_child()

  if self.child_active then
    self.child_active:on_mouse_released(button, x, y)
  end

  if not self.dragged then
    for _, child in pairs(self.childs) do
      local mouse_on_top = child:mouse_on_top(x, y)
      if
        mouse_on_top
        or
        child.mouse_is_pressed
      then
        child:on_mouse_released(button, x, y)
        if child.input_text then
          self:swap_active_child(child)
        end
        if mouse_on_top and child.mouse_is_pressed then
          child:on_click(button, x, y)
        end
        return true
      end
    end
  end

  if
    not self.dragged
    and
    not self.mouse_is_pressed
  then
    return false
  end

  if self.mouse_is_pressed then
    if self:mouse_on_top(x, y) then
      self:on_click(button, x, y)
    end
    self.mouse_is_pressed = false
    if self.parent then
      self.parent.mouse_is_pressed = false
    end
    if self.draggable then
      system.set_cursor("arrow")
    end
  end

  self.dragged = false

  return true
end

---Event emitted when the value of the widget changes.
---If applicable, child widgets should call this method
---when its value changes.
---@param value any
function Widget:on_change(value) end

---Click event emitted on a succesful on_mouse_pressed
---and on_mouse_released events combo.
---@param button widget.clicktype
---@param x number
---@param y number
function Widget:on_click(button, x, y) end

---Emitted to input_text widgets when clicked.
function Widget:activate() end

---Emitted to input_text widgets on lost focus.
function Widget:deactivate() end

---Besides the on_mouse_moved this event emits on_mouse_enter
---and on_mouse_leave for easy hover effects. Also, if the
---widget is scrollable and pressed this will drag it unless
---there is an active input_text child active.
---@param x number
---@param y number
---@param dx number
---@param dy number
function Widget:on_mouse_moved(x, y, dx, dy)
  if not self.visible then return false end

  if self.captured_widget then
    self.captured_widget:on_mouse_moved(x, y, dx, dy)
    return true
  end

  Widget.super.on_mouse_moved(self, x, y, dx, dy)

  -- store latest mouse coordinates for usage on the on_mouse_wheel event.
  self.mouse.x = x
  self.mouse.y = y

  if self.child_active then
    self.child_active:on_mouse_moved(x, y, dx, dy)
  end

  if not self.dragged then
    local hovered = nil
    for _, child in pairs(self.childs) do
      if
        not hovered
        and
        (child:mouse_on_top(x, y) or child.mouse_is_pressed)
      then
        hovered = child
      elseif child.mouse_is_hovering then
        if widget_showing_tooltip then
          widget_showing_tooltip = false
          core.status_view:remove_tooltip()
        end
        child.mouse_is_hovering = false
        child:on_mouse_leave(x, y, dx, dy)
        system.set_cursor("arrow")
      end
    end

    if hovered then
      hovered:on_mouse_moved(x, y, dx, dy)
      if last_hovered_child and not last_hovered_child:mouse_on_top(x, y) then
        last_hovered_child:on_mouse_leave(x, y, dx, dy)
        last_hovered_child.mouse_is_hovering = false
        last_hovered_child = nil
      end
      return true;
    end
  end

  if
    not self:mouse_on_top(x, y)
    and
    not self.mouse_is_pressed
    and
    not self.mouse_is_hovering
  then
    return false
  end

  local is_over = true

  if self:mouse_on_top(x, y) then
    if not self.mouse_is_hovering  then
      system.set_cursor("arrow")
      self.mouse_is_hovering = true
      if self.tooltip or self.tooltip_command then
        local binding = self.tooltip_command and keymap.get_binding(
          self.tooltip_command
        )
        ---@type table
        local tooltip = {}
        if self.tooltip and #self.tooltip > 0 then
          tooltip = type(self.tooltip) == "table"
            and {table.unpack(self.tooltip)}
            or {self.tooltip}
        end
        if binding then
          if #tooltip == 0 then
            table.insert(tooltip, command.prettify_name(self.tooltip_command))
          end
          table.insert(tooltip, style.dim)
          table.insert(tooltip, "  " .. binding)
        end
        if(#tooltip > 0) then
          widget_showing_tooltip = true
          core.status_view:show_tooltip(tooltip)
        end
      end
      self:on_mouse_enter(x, y, dx, dy)
      last_hovered_child = self
    end
  else
    self:on_mouse_leave(x, y, dx, dy)
    if widget_showing_tooltip then
      widget_showing_tooltip = false
      core.status_view:remove_tooltip()
    end
    self.mouse_is_hovering = false
    is_over = false
  end

  if not self.child_active and self.mouse_is_pressed and self.draggable then
    system.set_cursor("hand")
    self:drag(x, y)
    self.dragged = true
    return true
  end

  return is_over
end

---Emitted once when the mouse hovers the widget.
function Widget:on_mouse_enter(x, y, dx, dy)
  for _, child in pairs(self.childs) do
    if child:mouse_on_top(x, y) then
      child:on_mouse_enter(x, y, dx, dy)
      break
    end
  end
end

---Emitted once when the mouse leaves the widget.
function Widget:on_mouse_leave(x, y, dx, dy)
  for _, child in pairs(self.childs) do
    if child.mouse_is_hovering then
      child:on_mouse_leave(x, y, dx, dy)
    end
  end
end

function Widget:on_mouse_left()
  if not self.captured_widget then
    Widget.super.on_mouse_left(self)
    self:on_mouse_moved(-1, -1, -1, -1)
  end
end

function Widget:on_mouse_wheel(y, x)
  if
    not self.visible
    or
    not self:mouse_on_top(self.mouse.x, self.mouse.y)
  then
    return false
  else
    local ctrl_pressed = false
    if self.skip_scroll_ctrl and not self.parent then
      local ctrl_key = PLATFORM == "Mac OS X" and "cmd" or "ctrl"
      ctrl_pressed = keymap.modkeys[ctrl_key]
      -- ensure only ctrl/cmd is pressed
      if ctrl_pressed then
        for key, status in pairs(keymap.modkeys) do
          if key ~= ctrl_key and status then
            ctrl_pressed = false
            break
          end
        end
      end
    end
    if ctrl_pressed then return false end
  end

  for _, child in pairs(self.childs) do
    if child:mouse_on_top(self.mouse.x, self.mouse.y) then
      if child:on_mouse_wheel(y, x) then
        return true
      end
    end
  end

  if self.scrollable then
    if keymap.modkeys["shift"] then
      x = y
      y = 0
    end
    if y and y ~= 0 then
      self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
    end
    if x and x ~= 0 then
      self.scroll.to.x = self.scroll.to.x + x * -config.mouse_wheel_scroll
    end
    return true
  end

  return false
end

---Can be overriden by widgets to listen for scale change events to apply
---any neccesary changes in sizes, padding, etc...
---@param new_scale number
---@param prev_scale number
function Widget:on_scale_change(new_scale, prev_scale)
  local font_type = type(self.font)
  if
    font_type == "userdata"
    or
    (font_type == "table" and not self.font.container)
  then
    self.font:set_size(
      self.font:get_size() * (new_scale / prev_scale)
    )
  end
  self:update_size_position()
end

---Registers a new animation to be ran on the update cycle.
---@param target? table If nil assumes properties belong to widget it self.
---@param properties table<string,number>
---@param options? widget.animation.options
function Widget:animate(target, properties, options)
  if not target then target = self end

  -- if name is set then prevent adding if another one with the same
  -- animation name is already running
  if options and options.name then
    for _, animation in ipairs(self.animations) do
      if animation.options and animation.options.name == options.name then
        return
      end
    end
  end

  table.insert(self.animations, {
    target = target,
    properties = properties,
    options = options
  })
end

---Runs all registered animations removing duplicated and finished ones.
function Widget:run_animations()
  if #self.animations > 0 then
    -- ensure animations like show_animated are more consistently rendered
    core.redraw = true
    self:schedule_update(true)

    ---@type table<widget.animation, widget.animation>
    local duplicates = {}

    local targets = {}
    local deleted = 0
    for i=1, #self.animations do
      local animation = self.animations[i - deleted]

      -- do not run animations that change same target to prevent conflicts.
      if not targets[animation.target] then
        local finished = true
        local options = animation.options or {}
        if not animation.last_value then animation.last_value = {} end
        for name, value in pairs(animation.properties) do
          if animation.target[name] ~= value then
            self:move_towards(animation.target, name, value, options.rate)
            if options.on_step then
              options.on_step(animation.target, name, animation.target[name], animation)
            end
            if animation.target[name] ~= value then
              -- avoid duplicated values from move_towards and finish the animation
              if animation.last_value[name] == animation.target[name] then
                animation.target[name] = value
              else
                finished = false
              end
            end
            animation.last_value[name] = animation.target[name]
          end
        end
        if finished then
          if options.on_complete then
            options.on_complete(self)
          end
          table.remove(self.animations, i - deleted)
          deleted = deleted + 1
        end
        targets[animation.target] = animation
      -- only registers it as duplicated if the animation does needs to
      -- perform any tasks on completion.
      elseif not targets[animation.target].on_complete then
        duplicates[targets[animation.target]] = animation
      end
    end

    -- remove older duplcated animations that modify same target and properties
    for duplicate, newer_animation in pairs(duplicates) do
      local exact_properties = true
      for name, _ in pairs(duplicate.properties) do
        if not newer_animation.properties[name] then
          exact_properties = false
          break
        end
      end
      if exact_properties then
        for name, _ in pairs(newer_animation.properties) do
          if not duplicate.properties[name] then
            exact_properties = false
            break
          end
        end
      end
      if exact_properties then
        for i, animation in ipairs(self.animations) do
          if animation == duplicate then
            table.remove(self.animations, i)
            break
          end
        end
      end
    end
  end
end

---Explicitly call the widget update procedure when the scale change even if
---the widget is not visible.
function Widget:update_if_scaled()
  if self.current_scale ~= SCALE then
    self.explicit_update = true
    self:update()
    -- after updating all widgets re-adjust sizes and positions, even if
    -- this is called directly in update -> on_scale_change it doesn't takes
    -- into account the sum of all sizing changes so we re-execute it
    self:update_size_position()
    self.explicit_update = false
  end
end

---If visible execute the widget calculations and returns true.
---@return boolean
function Widget:update()
  if self.perform_update_size_position then
    self:update_size_position()
    self.perform_update_size_position = false
  end

  if not self:is_visible() then return false end

  Widget.super.update(self)

  -- call this to be able to properly scroll
  self:update_position()

  -- run any pending animations
  self:run_animations()

  if not self.explicit_update then
    for _, child in pairs(self.childs) do
      child:update()
    end
  else
    for _, child in pairs(self.childs) do
      child:update_if_scaled()
    end
  end

  if not self.updated and self.size.y > 0 and self.size.x > 0 then
    self.updated = true
  end

  return true
end

---Similar to update, but here you should perform expensive calculations that
---will get executed for a predefined period of time when a widget is
---initialized, scale has changed or a widget switched from hidden to visible.
function Widget:update_size_position()
  for _, child in pairs(self.childs) do
    child:update_size_position()
  end
end

---Flag that indicates if the update schedule is in progress.
---@type boolean
local widget_update_scheduled = false

---Schedule a core update and redraw. Since widgets try to not fire updates
---and draws to child widgets to reduce cpu consumption this function can be
---used when a re-update and re-draw is strictly needed.
function Widget:schedule_update(delayed)
  if (self.parent or delayed) and not widget_update_scheduled then
    -- on older versions of lua coroutine.running() returns nil of on main
    -- so we also take that into account
    local co, is_main = coroutine.running()
    if not co or not is_main then
      -- already running on a coroutine so we don't need to schedule a new one
      core.redraw = true
      return
    end
    widget_update_scheduled = true
    core.add_thread(function()
      if delayed then coroutine.yield(0.1) end
      core.redraw = true
      widget_update_scheduled = false
    end)
  elseif not self.parent or delayed then
    core.redraw = true
  end
end

function Widget:draw_scrollbar()
  if self.scrollable then
    Widget.super.draw_scrollbar(self)
  end
end

---If visible draw the widget and returns true.
---@return boolean
function Widget:draw()
  if
    not self:is_visible() or not self.updated
    or
    self.size.x <= 0 or self.size.y <= 0
  then
    return false
  end

  Widget.super.draw(self)

  self:draw_border()

  if self.render_background then
    if self.background_color then
      self:draw_background(self.background_color)
    else
      self:draw_background(
        self.parent and style.background or style.background2
      )
    end
  end

  if #self.childs > 0 then
    core.push_clip_rect(
      self.position.x,
      self.position.y,
      self.size.x,
      self.size.y
    )
  end

  for i=#self.childs, 1, -1 do
    self.childs[i]:draw()
  end

  if #self.childs > 0 then
    core.pop_clip_rect()
  end

  self:draw_scrollbar()

  return true
end

---Recursively destroy all childs from the widget.
function Widget:destroy_childs()
  for _=1, #self.childs do
    self.childs[1]:destroy_childs()
    table.remove(self.childs, 1)
  end
end

---If floating, remove the widget from the floating widgets list
---to allow proper garbage collection.
function Widget:destroy()
  if not self.parent or self.defer_draw then
    for idx, widget in ipairs(floating_widgets) do
      if widget == self then
        widget:destroy_childs()
        floating_widgets[idx] = nil
        table.remove(floating_widgets, idx)
        break
      end
    end
  end
end

---Toggle the forced interception of given event even if all the conditions
---for emitting it are not met.
---
---Note: only "mouse_released" is implemented for the moment on floating views
---for use in the SelectBox, maybe a better system can be implemented on
---the future.
---@param name "mouse_released"
---@param force boolean If omitted is set to true by default
function Widget:force_event(name, force)
  if type(force) ~= "boolean" or force then
    self.force_events[name] = true
  else
    self.force_events[name] = nil
  end
end

---Flag that indicates if the rootview events are already overrided.
---@type boolean
local root_overrided = false

---Called when initializing a floating widget to generate RootView overrides,
---this function will only override the events once.
function Widget.override_rootview()
  if root_overrided then return end
  root_overrided = true

  if not RootView then RootView = require "core.rootview" end

  local root_view_on_mouse_pressed = RootView.on_mouse_pressed
  local root_view_on_mouse_released = RootView.on_mouse_released
  local root_view_on_mouse_moved = RootView.on_mouse_moved
  local root_view_on_mouse_wheel = RootView.on_mouse_wheel
  local root_view_update = RootView.update
  local root_view_draw = RootView.draw
  local root_view_on_file_dropped = RootView.on_file_dropped
  local root_view_on_text_input = RootView.on_text_input

  function RootView:on_mouse_pressed(button, x, y, clicks)
    local pressed = false
    for i=#floating_widgets, 1, -1 do
      local widget = floating_widgets[i]
      if widget.visible then
        widget.mouse_pressed_outside = not widget:mouse_on_top(x, y)
        if
          (not widget.defer_draw and not widget.child_active)
          or
          widget.mouse_pressed_outside
          or
          (pressed or not widget:on_mouse_pressed(button, x, y, clicks))
        then
          widget:swap_active_child()
        else
          pressed = true
        end
      end
    end
    if not pressed then
      return root_view_on_mouse_pressed(self, button, x, y, clicks)
    else
      return true
    end
  end

  function RootView:on_mouse_released(button, x, y)
    local released = false
    for i=#floating_widgets, 1, -1 do
      local widget = floating_widgets[i]
      if widget.visible then
        if
          (not widget.defer_draw and not widget.child_active)
          or
          (
            not widget.force_events["mouse_released"]
            and
            widget.mouse_pressed_outside
          )
          or
          not widget:on_mouse_released(button, x, y)
        then
          widget.mouse_pressed_outside = false
        else
          released = true
        end
      end
    end
    if not released then
      root_view_on_mouse_released(self, button, x, y)
    end
  end

  function RootView:on_mouse_moved(x, y, dx, dy)
    if core.active_view ~= core.command_view then
      for i=#floating_widgets, 1, -1 do
        local widget = floating_widgets[i]
        if widget.visible then
          if
            (not widget.defer_draw and not widget.child_active)
            or
            widget.mouse_pressed_outside
            or
            not widget:on_mouse_moved(x, y, dx, dy)
          then
              if
                not widget.is_scrolling and not widget.captured_widget
                and
                not widget.child_active and widget.outside_view
              then
                core.set_active_view(widget.outside_view)
                widget.outside_view = nil
              elseif widget.outside_view then
                core.request_cursor("arrow")
              end
          else
            if not widget.child_active and widget.defer_draw then
              if not widget.outside_view then
                widget.outside_view = core.active_view
              end
              core.set_active_view(widget)
            end
            return true
          end
        end
      end
      if last_hovered_child and not last_hovered_child:mouse_on_top(x, y) then
        last_hovered_child:on_mouse_moved(x, y, dx, dy)
        last_hovered_child = nil
      end
    end
    return root_view_on_mouse_moved(self, x, y, dx, dy)
  end

  function RootView:on_mouse_wheel(y, x)
    for i=#floating_widgets, 1, -1 do
      local widget = floating_widgets[i]
      if
        widget.visible and widget.defer_draw and widget:on_mouse_wheel(y, x)
      then
        return true
      end
    end
    return root_view_on_mouse_wheel(self, y, x)
  end

  function RootView:on_file_dropped(filename, x, y)
    for i=#floating_widgets, 1, -1 do
      local widget = floating_widgets[i]
      if
        widget.visible and widget.defer_draw
        and
        widget:on_file_dropped(filename, x, y)
      then
        return true
      end
    end
    return root_view_on_file_dropped(self, filename, x, y)
  end

  function RootView:on_text_input(text)
    for i=#floating_widgets, 1, -1 do
      local widget = floating_widgets[i]
      if
        widget.visible and widget.defer_draw and widget:on_text_input(text)
      then
        return true
      end
    end
    return root_view_on_text_input(self, text)
  end

  function RootView:update()
    root_view_update(self)
    local count = #floating_widgets
    for i=1, count, 1 do
      local widget = floating_widgets[i]
      if widget.visible and widget.defer_draw then
        widget:update()
      end
    end
  end

  function RootView:draw()
    local count = #floating_widgets
    for i=1, count, 1 do
      local widget = floating_widgets[i]
      if widget.visible and widget.defer_draw then
        core.root_view:defer_draw(widget.draw, widget)
      end
    end
    root_view_draw(self)
  end
end


return Widget
