--[[
  @package    eg4-crank-ui-2
  @file       keyboard.lua
  @brief      This file contains the implementation of the keyboard and numeric entry screens.
]]

local vki = require("controls/sbt-virtual-keyboard")
local virtual_keyboard
-- static map for local info
local info

---
--- @brief  Sets the cursor position in the text entry field
--- @param  mapargs  The map arguments from the callback
local function set_cursor_position(mapargs)

  local value = gre.get_value(mapargs.context_screen .. ".value")
  local len = utf8.len(value)
  local cursor_position = virtual_keyboard and virtual_keyboard:GetInsertPos() or len
  if cursor_position < 0 then cursor_position = 0
  elseif cursor_position > len then cursor_position = #value end

  local field = mapargs.context_layer..".field"

  if gre.get_value(mapargs.context_screen .. ".secret") == 1 and gre.get_value(field..".show.alpha_pressed") == 0 then
    value = string.rep("*", len)
  end

  local width = gre.get_value(field..".value.grd_width")
  if not info then info = {} end
  info.font = info.font or gre.get_value("font_regular")
  info.size = info.size or gre.get_value(field..".value.font_size")

  local text_width = gre.get_string_size(info.font, info.size, value).width
  local x = gre.get_string_size(info.font, info.size, value:sub(1, utf8.offset(value, cursor_position + 1) - 1)).width
  local xoffset = cursor_position == 0 and 0 or gre.get_value(field..".xoffset") or 0

  if text_width <= width then
    xoffset = 0
  else
    if x + xoffset < 0 then
      xoffset = xoffset + x
      x = 0
    elseif xoffset + x - 2 > width then
      xoffset = xoffset + width - x - 2
    end

    if xoffset > 0 then
      xoffset = 0
    elseif xoffset < width - text_width then
      xoffset = width - text_width
    end
  end

  gre.set_value(field..".xoffset", xoffset,
                field..".cursor.x", x + xoffset,
                field..".cursor_highlight.x", x + xoffset,
                field..".width", text_width,
                field..".highlight.grd_hidden", info.start_value == "" and 1 or 0)
end

---
--- @brief  Validates the entered value
--- @param  context_screen  The context screen
--- @param  value           The value to validate
--- @return true if the value is valid, nil otherwise
local function validate_value(context_screen, value)
  local validate = gre.get_value(context_screen .. ".validate")
  return type(_G[validate]) ~= "function" or _G[validate](value, context_screen)
end

---
--- @brief  Indicate the current entry validity
--- @param  field  The field to set the validation state for
--- @param  value_ok  true if the value is valid, false otherwise
local last_field  -- track last field
local function set_validation_state(field, value_ok)
  if info.skip_validation then
    info.skip_validation = nil
    return
  end

  field = field or last_field
  last_field = field
  gre.set_value(field..".hint.grd_hidden", value_ok and 0 or 1,
                field..".focus_ok.grd_hidden", value_ok and 0 or 1,
                field..".error.grd_hidden", value_ok and 1 or 0,
                field..".focus_error.grd_hidden", value_ok and 1 or 0)
end

---
--- @brief  Indicate the current entry validity
function cb_show_entry_invalid(mapargs)
  set_validation_state(nil, false)
end

---
-- @brief  Update the screen when the value changes
-- @param  mapargs  The map arguments from the callback
function cb_entry_value_updated_text(mapargs)
  local field = mapargs.context_layer..".field"

  local data = {}
  local value = gre.get_value(mapargs.context_screen .. ".value")

  data[field..".cursor_highlight.grd_hidden"] = 1
  data[field..".request.grd_hidden"] = #value == 0 and 0 or 1
  data[field..".cursor.grd_hidden"] = #value == 0 and 1 or 0
  data[field..".cursor_hitbox.grd_hidden"] = #value == 0 and 1 or 0

  local hide_value = gre.get_value(mapargs.context_screen .. ".secret") == 1 and gre.get_value(field..".show.alpha_pressed") == 0
  data[field..".secret.grd_hidden"] = not hide_value
  data[field..".value.grd_hidden"] = hide_value
  local secret_text = string.rep("*", utf8.len(value))
  data[field..".secret.text"] = secret_text

  set_cursor_position(mapargs)

  local value_ok = validate_value(mapargs.context_screen, value)
  set_validation_state(field, value_ok)

  gre.set_data(data)
end

---
-- @brief  Adds extra keys to the numeric entry keypad. Support for up to 2 extra keys is provided.
-- @param  mapargs  The map arguments from the callback
local function update_extra_keys_for_numeric_entry(mapargs)
  local extra_1 = gre.get_value(mapargs.context_screen..".extra_key_1")
  local extra_2 = gre.get_value(mapargs.context_screen..".extra_key_2")
  local context_group = mapargs.context_layer..".keypad"
  if extra_1 and extra_1 ~= "" then
    if extra_2 and extra_2 ~= "" then
      local x = gre.get_value(context_group..".8.grd_x")
      local w = gre.get_value(context_group..".8.grd_width")
      gre.set_value(context_group..".0.grd_x", x,
                    context_group..".0.grd_width", w,
                    context_group..".extra_1.text", extra_1,
                    context_group..".extra_1.grd_hidden", 0,
                    context_group..".extra_2.text", extra_2,
                    context_group..".extra_2.grd_hidden", 0)
    else
      local x = gre.get_value(context_group..".8.grd_x")
      local w = gre.get_value(context_group..".9.grd_x") - x + gre.get_value(context_group..".9.grd_width")
      gre.set_value(context_group..".0.grd_x", x,
                    context_group..".0.grd_width", w,
                    context_group..".extra_1.text", extra_1,
                    context_group..".extra_1.grd_hidden", 0,
                    context_group..".extra_2.grd_hidden", 1)
    end
  else
    if extra_2 and extra_2 ~= "" then
      local x = gre.get_value(context_group..".7.grd_x")
      local w = gre.get_value(context_group..".8.grd_x") - x + gre.get_value(context_group..".8.grd_width")
      gre.set_value(context_group..".0.grd_x", x,
                    context_group..".0.grd_width", w,
                    context_group..".extra_1.grd_hidden", 1,
                    context_group..".extra_2.text", extra_2,
                    context_group..".extra_2.grd_hidden", 0)
    else
      local x = gre.get_value(context_group..".7.grd_x")
      local w = gre.get_value(context_group..".9.grd_x") - x + gre.get_value(context_group..".9.grd_width")
      gre.set_value(context_group..".0.grd_x", x,
                    context_group..".0.grd_width", w,
                    context_group..".extra_1.grd_hidden", 1,
                    context_group..".extra_2.grd_hidden", 1)
    end
  end
end



---
-- @brief  Adds extended keys to the numeric entry keypad. Support for up to 2 extra keys is provided.
-- @param  mapargs  The map arguments from the callback
local function update_extended_keys_page(mapargs)
  local offset = info.extended_keys_page
  for i = 1,8 do
    local key = info.extended_keys:sub(offset + i, offset + i)
    gre.set_value(mapargs.context_layer..".keypad.a"..i..".text", key,
                  mapargs.context_layer..".keypad.a"..i..".grd_hidden", key == "" and 1 or 0)
  end
  gre.set_value(mapargs.context_layer..".keypad.paging.grd_hidden", info.extended_keys:len() > 8 and 0 or 1)

end

function cb_page_extended_keys(mapargs)
  local i = info.extended_keys_page + 7
  if i >= info.extended_keys:len() then i = 0 end
  info.extended_keys_page = i
  update_extended_keys_page(mapargs)
end

---
-- @brief  Adds extended keys to the numeric entry keypad. Support for up to 2 extra keys is provided.
-- @param  mapargs  The map arguments from the callback
function update_extended_keys_for_numeric_entry(mapargs)
  info.extended_keys = ""
  info.extended_keys_page = 0
  local fn = gre.get_value(mapargs.context_screen..".extended_keys_fn")
  if type(_G[fn]) == "function" then
    info.extended_keys = _G[fn](mapargs.value)
  end
  update_extended_keys_page(mapargs)
end

---
-- @brief  Change visible keypad group to the group specified by the name of the control.
-- @param  mapargs  The map arguments from the callback
local function change_key_group(mapargs)
  local group = mapargs.context_layer.."."..mapargs.context_control:object_name()
  if gre.get_value(mapargs.context_layer..".controls.placeholder.grd_hidden") == 0 then
    local y = gre.get_value(mapargs.context_layer..".controls.placeholder.grd_y")
    local h = gre.get_value(mapargs.context_layer..".controls.placeholder.grd_height")
    local disable_symbols = tonumber(gre.get_value(mapargs.context_screen..".disable_symbols")) or 0
    gre.set_value(mapargs.context_layer..".keypad.grd_hidden", 1,
                  mapargs.context_layer..".keyboard.grd_hidden", 1,
                  mapargs.context_layer..".symbols.grd_hidden", 1,
                  group..".grd_hidden", 0,
                  group..".keyboard.grd_y", y,
                  group..".keyboard.grd_height", h,
                  group..".keyboard.grd_hidden", 0,
                  group..".keypad.grd_y", y,
                  group..".keypad.grd_height", h,
                  group..".keypad.grd_hidden", 0,
                  group..".symbols.grd_y", y,
                  group..".symbols.grd_height", h,
                  group..".symbols.grd_hidden", disable_symbols)
  end
end

---
-- @brief  Updates the keyboard controls on the right side
-- @param  mapargs  The map arguments from the callback
local function update_control_keys(mapargs)
  local f1 = gre.get_value(mapargs.context_screen..".function_fn_1") or ""
  local k1 = gre.get_value(mapargs.context_screen..".function_key_1") or ""
  local f2 = gre.get_value(mapargs.context_screen..".function_fn_2") or ""
  local k2 = gre.get_value(mapargs.context_screen..".function_key_2") or ""
  local function_1 = f1 == "" or k1 == ""
  local function_2 = f2 == "" or k2 == ""
  local keypad_only = gre.get_value(mapargs.context_layer..".keyboard.grd_hidden") == nil
  local group = mapargs.context_layer..".controls"
  gre.set_value(group..".function_1.grd_hidden", function_1 and 1 or 0,
                group..".function_2.grd_hidden", function_2 and 1 or 0,
                group..".placeholder.grd_hidden", keypad_only and 1 or 0,
                mapargs.context_layer..".keypad.grd_hidden", keypad_only and 0 or 1)

  local list = {}
  for _,object in ipairs(gredom.get_object(group):get_children()) do
    if not object:get_hidden() then
      table.insert(list, object)
    end
  end

  local offset = gre.get_value(group..".function_1.grd_y")
  local total = gre.get_value(group..".ok.grd_y") + gre.get_value(group..".ok.grd_height") - offset
  local spacer = gre.get_value(group..".spacer")
  if #list == 1 then
    list[1]:set_y(offset)
    list[1]:set_height(total)
  elseif #list == 2 then
    local height = (total - spacer) / 2
    list[2]:set_y(offset)
    list[2]:set_height(height)
    list[1]:set_y(offset + height + spacer)
    list[1]:set_height(total - (height + spacer))
  elseif #list == 3 then
    local height = (total - 3 * spacer) / 4
    list[3]:set_y(offset)
    list[3]:set_height(height)
    list[2]:set_y(offset + height + spacer)
    list[2]:set_height(height)
    list[1]:set_y(offset + 2 * (height + spacer))
    list[1]:set_height(total - 2 * (height + spacer))
  elseif #list == 4 then
    local height = (total - 3 * spacer) / 4
    list[4]:set_y(offset)
    list[4]:set_height(height)
    list[3]:set_y(offset + height + spacer)
    list[3]:set_height(height)
    list[2]:set_y(offset + 2 * (height + spacer))
    list[2]:set_height(height)
    list[1]:set_y(offset + 3 * (height + spacer))
    list[1]:set_height(total - 3 * (height + spacer))
  elseif #list == 5 then
    local height = (total - 4 * spacer) / 5
    list[5]:set_y(offset)
    list[5]:set_height(height)
    list[4]:set_y(offset + height + spacer)
    list[4]:set_height(height)
    list[3]:set_y(offset + 2 * (height + spacer))
    list[3]:set_height(height)
    list[2]:set_y(offset + 3 * (height + spacer))
    list[2]:set_height(height)
    list[1]:set_y(offset + 4 * (height + spacer))
    list[1]:set_height(total - 4 * (height + spacer))
  end

  mapargs.context_control = gre.get_value(mapargs.context_screen..".start_key_group") or "keyboard"
  change_key_group(mapargs)
end


---
--- @brief  Initialise the visible keyboard layer for the code entry screen option.
local function init_use_code_entry(mapargs)
  -- Check if the code entry layer is present on the current screen
  if gre.get_value(mapargs.context_screen..".code_entry_layer.grd_x") == nil then return end
  local value = sbsettings.get_use_code_entry_keypad()
  gre.set_value(mapargs.context_screen..".code_entry_layer.grd_hidden", 1 - value,
                mapargs.context_screen..".text_entry_layer.grd_hidden", value )
end

---
-- @brief  Fetch the keyboard layer for the context screen
-- @param  mapargs  The map arguments from the callback
-- @return The layer name or nil if not found
local function fetch_keyboard_layer(mapargs)
  init_use_code_entry(mapargs)

  -- Return first visible keyboard layer or first keyboard layer if none are visible
  local layers = mapargs.context_screen:get_children()
  if layers == nil then return nil end
  local layer
  for i = #layers,1,-1 do
    local item = tostring(layers[i])
    if gre.get_value(item..".field.grd_x") then
      layer = item:object_name()
      if gre.get_value(item..".grd_hidden") == 0 then
        return layer
      end
    end
  end
  return layer
end

---
--- @brief  Reset the active keyboard for the layer
--- @param  layer  The layer to reset
local function reset_active_keyboard(layer)
  local groups = layer:get_children()
  local data = {}
  for i = 1, #groups-1 do
    local group = tostring(groups[i])
    if not group:ends_with(".background") then
      data[group..".grd_hidden"] = (i == #groups-1) and 0 or 1
    end
  end
  gre.set_data(data)
  return nil
end

---
--- @brief  Get the value for the maximum string length. This can also be either a
---         global or sbsettings function.
--- @param  context_screen  The context screen
--- @return The maximum string length
local function get_max_len(context_screen)
  local value = gre.get_value(context_screen .. ".max_len")
  -- check for custom max_len function
  if type(_G[tostring(value)]) == "function" then
    value = _G[tostring(value)]()
  elseif type(sbsettings[tostring(value)]) == "function" then
    value = sbsettings[tostring(value)]()
  end
  return tonumber(value)
end

--- Emulates a key down/key up event
--
--@param ucs2Value A UCS2 code point value
--@param text The UTF8 text from which the USC2 code point was derived.
--@param modifiers The shift modifiers for the keyboard input.
--@param KVI The instance of the keyboard
local function cb_vki_key_emulation(code, text, modifiers, virtualKeyboard, channel)
  if(modifiers == nil) then
    modifiers = 0
  end

  local active_screen = gre.env("active_screen")

  local args = {}
  args["code"] = code
  args["modifiers"] = modifiers

  if(code < math.pow(2,16)) then --Less than 2 bytes
    args["key"] = code
  end

  if(channel == nil) then
    gre.send_event_data("gre.keydown", "4u1 code 2u1 key 2u1 modifiers", args)
    gre.send_event_data("gre.keyup", "4u1 code 2u1 key 2u1 modifiers", args)
  else
    gre.send_event_data("gre.keydown", "4u1 code 2u1 key 2u1 modifiers", args, channel)
    gre.send_event_data("gre.keyup", "4u1 code 2u1 key 2u1 modifiers", args, channel)
  end
end

---
--- @brief  Clear the selected text in the text entry field
--- @return true if the text was cleared, nil otherwise
function clear_selected_text()
  if info.start_value == "" then return end
  gre.set_value(gre.env("active_screen")..".value", "")
  info.start_value = ""
  return true
end

---
--- @brief  Initialise the keyboard.
--- @param  gre#context mapargs
---
local function vki_init(mapargs)
  virtual_keyboard = vki.new(
    mapargs.context_screen,
    mapargs.context_layer,
    "keyboard",
    "key_template_control",
    mapargs.context_screen..".value",
    gre.get_value("kb_layout"),
    gre.get_value("font_normal"),
    get_max_len(mapargs.context_screen),
    (gre.get_value(mapargs.context_screen .. ".upper_case") == 1) and 2 or 0,
    clear_selected_text)
  virtual_keyboard:SetEmulation(cb_vki_key_emulation)
end

---
--- @brief  Process key press
--- @param  gre#context mapargs
---
function cb_vki_key_press(mapargs)
  virtual_keyboard:Press(mapargs)
end

---
--- @brief  Process key release
--- @param  gre#context mapargs
---
function cb_vki_key_release(mapargs)
  virtual_keyboard:Release(mapargs)
end

---
--- @brief  Process key outbound
--- @param  gre#context mapargs
---
function cb_vki_key_outbound(mapargs)
  virtual_keyboard:Outbound(mapargs)
end

---
--- @brief  Process short cut ended
--- @param  gre#context mapargs
---
function cb_vki_shortcut_stop(mapargs)
  virtual_keyboard:StopShortcut(mapargs)
end

---
--- @brief  Process short cut key selected
--- @param  gre#context mapargs
---
function cb_vki_shortcut_key_press(mapargs)
  virtual_keyboard:ShortcutPress(mapargs)
end

---
--- @brief  Process short cut key selected
--- @param  gre#context mapargs
---
function CBVKI_SCKeyRelease(mapargs)
  virtual_keyboard:ShortcutRelease(mapargs)
end

---
-- @brief  Callback to reset the text entry field
-- @param  mapargs  The map arguments from the callback
function cb_reset_text_entry(mapargs)
  info = {
    secret = gre.get_value(mapargs.context_screen .. ".secret") or 0,
    max_len = get_max_len(mapargs.context_screen) or 10,
    hot_key_function = _G[gre.get_value(mapargs.context_screen .. ".process_hot_key")],
    auto_accept_timeout = gre.get_value(mapargs.context_screen .. ".auto_accept_timeout") or 0,
  }

  if info.secret == 1 then
    info.start_value = ""
  else
    info.start_value = gre.get_value(mapargs.context_screen .. ".start_value") or ""
  end

  gre.set_value(
    mapargs.context_screen .. ".value", info.start_value,
    mapargs.context_screen .. ".start_value", ""
  )

  -- Determine the keyboard layer. This will be the highest layer in the screen
  mapargs.context_layer = fetch_keyboard_layer(mapargs)
  vki_init(mapargs)

  -- Set show control
  local show = mapargs.context_layer..".field.show"
  gre.set_value(show..".grd_hidden", gre.get_value(mapargs.context_screen..".secret") ~= 1)
  gre.set_value(show..".alpha_pressed", 0)

  update_extra_keys_for_numeric_entry(mapargs)
  update_extended_keys_for_numeric_entry(mapargs)
  update_control_keys(mapargs)

  cb_entry_value_updated_text(mapargs)
end

---
--- @brief  Callback to process the ok key operation
--- @param  mapargs  The map arguments from the callback
local function do_keypress_ok(screen, layer)
  local value = gre.get_value(screen..".value")
  local submit = gre.get_value(screen..".submit")
  local value_ok = _G[submit](value, gre.get_value(screen..".port_id"))
  info.skip_validation = nil
  set_validation_state(layer..".field", value_ok)
  if value_ok then
    local event = gre.get_value(screen..".event")
    if event and event ~= "" then send_event(event) end
  else
    if value ~= "" and gre.get_value(screen .. ".secret") == 1 then
      gre.set_value(screen..".value", "")
      info.skip_validation = true
    end
    cb_error_tone()
  end
end

---
--- @brief  Callback to process the ok key operation
--- @param  mapargs  The map arguments from the callback
function cb_keypress_ok(mapargs)
  do_keypress_ok(mapargs.context_screen, mapargs.context_layer)
end

---
--- @brief  Stop the auto accept timer
function cb_stop_auto_accept_entry()
  if not info.auto_accept_timer then return end
  gre.timer_clear_timeout(info.auto_accept_timer)
  info.auto_accept_timer = nil
end

---
--- @brief  Restart the auto accept timer and process the ok key operation when the timer expires
--- @param  mapargs  The map arguments from the callback
local function restart_auto_accept(mapargs)
  cb_stop_auto_accept_entry()
  if info.auto_accept_timeout == 0 then return end
  local screen, layer = mapargs.context_screen, mapargs.context_layer
  info.auto_accept_timer = gre.timer_set_timeout(info.auto_accept_timeout, function()
    do_keypress_ok(screen, layer)
    info.auto_accept_timer = nil
  end)
end

---
--- @brief Process the backspace key operation
--- @param  mapargs  The map arguments from the callback
function cb_keypress_backspace(mapargs)
  if clear_selected_text() then return end
  virtual_keyboard:VKI_insert("\b")
end

---
--- @brief Process the keyboard key operation
--- @param  mapargs  The map arguments from the callback
function cb_keypress_keypad(mapargs)
  change_key_group(mapargs)
end

---
--- @brief Process the keypad key operation
--- @param  mapargs  The map arguments from the callback
function cb_keypress_keyboard(mapargs)
  gre.set_value(mapargs.context_group..".symbols.grd_hidden", 0)
  virtual_keyboard:VKI_symbols(false)
  change_key_group(mapargs)
end

---
--- @brief Process the symbols key operation
--- @param  mapargs  The map arguments from the callback
function cb_keypress_symbols(mapargs)
  gre.set_value(mapargs.context_control..".grd_hidden", 1)
  virtual_keyboard:VKI_symbols(true)
end

---
-- @brief  Process a keypress
-- @param  mapargs  The map arguments from the callback
local function do_keypress(mapargs)
  restart_auto_accept(mapargs)
  local key = gre.get_value(mapargs.context_control..".text")

  -- If start_value is highlighted and the value is changed, remove the start_value
  if info.start_value ~= "" then
    cb_keypress_backspace(mapargs)
  end

  virtual_keyboard:VKI_insert(key)
  if info.hot_key_function then
    info.hot_key_function(key)
  end
end

---
-- @brief  Transition to the browser screen
-- @param  mapargs  The map arguments from the callback
function cb_keypress_browse(mapargs)
  screen_transition(gre.get_value(mapargs.context_screen..".browser"))
end

---
-- @brief  Process the extra key 1 operation
-- @param  mapargs  The map arguments from the callback
function cb_keypress_extra_1(mapargs)
  local fn = _G[gre.get_value(mapargs.context_screen..".extra_fn_1")]
  if type(fn) == 'function' then
    fn(mapargs)
    return
  end
  do_keypress(mapargs)
end

---
-- @brief  Process the extra key 2 operation
-- @param  mapargs  The map arguments from the callback
function cb_keypress_extra_2(mapargs)
  local fn = _G[gre.get_value(mapargs.context_screen..".extra_fn_2")]
  if type(fn) == 'function' then
    fn(mapargs)
    return
  end
  do_keypress(mapargs)
end

---
-- @brief  Process the extra key 1 operation
-- @param  mapargs  The map arguments from the callback
function cb_keypress_function_1(mapargs)
  local fn = _G[gre.get_value(mapargs.context_screen..".function_fn_1")]
  if type(fn) == 'function' then
    fn(mapargs)
    return
  end
  do_keypress(mapargs)
end

---
-- @brief  Process the extra key 2 operation
-- @param  mapargs  The map arguments from the callback
function cb_keypress_function_2(mapargs)
  local fn = _G[gre.get_value(mapargs.context_screen..".function_fn_2")]
  if type(fn) == 'function' then
    fn(mapargs)
    return
  end
  do_keypress(mapargs)
end

---
-- @brief  Callback to process a keypress
-- @param  mapargs  The map arguments from the callback
function cb_keypress(mapargs)
  local key = mapargs.context_control:object_name()
  local fn = "cb_keypress_"..key
  if type(_G[fn]) == "function" then
    _G[fn](mapargs)
    return
  end
  do_keypress(mapargs)
end

---
-- @brief  Callback when the backspace key is released or goes out of bounds
function cb_backspace_release()
  if info.clear_value_timer then
    gre.timer_clear_timeout(info.clear_value_timer)
    info.clear_value_timer = nil
  end
end

---
-- @brief  Callback when the backspace key is pressed
function cb_backspace_press(mapargs)
  cb_backspace_release()
  local args = {
    screen = mapargs.screen,
    layer = mapargs.context_layer,
  }
  info.clear_value_timer = gre.timer_set_timeout(700, function()
    info.clear_value_timer = nil
    -- Clear text entry field when timeout expires
    mapargs.value = ""
    mapargs.context_group = mapargs.context_layer..".field"
    gre.set_value(mapargs.context_screen .. ".value", mapargs.value)
    virtual_keyboard:SetInsertPos()
    restart_auto_accept(mapargs)
  end)
end

---
-- @brief  Updates the cursor position in relation to current touch point
--
--         Calculates and sets the position of the cursor in relation
--         to the current touch point.
-- @param  mapargs  The map arguments from the callback
local function update_cursor(mapargs)
  local text = gre.get_value(mapargs.context_screen .. ".value")
  if gre.get_value(mapargs.context_screen .. ".secret") == 1 then
    text = string.rep("*", utf8.len(text))
  end

  local field = mapargs.context_group
  local control = field..".value"
  local x
  if gre.get_value(mapargs.context_screen .. ".disable_cursor") == 1 then
    x = 9999
  else
    x = mapargs.context_event_data.x - (
      gre.get_layer_attrs(mapargs.context_layer, "x").x +
      gre.get_value(field..".grd_x") +
      gre.get_value(control..".grd_x"))
  end

  local width = gre.get_value(control..".grd_width")
  local xoffset = gre.get_value(field..".xoffset") or 0

  virtual_keyboard:SetInsertPos()
  x = x - xoffset
  local w1 = 0
  for i=1,utf8.len(text) do
    local w2 = gre.get_string_size(info.font, info.size, text:sub(1, utf8.offset(text, i + 1) - 1)).width
    if w2 >= -xoffset then
      if x < (w1 + w2) / 2 then
        virtual_keyboard:SetInsertPos(i - 1)
        if -w1 > xoffset then
          gre.set_value(mapargs.context_group..".xoffset", -w1)
        end
        break
      end
    end
    w1 = w2
  end
  info.start_value = ""
  set_cursor_position(mapargs)
end

---
-- @brief  Call back to process the current touch point when the cursor is being moved
-- @param  mapargs  The map arguments from the callback
function cb_move_cursor(mapargs)
  if not info.start_cursor_position then return end
  update_cursor(mapargs)
end

---
-- @brief  Call back to start the cursor move process.
-- @param  mapargs  The map arguments from the callback
function cb_start_cursor(mapargs)
  local value = gre.get_value(mapargs.context_screen .. ".value")
  if #value == 0 then return end
  local field = mapargs.context_group
  info.start_cursor_position = virtual_keyboard:GetInsertPos() or 0
  info.start_cursor_xoffset = gre.get_value(field..".xoffset") or 0
  info.cursor_hitbox_zindex = gre.get_value(field..".cursor_hitbox.grd_zindex")
  info.start_cursor_height = gre.get_value(field..".cursor_hitbox.grd_height")
  gre.set_value(
    field..".cursor_hitbox.grd_zindex", 255,
    field..".cursor_highlight.grd_hidden", 0,
    field..".cursor_hitbox.grd_height", info.start_cursor_height + 100)
  cb_move_cursor(mapargs)
end

---
-- @brief  Call back to stop the cursor move process.
-- @param  mapargs  The map arguments from the callback
function cb_release_cursor(mapargs)
  if not info.start_cursor_position then return end
  info.start_cursor_position = nil
  local field = mapargs.context_group
  gre.set_value(field..".cursor_highlight.grd_hidden", 1,
                field..".cursor_hitbox.grd_zindex", info.cursor_hitbox_zindex,
                field..".cursor_hitbox.grd_height", info.start_cursor_height)
  cb_move_cursor(mapargs)
end

---
-- @brief  Call back to cancel the cursor move process and restore the
--         cursor to its original position.
-- @param  mapargs  The map arguments from the callback
function cb_cancel_cursor(mapargs)
  if not info.start_cursor_position then return end
  info.cursor_position = info.start_cursor_position
  info.start_cursor_position = nil
  local field = mapargs.context_group
  gre.set_value(field..".xoffset", info.start_cursor_xoffset,
                field..".cursor_highlight.grd_hidden", 1,
                field..".cursor_hitbox.grd_zindex", info.cursor_hitbox_zindex,
                field..".cursor_hitbox.grd_height", info.start_cursor_height)
  set_cursor_position(mapargs)
end

---
-- @brief  Checks the IP address as it is being entered to ensure that the
--         user is entering a valid IP address. It checks that the text so
--         far enter may lead to a valid IP address.
-- @param  ip_address  The IP address to check
-- @return true if the IP address entry is going well, false otherwise
function cb_validate_ip_address_entry(ip_address)
  local chunks = ip_address:split(".")
  if #chunks > 4 then return false end
  for i,v in pairs(chunks) do
    if i == #chunks and v == "" then return true end
    v = tonumber(v)
    if v == nil or v > 255 then return false end
  end
  return true
end

---
-- @brief  Checks the IP address as it is valid.
-- @param  ip_address  The IP address to check
-- @return true if the IP address entry is going well, false otherwise
function cb_validate_ip_address_value(ip_address)
  local chunks = ip_address:split(".")
  if #chunks ~= 4 then return false end
  for i,v in pairs(chunks) do
    v = tonumber(v)
    if v == nil or v > 255 then return false end
  end
  return true
end

---
-- @brief  Checks the subnet mask as it is being entered to ensure that the
--         user is entering a valid information. It checks that the text so
--         far enter may lead to a valid subnet mask.
-- @param  value  The subnet mask to check
-- @return true if the subnet mask entry is going well, nil otherwise
function cb_validate_netmask_entry(value)
  local chunks = value:split(".")
  if #chunks > 4 then return end
  local valid_chunk_values = {
    ["255"] = true,
    ["254"] = true,
    ["252"] = true,
    ["248"] = true,
    ["240"] = true,
    ["224"] = true,
    ["192"] = true,
    ["128"] = true,
    ["000"] = true,
    ["00"] = true,
    ["0"] = true
  }
  local progress = 'start'
  for i,v in pairs(chunks) do
    if i == #chunks and v == "" then return true end
    local byte = tonumber(v)
    if progress == 'start' then
      if byte ~= 255 then
        progress = 'middle'
      end
    end
    if progress == 'middle' then
      if byte == 255 then return end
      if i == #chunks then
        for c,_ in pairs(valid_chunk_values) do
          if c:find("^"..v) then
            return true
          end
        end
      end
      if not valid_chunk_values[v] then return end
      progress = 'end'
    elseif progress == 'end' then
      if byte ~= 0 then return end
    end
  end
  return true
end

---
-- @brief  validates the subnet mask value
-- @param  value  The subnet mask to check
-- @return true if the subnet mask entry is going well, nil otherwise
function cb_validate_netmask_value(value)
  local chunks = value:split(".")
  if #chunks ~= 4 then return end
  local binary = ""
  for _,v in pairs(chunks) do
    v = tonumber(v)
    if v == nil or v > 255 then return end
    for i = 7,0,-1 do
      if v >= 2^i then
        binary = binary .. "1"
        v = v - 2^i
      else
        binary = binary .. "0"
      end
    end
  end
  return binary:find("^1*0*$") and true or nil
end

---
--- @brief  Hides the search text entry keyboard
function cb_hide_search_keyboard(mapargs)
  cb_reset_tabs(mapargs)
  gre.set_value(
    mapargs.context_screen..".search_text_entry_layer.grd_hidden", 1,
    mapargs.context_screen..".browser_heading_layer.grd_hidden", 0,
    mapargs.context_screen:find_child_object("navigate")..".grd_hidden", 0)
end

---
--- @brief  Hides the search text entry keyboard from pressing OK.
function cb_hide_search_keyboard_from_submit()
  cb_hide_search_keyboard({ context_screen = gre.env("active_screen") })
  return true
end

---
--- @brief  Cache the entry defaults for the screen
--- @param  screen  The screen to cache the defaults for
local entry_defaults_cache = {}
local function cache_entry_defaults(screen)
  if entry_defaults_cache[screen] then return end
  entry_defaults_cache[screen] = {}
  for _,item in ipairs(gredom.get_object(screen):get_variables()) do
    entry_defaults_cache[screen][item:object_name()] = gre.get_value(item)
  end
end


--- @brief Initialise the entry screen values from the context control
--- @param screen The screen to initialise
--- @param context_control The context control which contains the values to initialise the screen
local function initialise_entry_screen(mapargs)
  cache_entry_defaults(mapargs.screen)

  -- Copy the values from the context control to the entry screen
  local data = {}
  local prefix = "entry_"
  for _,item in ipairs(gredom.get_object(mapargs.context_control):get_variables()) do
    item = item:object_name()
    if item:starts_with(prefix) then
      local value = gre.get_value(mapargs.context_control.."."..item)
      data[mapargs.screen.."."..item:sub(#prefix + 1)] = value
    end
  end

  -- Ensure that defaults are applied for the remaining items are set.
  for k,v in pairs(entry_defaults_cache[mapargs.screen]) do
    if not data[mapargs.screen.."."..k] then
      data[mapargs.screen.."."..k] = v
    end
  end

  local value = ""
  if mapargs.value then
    value = mapargs.value
  elseif gre.get_value(mapargs.context_control..".format") ~= "" then
    -- Fetch raw value if the context control has a format
    local fn = "get_"..mapargs.context_control:object_name()
    fn = _G[fn] or sbsettings[fn]
    local port_id = gre.get_value(mapargs.context_screen..".port_id")
    value = fn and fn(port_id) or ""
  else
    value = gre.get_value(mapargs.context_control..".value") or ""
  end
  data[mapargs.screen..".start_value"] = value

  data[mapargs.screen..".port_id"] = gre.get_value(mapargs.context_screen ..".port_id") or ""

  gre.set_data(data)
end

---
--- @brief  Copy required parameters and start numeric entry
function cb_numeric_entry_start(mapargs)
  mapargs.screen = "general_numeric_entry"
  initialise_entry_screen(mapargs)
  if tonumber(gre.get_value(mapargs.context_control..".entry_no_login")) == 1 then
    screen_transition(mapargs.screen)
  else
    cb_maintenance_settings_start(mapargs)
  end
end

---
--- @brief  Copy required parameters and start numeric entry
function cb_text_entry_start(mapargs)
  mapargs.screen = "general_text_entry"
  initialise_entry_screen(mapargs)
  if tonumber(gre.get_value(mapargs.context_control..".entry_no_login")) == 1 then
    screen_transition(mapargs.screen)
  else
    cb_maintenance_settings_start(mapargs)
  end
end

---
--- @brief  Start the IP address entry
--- @param  mapargs  The map arguments from the callback
function cb_ip_address_entry_start(mapargs)
  mapargs.screen = "ip_address_entry"

  -- If not set, copy the entry heading from the context control text
  local heading = gre.get_value(mapargs.context_control..".entry_heading")
  if not heading then
    gre.set_value(mapargs.context_control..".entry_heading", gre.get_value(mapargs.context_control..".text") or "")
  end

  initialise_entry_screen(mapargs)
  if tonumber(gre.get_value(mapargs.context_control..".entry_no_login")) == 1 then
    screen_transition(mapargs.screen)
  else
    cb_maintenance_settings_start(mapargs)
  end
end
