local dmf = get_mod("DMF")

local _custom_view_persistent_data = dmf:persistent_table("custom_view_data")

local _custom_views_data = {}

local _ingame_ui
local _loaded_views = {}

local _key_watch = false

local ERRORS = {
  THROWABLE = {
    -- inject_view:
    view_already_exists = "view with name '%s' already persists in original game.",
    transition_already_exists = "transition with name '%s' already persists in original game.",
    view_initializing_failed = "view initialization failed due to error during 'init_view_function' execution.",
    -- validate_view_data:
    view_name_wrong_type = "'view_name' must be a string, not %s.",
    view_transitions_wrong_type = "'view_transitions' must be a table, not %s.",
    view_settings_wrong_type = "'view_settings' must be a table, not %s.",
    transition_wrong_type = "all transitions inside 'view_transitions' must be functions, but '%s' transition is %s.",
    transition_name_taken = "transition name '%s' is already used by '%s' mod for '%s' view.",
    init_view_function_wrong_type = "'view_settings.init_view_function' must be a function, not %s.",
    active_wrong_type = "'view_settings.active' must be a table, not %s.",
    active_missing_element = "'view_settings.active' must contain 2 elements: 'inn' and 'ingame'.",
    active_element_wrong_name = "the only allowed names for 'view_settings.active' elements are 'inn' and 'ingame'; " ..
                                 "you can't name your element '%s'.",
    active_element_wrong_type = "'view_settings.active.%s' must be boolean, not %s.",
    blocked_transitions_wrong_type = "'view_settings.blocked_transitions' (optional) must be a table, not %s.",
    blocked_transitions_missing_element = "'view_settings.blocked_transitions' must contain 2 table elements: " ..
                                           "'inn' and 'ingame'.",
    blocked_transitions_element_wrong_name = "the only allowed names for 'view_settings.active' elements are " ..
                                              "'inn' and 'ingame'; you can't name your element '%s'.",
    blocked_transitions_element_wrong_type = "'view_settings.blocked_transitions.%s' must be a table, not %s.",
    blocked_transition_invalid = "you can't put transition '%s' into 'view_settings.blocked_transitions.%s', " ..
                                  "because it's not listed in 'view_transitions'.",
    blocked_transition_wrong_value = "invalid value for 'view_settings.blocked_transitions.%s.%s'; must be 'true'."
  },
  REGULAR = {
    view_not_registered = "[Custom Views] Toggling view with keybind: view '%s' wasn't registered for this mod.",
    transition_not_registered = "[Custom Views] Toggling view with keybind: transition '%s' wasn't registered for " ..
                                 "'%s' view."
  },
  PREFIX = {
    view_initializing = "[Custom Views] Calling 'init_view_function'",
    view_destroying = "[Custom Views] Destroying view '%s'",
    register_view_validation = "[Custom Views] (register_view) View data validating '%s'",
    register_view_injection = "[Custom Views] (register_view) View injection '%s'",
    ingameui_hook_injection = "[Custom Views] View injection '%s'",
    handle_transition_fade = "[Custom Views] (handle_transition) executing 'ingame_ui.transition_with_fade' for " ..
                              "transition '%s'",
    handle_transition_no_fade = "[Custom Views] (handle_transition) executing 'ingame_ui.handle_transition' for " ..
                                 "transition '%s'"
  }
}

-- #####################################################################################################################
-- ##### Local functions ###############################################################################################
-- #####################################################################################################################

local function is_view_active_for_current_level(view_name)
  -- @TODO: Add active setting per mechanism type
  return true
end


-- @THROWS_ERRORS
local function inject_view(view_name)
  if not is_view_active_for_current_level(view_name) then
    return
  end

  local view_settings = _custom_views_data[view_name].view_settings
  local mod           = _custom_views_data[view_name].mod

  local init_view_function = view_settings.init_view_function

  -- Check for collisions. @TODO: Check for collisions by mod
  --if _ingame_ui._view_list[view_name] then
  --  dmf.throw_error(ERRORS.THROWABLE.view_already_exists, view_name)
  --end
  --for transition_name, _ in pairs(transitions) do
  --  if _ingame_ui_transitions[transition_name] then
  --    dmf.throw_error(ERRORS.THROWABLE.transition_already_exists, transition_name)
  --  end
  --end

  -- Initialize and inject view.
  local success = dmf.safe_call(mod, ERRORS.PREFIX.view_initializing, init_view_function,
                                                                             view_settings, {})
  if success then
    _ingame_ui._view_list[view_name] = view_settings
  else
    dmf.throw_error(ERRORS.THROWABLE.view_initializing_failed)
  end

  -- Inject view transitions.
  --for transition_name, transition_function in pairs(transitions) do
  --  _ingame_ui_transitions[transition_name] = transition_function
  --end

  -- Inject view blocked transitions.
  --for blocked_transition_name, _ in pairs(blocked_transitions) do
  --  _ingame_ui.blocked_transitions[blocked_transition_name] = true
  --end
end


local function remove_injected_views(on_reload)
  -- These elements should be removed only on_reload, because, otherwise, they will be deleted automatically.
  if on_reload then

    for view_name, _ in pairs(_custom_views_data) do

      -- Close the view if active
      if Managers.ui:view_active(view_name) then
        
        local force_close = true
        Managers.ui:close_view(view_name, force_close)
      end

      -- Remove the injected view
      _ingame_ui._view_list[view_name] = nil
    end
  end

  --for _, view_data in pairs(_custom_views_data) do
    -- Remove injected transitions.
  --  for transition_name, _ in pairs(view_data.view_transitions) do
  --    _ingame_ui_transitions[transition_name] = nil
  --  end

    -- Remove blocked transitions
  --  local blocked_transitions = view_data.view_settings.blocked_transitions[_ingame_ui.is_in_inn and "inn" or "ingame"]
  --  for blocked_transition_name, _ in pairs(blocked_transitions) do
  --    _ingame_ui.blocked_transitions[blocked_transition_name] = nil
  --  end
  --end
end


-- @THROWS_ERRORS
local function validate_view_data(view_data)
  -- Basic checks.
  if type(view_data.view_name) ~= "string" then
    dmf.throw_error(ERRORS.THROWABLE.view_name_wrong_type, type(view_data.view_name))
  end
  if type(view_data.view_transitions) ~= "table" then
    dmf.throw_error(ERRORS.THROWABLE.view_transitions_wrong_type, type(view_data.view_transitions))
  end
  if type(view_data.view_settings) ~= "table" then
    dmf.throw_error(ERRORS.THROWABLE.view_settings_wrong_type, type(view_data.view_settings))
  end

  -- VIEW TRANSITIONS

  local view_transitions = view_data.view_transitions
  for transition_name, transition_function in pairs(view_transitions) do
    if type(transition_function) ~= "function" then
      dmf.throw_error(ERRORS.THROWABLE.transition_wrong_type, transition_name, type(transition_function))
    end
    for another_view_name, another_view_data in pairs(_custom_views_data) do
      for another_transition_name, _ in pairs(another_view_data.view_transitions) do
        if transition_name == another_transition_name then
          dmf.throw_error(ERRORS.THROWABLE.transition_name_taken, transition_name, another_view_data.mod:get_name(),
                                                                   another_view_name)
        end
      end
    end
  end

  -- VIEW SETTINGS

  local view_settings = view_data.view_settings

  -- Use default values for optional fields if they are not defined.
  view_settings.blocked_transitions = view_settings.blocked_transitions or {inn = {}, ingame = {}}

  -- Verify everything.
  if type(view_settings.init_view_function) ~= "function" then
    dmf.throw_error(ERRORS.THROWABLE.init_view_function_wrong_type, type(view_settings.init_view_function))
  end

  -- Verify active if present
  local active = view_settings.active
  if active then
    if type(active) ~= "table" then
      dmf.throw_error(ERRORS.THROWABLE.active_wrong_type, type(active))
    end
    if active.inn == nil or active.ingame == nil then
      dmf.throw_error(ERRORS.THROWABLE.active_missing_element)
    end
    for level_name, value in pairs(active) do
      if level_name ~= "inn" and level_name ~= "ingame" then
        dmf.throw_error(ERRORS.THROWABLE.active_element_wrong_name, level_name)
      end
      if type(value) ~= "boolean" then
        dmf.throw_error(ERRORS.THROWABLE.active_element_wrong_type, level_name, type(value))
      end
    end
  end

  -- Verify blocked transitions if present
  local blocked_transitions = view_settings.blocked_transitions
  if blocked_transitions then
    if type(blocked_transitions) ~= "table" then
      dmf.throw_error(ERRORS.THROWABLE.blocked_transitions_wrong_type, type(blocked_transitions))
    end
    if not blocked_transitions.inn or not blocked_transitions.ingame then
      dmf.throw_error(ERRORS.THROWABLE.blocked_transitions_missing_element)
    end
    for level_name, level_blocked_transitions in pairs(blocked_transitions) do
      if level_name ~= "inn" and level_name ~= "ingame" then
        dmf.throw_error(ERRORS.THROWABLE.blocked_transitions_element_wrong_name, level_name)
      end
      if type(level_blocked_transitions) ~= "table" then
        dmf.throw_error(ERRORS.THROWABLE.blocked_transitions_element_wrong_type, level_name,
                                                                                  type(level_blocked_transitions))
      end
      for transition_name, value in pairs(level_blocked_transitions) do
        if not view_transitions[transition_name] then
          dmf.throw_error(ERRORS.THROWABLE.blocked_transition_invalid, transition_name, level_name)
        end
        if value ~= true then
          dmf.throw_error(ERRORS.THROWABLE.blocked_transition_wrong_value, level_name, transition_name)
        end
      end
    end
  end
end


-- Checks:
--   * View registered
--   * View is loaded/loadable
--   * View is not already active
--   * View is not in the middle of closing
local function can_open_view(view_name)

  if _ingame_ui then
    if
      _custom_views_data[view_name]                   and
      _custom_view_persistent_data.loader_initialized and
      not Managers.ui:view_active(view_name)          and
      not Managers.ui:is_view_closing(view_name)
    then
      return true
    end
  end

  return false
end

-- #####################################################################################################################
-- ##### DMFMod ########################################################################################################
-- #####################################################################################################################

--[[
  Wraps ingame_ui transition handling calls in a lot of safety checks. Returns 'true', if call is successful.
  * transition_name    [string]  : name of a transition that should be perfomed
  * ignore_active_menu [boolean] : if 'ingame_ui.menu_active' should be ignored
  * fade               [boolean] : if transition should be performed with fade
  * transition_params  [anything]: parameter, which will be passed to callable transition function, 'on_exit' method of
                                   the old view and 'on_enter' method of the new view
--]]
function DMFMod:handle_transition()
  return true
end


--[[
  Opens a file with a view data and validates it. Registers the view and returns 'true' if everything is correct.
  * view_data_file_path [string]: path to a file returning view_data table
--]]
function DMFMod:register_view(view_data)
  if dmf.check_wrong_argument_type(self, "register_view", "view_data", view_data, "table") then
    return
  end

  view_data = table.clone(view_data)

  local view_name = view_data.view_name
  view_data.view_settings.name = view_name

  if view_data.view_settings.close_on_hotkey_pressed == nil then
    view_data.view_settings.close_on_hotkey_pressed = true
  end

  if not dmf.safe_call_nrc(self, {ERRORS.PREFIX.register_view_validation, view_name}, validate_view_data,
                                                                                                         view_data) then
    return
  end

  _custom_views_data[view_name] = {
    mod              = self,
    view_settings    = view_data.view_settings,
    view_transitions = view_data.view_transitions,
    view_options     = view_data.view_options,
  }

  if _ingame_ui then
    if not dmf.safe_call_nrc(self, {ERRORS.PREFIX.register_view_injection, view_name}, inject_view, view_name) then
      _custom_views_data[view_data.view_name] = nil
    end
  end

  return true
end

-- #####################################################################################################################
-- ##### Hooks #########################################################################################################
-- #####################################################################################################################


-- Track the creation of the view loader
dmf:hook_safe(CLASS.ViewLoader, "init", function()
  _custom_view_persistent_data.loader_initialized = true
end)


-- Track the loading of views, set the loader flag if class selection is reached
dmf:hook_safe(CLASS.UIManager, "load_view", function(self, view_name)
  if view_name == "class_selection_view" then
    _custom_view_persistent_data.loader_initialized = true
  end
  _loaded_views[view_name] = true
end)

-- Track the unloading of views
dmf:hook_safe(CLASS.UIManager, "unload_view", function(self, view_name)
  _loaded_views[view_name] = nil
end)


-- Store the view handler for later use and inject views
dmf:hook_safe(CLASS.UIViewHandler, "init", function(self)
  _ingame_ui = self
  for view_name, _ in pairs(_custom_views_data) do
    if not dmf.safe_call_nrc(dmf, {ERRORS.PREFIX.ingameui_hook_injection, view_name}, inject_view, view_name) then
      _custom_views_data[view_name] = nil
    end
  end
end)

-- Track the start of key watches
dmf:hook_safe(CLASS.InputManager, "start_key_watch", function(self)
  _key_watch = true
end)

-- Track the end of key watches
dmf:hook_safe(CLASS.InputManager, "stop_key_watch", function(self)
  _key_watch = false
end)

-- #####################################################################################################################
-- ##### DMF internal functions and variables ##########################################################################
-- #####################################################################################################################

function dmf.remove_custom_views()
  if _ingame_ui then
    remove_injected_views(true)
  end
end


-- Opens/closes a view if all conditions are met. Since keybinds module can't do UI-related checks, all the cheks are
-- done in this function. This function is called every time some view-toggling keybind is pressed.
function dmf.keybind_toggle_view(mod, view_name, keybind_transition_data, can_perform_action, is_keybind_pressed)

  if _ingame_ui then

    -- Check that the view is registered
    local view_data = _custom_views_data[view_name]
    if not view_data or (view_data.mod ~= mod) then
      mod:error(ERRORS.REGULAR.view_not_registered, view_name)
      return
    end

    -- If the view is open, this is a toggle close
    if Managers.ui:view_active(view_name) then

      -- Don't close the view if it's already closing or we have an active key watch
      if not Managers.ui:is_view_closing(view_name) and not _key_watch then
        local force_close = true
        Managers.ui:close_view(view_name, force_close)
      end

    -- Otherwise, this is a toggle open
    elseif can_perform_action and is_keybind_pressed then
      
      local validation_function = view_data.view_settings.validation_function
      local can_open_and_validated = can_open_view(view_name) and (not validation_function or validation_function())

      -- Checks for inactive, not closing, no other open view, loaded/loadable, and validation
      if not can_open_and_validated then
        return
      end

      local view_options = view_data.view_options
      local close_all = view_options and view_options.close_all or false
      local close_previous = view_options and view_options.close_previous or false
      local close_transition_time = view_options and view_options.close_transition_time or nil
      local transition_time = view_options and view_options.transition_time or nil

      local view_context = {}
      local use_transition_ui = view_data.view_settings.use_transition_ui
      local no_transition_ui = use_transition_ui == false
      local view_settings_override = no_transition_ui and {
        use_transition_ui = false
      }

      -- Open the view with default parameters
      Managers.ui:open_view(view_name, transition_time, close_previous,
                                      close_all, close_transition_time, view_context, view_settings_override)

    end
  end
end

-- #####################################################################################################################
-- ##### Script ########################################################################################################
-- #####################################################################################################################

-- If DMF is reloaded mid-game, get ingame_ui.
_ingame_ui = Managers.ui and Managers.ui._view_handler
