---
-- @package   eg4-crank-ui
-- @file      text-entry.lua
-- @brief     Functionality supporting text entry
--
-- @copyright Copyright 2019 Hanover Displays Limited.
-- @license   This program is the confidential and proprietary product of
--            Hanover Displays Limited. Any unauthorised use, reproduction or
--            transfer of this program is strictly prohibited. (Subject to 
--            limited distribution and restricted disclosure only.) All 
--            rights reserved.                                              
--
--

local VKI = require("sbt/sbt-virtual-keyboard")

local cursor_control = nil
local cursor_count = 0
local cursor_timer = nil
local cursor_moving = nil
local auto_accept_entry = {}
local highlight_active

---
--- @brief  Updates the 'blinking' text entry cursor.
---
local function updateCursorControl()
  cursor_count = cursor_count + 1
  if cursor_count == 4 then
    cursor_count = 0
    gre.set_control_attrs(cursor_control, { hidden = 1 })
  else
    gre.set_control_attrs(cursor_control, { hidden = 0 })
  end
end

---
--- @brief  Resets the timer and control for the 'blinking' text entry cursor.
--- @param  control - cursor control reference
---
local function resetCursorControl(control)
  if control ~= nil then cursor_control = control end
  if cursor_timer ~= nil then
    gre.timer_clear_interval(cursor_timer)
  end
  cursor_timer = gre.timer_set_interval(updateCursorControl, 250)
  cursor_count = 0
  updateCursorControl()
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 CBKVI_KeyEmulation(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  Initialise the keyboard.
--- @param  gre#context mapargs
---
function VKI_Init(mapargs)
  local kb_layout = gre.get_value(mapargs.context_layer .. ".kb_layout")
  local max_len = gre.get_value(mapargs.context_screen .. ".max_len")
  local font = gre.get_value("font_normal")
  virtualKeyboard = VKI.new(mapargs.context_screen, mapargs.context_layer, "key_template_control", mapargs.context_screen .. ".value", kb_layout, font, "not.available", max_len)
  virtualKeyboard:SetEmulation(CBKVI_KeyEmulation)
end

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

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

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

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

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

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

---
--- @brief  Default text entry submit function
--- @param  gre#context mapargs
---
function CBTextEntrySubmitDefault(mapargs)
  print(mapargs.value)
end

---
--- @brief  Sets the cursor position using the value provided.
--- @param  gre#context mapargs
--- @return position of cursor
local function SetCursorPosition(context_group, text_control, value)
  local cursor_pos = 1
  if value ~= "" then
    local font = gre.get_value("font_bold")
    local size = gre.get_value(text_control .. ".font_size")
    local total_width = gre.get_string_size(font, size, value).width
    local width = gre.get_value(text_control..".grd_width")
    local insert_pos = virtualKeyboard:GetInsertPos() or #value

    cursor_pos = gre.get_string_size(font, size, value:sub(1, insert_pos)).width + 1
    if total_width > width then
      if cursor_pos >= total_width then
        cursor_pos = width
        gre.set_value(context_group..".xoffset",  width - total_width)
      else
        local xoffset = gre.get_value(context_group..".xoffset")
        if total_width + xoffset <= width then
          xoffset = width - total_width
        end
        cursor_pos = cursor_pos + xoffset
        if cursor_pos > width then
          xoffset = xoffset + (width - cursor_pos)
          cursor_pos = width
        end
        gre.set_value(context_group..".xoffset",  xoffset)
      end
      gre.set_value(context_group..".width",  total_width)
    else
      gre.set_value(context_group..".xoffset", 0)
      gre.set_value(context_group..".width",  width)
    end
  end
  gre.set_value(context_group .. ".highlight_control.grd_hidden", highlight_active and 0 or 1)
  gre.set_value(context_group .. ".highlight_control.width", cursor_pos - 1)
  gre.set_value(context_group .. ".type_cursor_control.grd_width", cursor_pos)
  resetCursorControl()
  return cursor_pos
end

---
--- @brief  Calculate cursor position.
--- @param  gre#context mapargs
--- @return position of cursor
local function CalcPositionCursor(mapargs)
  local x = mapargs.context_event_data.x -
            gre.get_layer_attrs(mapargs.context_layer, "x").x -
            (gre.get_value(mapargs.context_group..".grd_x") or 0) - 
            gre.get_value(mapargs.context_control..".grd_x") -
            (gre.get_value(mapargs.context_control..".xoffset") or 0) 
  local width = gre.get_value(mapargs.context_control..".grd_width")

  local value = gre.get_value(mapargs.context_screen .. ".value")
  if gre.get_value(mapargs.context_group .. ".hide_control.grd_hidden") == 0 then
    value = string.rep("*", value:len())
  end
  
  local xoffset = gre.get_value(mapargs.context_group..".xoffset") or 0
  x = x - xoffset

  local font = gre.get_value("font_bold")
  local size = gre.get_value(mapargs.context_control .. ".font_size")
  local w1 = 0
  virtualKeyboard:SetInsertPos(nil)  
  for i=1,#value do
    local w2 = gre.get_string_size(font, size, value:sub(1, i)).width
    if w2 >= -xoffset then
      if x < (w1 + w2) / 2 then
        virtualKeyboard:SetInsertPos(i - 1)  
       if -w1 > xoffset then
         gre.set_value(mapargs.context_group..".xoffset", -w1)
       end
        break
      end
    end
    w1 = w2
  end

  return SetCursorPosition(mapargs.context_group, mapargs.context_control, value)
end


---
--- @brief  Callback to start cursor position movement.
--- @param  gre#context mapargs
function CBStartPositionCursor(mapargs)
  highlight_active = nil
  cursor_moving = mapargs.context_control
  local x = CalcPositionCursor(mapargs)
  local control = mapargs.context_group..".cursor_motion"
  gre.set_value(control..".grd_hidden", 0)
  gre.set_value(control..".xoffset", x + gre.get_value(control..".xstart"))
end

---
--- @brief  Callback when the cursor position is moving.
--- @param  gre#context mapargs
function CBProgressPositionCursor(mapargs)
  local control = mapargs.context_control
  if cursor_moving ~= nil then
    mapargs.context_control = cursor_moving
    local x = CalcPositionCursor(mapargs)
    gre.set_value(control..".xoffset", x + gre.get_value(control..".xstart"))
  end
end

---
--- @brief  Callback to end cursor position movement.
--- @param  gre#context mapargs
function CBEndPositionCursor(mapargs)
  if cursor_moving ~= nil then
    gre.set_value(mapargs.context_control..".grd_hidden", 1)
    mapargs.context_control = cursor_moving
    CalcPositionCursor(mapargs)
    cursor_moving = nil
    if gre.get_value(mapargs.context_layer..".keyboard_group.grd_hidden") == 1 then
      gre.set_value(
        mapargs.context_layer..".keyboard_group.grd_hidden", 0,
        mapargs.context_group..".hide_keyboard_control.grd_hidden", 0,
        mapargs.context_layer..".heading_group.show_code_entry.grd_hidden", 1)
    end
  end
end

---
--- @brief  Process submit key press. 
--- @param  gre#context mapargs
---
function CBTextEntrySubmitPress(mapargs)
  if mapargs.context_event_data == nil then
    mapargs.context_event_data = {}
  end
  mapargs.context_event_data.code = 10
  mapargs.value = gre.get_value(mapargs.context_screen .. ".value")
  CBTextEntryKeyPress(mapargs)
end

---
--- @brief  Stop auto accept entry and clear the context 
---
function CBStopAutoAcceptEntry()
  if auto_accept_entry.timer_id ~= nil then 
    gre.timer_clear_timeout(auto_accept_entry.timer_id) 
  end
  auto_accept_entry = {}
end

---
--- @brief  Timeout callback for auto-accept entry 
---
local function CBAutoAcceptEntry()
  -- Check if context present
  if next(auto_accept_entry) then
    -- populate mapargs with context required to accept entry
    local mapargs = {
      ["value"] = auto_accept_entry.value,
      ["context_screen"] = auto_accept_entry.context_screen,
      ["context_layer"] = auto_accept_entry.context_layer,
      ["context_event_data"] = {
        ["code"] = 10
      }
    }
    auto_accept_entry = {}
    CBTextEntryKeyPress(mapargs)
  end
end

---
--- @brief  Process key presses. Controls the operation of key pad in response 
---         to the current text value.
--- @param  gre#context mapargs
---
function CBTextEntryKeyPress(mapargs)
  mapargs.value = mapargs.value or ""

  local clear_error_info = true

  local callback = _G[gre.get_value(mapargs.context_screen .. ".process_hot_key")]
  if type(callback) == "function" then
    callback(mapargs)
  end

  -- Perform the current value for a validity check.
  local validated = true
  local callback = _G[gre.get_value(mapargs.context_screen .. ".submit_check")]
  if type(callback) == "function" and not callback(mapargs) then
    validated = false
    clear_error_info = false
  end

  -- Clear the auto accept timer and context
  CBStopAutoAcceptEntry()

  -- Check current value is valid
  if validated then
    if mapargs.context_event_data.code == 10 then
      local callback = _G[gre.get_value(mapargs.context_screen .. ".submit_value")]
      if type(callback) == "function" then
        callback(mapargs)
      end
      mapargs.value = ""
      gre.set_value(mapargs.context_screen .. ".value", "")
      clear_error_info = false
    elseif mapargs.value ~= "" then      
      -- Check for auto-accept
      local timeout = gre.get_value(mapargs.context_screen..".auto_accept_timeout")
      if timeout ~= nil then
        -- Store the current context for the auto accept time-out callback and start timeout
        auto_accept_entry = {
          ["context_screen"] = mapargs.context_screen,
          ["context_layer"] = mapargs.context_layer,
          ["value"] = mapargs.value,
          ["timer_id"] = gre.timer_set_timeout(CBAutoAcceptEntry, timeout) }
      end
    end
  end
  
  if highlight_active and mapargs.context_event_data.code ~= nil then
    highlight_active = nil
    mapargs.value = mapargs.context_event_data.code == 8 and "" or string.char(mapargs.context_event_data.code)
    gre.set_value(mapargs.context_screen .. ".value", mapargs.value)
  end

  local context_group = mapargs.context_layer .. ".text_entry_group"

  local hidden_value = string.rep("*", string.len(mapargs.value))
  gre.set_value(context_group .. ".hidden_control.text", hidden_value)

  if clear_error_info then
    gre.set_control_attrs(context_group .. ".text_entry_info_control", { hidden = 1 })
    gre.set_control_attrs(context_group .. ".error_icon", { hidden = 1 })
  end

  if mapargs.value == "" then
    gre.set_control_attrs(context_group .. ".hint_control", { hidden = 0 })
    gre.set_control_attrs(context_group .. ".clear_control", { hidden = 1 })
    SetCursorPosition(context_group, context_group..".value_control", "")
  else
    gre.set_control_attrs(context_group .. ".hint_control", { hidden = 1 })
    gre.set_control_attrs(context_group .. ".clear_control", { hidden = 0 })
    
    if gre.get_value(context_group .. ".hide_control.grd_hidden") == 0 then
      gre.set_control_attrs(context_group .. ".value_control", { hidden = 1 })
      gre.set_control_attrs(context_group .. ".hidden_control", { hidden = 0 })
      SetCursorPosition(context_group, context_group..".hidden_control", hidden_value)
    else
      gre.set_control_attrs(context_group .. ".value_control", { hidden = 0 })
      gre.set_control_attrs(context_group .. ".hidden_control", { hidden = 1 })
      SetCursorPosition(context_group, context_group..".value_control", mapargs.value)
    end
  end
end

---
--- @brief  Callback to show the current value.
--- @param  gre#context mapargs
---
function CBTextEntryShowValue(mapargs)
  gre.set_control_attrs(mapargs.context_group .. ".value_control", { hidden = 0 })
  gre.set_control_attrs(mapargs.context_group .. ".hidden_control", { hidden = 1 })
  local value = gre.get_value(mapargs.context_screen .. ".value")
  SetCursorPosition(mapargs.context_group, mapargs.context_group..".value_control", value)
end

---
--- @brief  Callback to hide the current value
--- @param  gre#context mapargs
---
function CBTextEntryHideValue(mapargs)
  gre.set_control_attrs(mapargs.context_group .. ".value_control", { hidden = 1 })
  gre.set_control_attrs(mapargs.context_group .. ".hidden_control", { hidden = 0 })
  
  local value = gre.get_value(mapargs.context_screen .. ".value")
  local hidden_value = string.rep("*", string.len(value))
  SetCursorPosition(mapargs.context_group, mapargs.context_group..".hidden_control", hidden_value)
end

---
--- @brief  Default callback for submit validity checking.
--- @param  gre#context mapargs
---
function CBTextEntrySubmitCheckDefault(mapargs)
  local context_group = mapargs.context_layer .. ".text_entry_group"
  if mapargs.value == "" then
    gre.set_control_attrs(context_group .. ".submit_control", { hidden = 1 })
    return false
  end
  gre.set_control_attrs(context_group .. ".submit_control", { hidden = 0 })
  return true
end

---
--- @brief  Default callback for submitting value.
--- @param  gre#context mapargs
---
function CBTextEntrySubmitValueDefault(mapargs)
  print (mapargs.value)
end

---
--- @brief  
--- @param  gre#context mapargs
---
function CBTextEntryClearValueRelease(mapargs)
  mapargs.value = ""
  mapargs.context_group = mapargs.context_layer .. ".text_entry_group"
  gre.set_value(mapargs.context_screen .. ".value", mapargs.value)
  virtualKeyboard:SetInsertPos(nil)
  CBTextEntryKeyPress(mapargs)
end

---
--- @brief  Resets the key pad to defaults.
--- @param  mapargs.layer - keypad layer being reset.
--- @param  mapargs.eye_control_enabled - "1" if eye control show be available and the text is normally hidden.
--- @param  mapargs.hide_keyboard - "1" if the keyboard is normally hidden.
--- @param  gre#context mapargs
---
function CBResetTextEntry(mapargs) 
  -- Find the data entry layer
  local layers = GetChildObjectList(mapargs.context_screen)
  for i = #layers,1,-1 do
    mapargs.layer = tostring(layers[i])
    if mapargs.layer:sub(-12) == "_entry_layer" then
      mapargs.layer = mapargs.layer:sub(#mapargs.context_screen + 2)
      break
    end
  end

  mapargs.context_layer = mapargs.layer
  local group = mapargs.context_layer..".text_entry_group"
  mapargs.value = gre.get_value(mapargs.context_screen..".start_value") or ""
  highlight_active = mapargs.value ~= ""

  -- Allow option to be either from the parameter or the model.
  mapargs.hide_keyboard = mapargs.hide_keyboard or gre.get_value(mapargs.context_screen..".hide_keyboard")

  gre.set_data({
    [mapargs.context_screen..".value"] = mapargs.value,
    [group..".hidden_control.text"] = "",
    [group..".cursor_motion.grd_hidden"] = 1,
    [group..".hint_control.grd_hidden"] = 0,
    [group..".submit_control.grd_hidden"] = 0,
    [group..".type_cursor_control.grd_width"] = 1,
    [group..".text_entry_info_control.grd_hidden"] = 1,
    [group..".hidden_control.grd_hidden"] = 1,
    [group..".value_control.grd_hidden"] = 1,
    [group..".highlight_control.grd_hidden"] = 1,
    [group..".clear_control.grd_hidden"] = 1,
    [group..".error_icon.grd_hidden"] = 1,
    [group..".show_control.grd_hidden"] = 1,
    [group..".hide_control.grd_hidden"] = (mapargs.eye_control_enabled == "1") and 0 or 1,
    [mapargs.context_layer..".keyboard_group.grd_hidden"] = (mapargs.hide_keyboard == "1") and 1 or 0,
    [group..".submit_control.grd_hidden"] = (mapargs.hide_keyboard == "1") and 1 or 0,
    [group..".hide_keyboard_control.grd_hidden"] = 1,
    [mapargs.context_layer..".heading_group.show_code_entry.grd_hidden"] = gre.get_value(mapargs.context_screen..".code_entry_screen") and 0 or 1
  })

  local value = gre.get_value(mapargs.context_screen..".banner_heading")
  if mapargs.hide_keyboard == "1" and value and value ~= "" then
    gre.set_value(mapargs.context_layer..".heading_group.grd_hidden", 0,
                  group..".grd_hidden", 1)
  else
    gre.set_value(mapargs.context_layer..".heading_group.grd_hidden", 1,
                  group..".grd_hidden", 0)
  end

  local extra_keys = gre.get_value(mapargs.context_screen .. ".extra_keys")
  if extra_keys == nil or extra_keys == "" then
    gre.set_value(group..".background.grd_width", 563)
  elseif string.len(extra_keys) <= 4 then
    gre.set_value(group..".background.grd_width", 443)
  else
    gre.set_value(group..".background.grd_width", 347)
  end

  resetCursorControl(group..".type_cursor_control")
  
  PopulateNumericKeypadExtraKeys(extra_keys)
  VKI_Init(mapargs)

  if mapargs.context_event_data == nil then
    mapargs.context_event_data = {}
  end
  CBTextEntryKeyPress(mapargs)
  
  -- Update the warning symbol position against the status information text.
  local font = gre.get_value("font_normal")
  UpdateLeftImagePositionText(mapargs.context_screen..".status_info", mapargs.context_layer .. ".text_entry_group.text_entry_info_control", font, 40)
  
  if mapargs.hide_keyboard == "1" then
    gre.set_control_attrs(group .. ".submit_control", { hidden = 1 })
  else
    local callback = _G[gre.get_value(mapargs.context_screen .. ".submit_check")]
    if type(callback) == "function" then
      mapargs.value = ""
      callback(mapargs)
    end
  end
end

---
--- @brief  Show banner and hide extra text group if banner present.
--- @param  gre#context mapargs
---
function CBHideKeyboardGroup(mapargs)
  local value = gre.get_value(mapargs.context_screen..".banner_heading")
  if value and value ~= "" then
    gre.set_value(mapargs.context_layer..".heading_group.grd_hidden", 0,
                  mapargs.context_layer..".text_entry_group.grd_hidden", 1)
  end
  gre.set_value(mapargs.context_layer..".heading_group.show_code_entry.grd_hidden", gre.get_value(mapargs.context_screen..".code_entry_screen") and 0 or 1)
end

---
--- @brief  Gets the IP Address type
--- @param  value - IP address to check.
--  @return nil, "string", "IPv4", "IPv6", 
---
function GetIPType(value)
  if type(value) ~= "string" then 
    return nil
  end

  -- check for format 1.11.111.111 for ipv4
  local chunks = { value:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") }
  if (#chunks == 4) then
    for _,v in pairs(chunks) do
      if tonumber(v) > 255 then 
        return "string"
      end
    end
    return "IPv4"
  end

  -- check for ipv6 format, should be max 8 'chunks' of numbers/letters
  local addr = value:match("^([a-fA-F0-9:]+)$")
  if addr ~= nil and #addr > 1 then
    -- address part
    local nc, dc = 0, false      -- chunk count, double colon
    for chunk, colons in addr:gmatch("([^:]*)(:*)") do
      -- max allowed chunks
      if nc > (dc and 7 or 8) then 
        return "string"
      end
      if #chunk > 0 and tonumber(chunk, 16) > 65535 then
        return "string"
      end
      if #colons > 0 then
        -- max consecutive colons allowed: 2
        if #colons > 2 then 
          return "string" 
        end
        -- double colon shall appear only once
        if #colons == 2 and dc == true then 
          return "string" 
        end
        if #colons == 2 and dc == false then 
          dc = true
        end
      end
      nc = nc + 1      
    end
    return "IPv6"
  end

  return "string"
end

---
--- @brief  Callback to validate an IPv4 address for numeric entry screenm
--- @param  gre#context mapargs
---
function CBValidationIPv4(mapargs)
  if GetIPType(mapargs.value) == "IPv4" then
    gre.set_control_attrs("numeric_entry_layer.text_entry_group.error_icon", { hidden = 1 })
    return true
  end
  gre.set_control_attrs("numeric_entry_layer.text_entry_group.error_icon", { hidden = 0 })
  return false
end

---
--- @brief  Callback to validate an IPv4 address or blank for numeric entry screenm
--- @param  gre#context mapargs
---
function CBValidationIPv4OrBlank(mapargs)
  if mapargs.value == "" then 
    return true 
  end
  return CBValidationIPv4(mapargs)
end

---
--- @brief  Callback to enter blank for numeric entry screen
--- @param  gre#context mapargs
---
function CBNumericEntryClearDataItem(mapargs) 
  local callback = _G[gre.get_value(mapargs.context_screen .. ".clear_action")]
  if type(callback) == "function" then
    callback(mapargs)
  else
    if mapargs.context_event_data == nil then mapargs.context_event_data = {} end 
    mapargs.context_event_data.code = 10
    CBTextEntryKeyPress(mapargs)
  end
  TransitionBackScreen(mapargs)
end


-- EOF --
