--[[--
An InputContainer is a WidgetContainer that handles user input events including multi touches and key presses.

See @{InputContainer:registerTouchZones} for examples of how to listen for multi touch input.

This example illustrates how to listen for a key press input event via the `key_events` hashmap:

    key_events = {
        PanBy20 = {
            { "Shift", Input.group.Cursor }, -- Shift + (any member of) Cursor
            event = "Pan",
            args = 20,
            is_inactive = true,
        },
        PanNormal = {
            { Input.group.Cursor }, -- Any member of Cursor (itself an array)
            event = "Pan",
            args = 10,
        },
        Exit = {
            { "Alt", "F4" }, -- Alt + F4
            { "Ctrl", "Q" }, -- Ctrl + Q
        },
        Home = {
            { { "Home", "H" } }, -- Any of Home or H (note the extra nesting!)
        },
        End = {
            { "End" }, -- NOTE: For a *single* key, we can forgo the nesting (c.f., match @ device/key).
        },
    },

It is recommended to reference configurable sequences from another table
and to store that table as a configuration setting.

]]

local DepGraph = require("depgraph")
local Device = require("device")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local Screen = Device.screen
local _ = require("gettext")

local InputContainer = WidgetContainer:extend{
    vertical_align = "top",
}

function InputContainer:_init()
    -- These should be instance-specific
    if not self.key_events then
        self.key_events = {}
    end
    if not self.ges_events then
        self.ges_events = {}
    end
    self.touch_zone_dg = nil
    self._zones = {}
    self._ordered_touch_zones = {}
end

function InputContainer:paintTo(bb, x, y)
    if self[1] == nil then
        return
    end
    if self.skip_paint then
        return
    end

    if not self.dimen then
        local content_size = self[1]:getSize()
        self.dimen = Geom:new{
            x = x, y = y,
            w = content_size.w, h = content_size.h,
        }
    else
        self.dimen.x = x
        self.dimen.y = y
    end
    if self.vertical_align == "center" then
        local content_size = self[1]:getSize()
        self[1]:paintTo(bb, x, y + math.floor((self.dimen.h - content_size.h)/2))
    else
        self[1]:paintTo(bb, x, y)
    end
end

--[[--

Register touch zones into this InputContainer.

See gesturedetector for a list of supported gestures.

NOTE: You are responsible for calling self:@{updateTouchZonesOnScreenResize} with the new
screen dimensions whenever the screen is rotated or resized.

@tparam table zones list of touch zones to register

@usage
local InputContainer = require("ui/widget/container/inputcontainer")
local test_widget = InputContainer:new{}
test_widget:registerTouchZones({
    {
        id = "foo_tap",
        ges = "tap",
        -- This binds the handler to the full screen
        screen_zone = {
            ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
        },
        handler = function(ges)
            print('User tapped on screen!')
            return true
        end
    },
    {
        id = "foo_swipe",
        ges = "swipe",
        -- This binds the handler to bottom half of the screen
        screen_zone = {
            ratio_x = 0, ratio_y = 0.5, ratio_w = 1, ratio_h = 0.5,
        },
        handler = function(ges)
            print("User swiped at the bottom with direction:", ges.direction)
            return true
        end
    },
})
require("ui/uimanager"):show(test_widget)

]]
function InputContainer:registerTouchZones(zones)
    local screen_width, screen_height = Screen:getWidth(), Screen:getHeight()
    if not self.touch_zone_dg then self.touch_zone_dg = DepGraph:new{} end
    for _, zone in ipairs(zones) do
        -- override touch zone with the same id to support reregistration
        if self._zones[zone.id] then
            self.touch_zone_dg:removeNode(zone.id)
        end
        self._zones[zone.id] = {
            def = zone,
            handler = zone.handler,
            gs_range = GestureRange:new{
                ges = zone.ges,
                rate = zone.rate,
                range = Geom:new{
                    x = screen_width * zone.screen_zone.ratio_x,
                    y = screen_height * zone.screen_zone.ratio_y,
                    w = screen_width * zone.screen_zone.ratio_w,
                    h = screen_height * zone.screen_zone.ratio_h,
                },
            },
        }
        self.touch_zone_dg:addNode(zone.id)
        -- print("added "..zone.id)
        if zone.overrides then
            for _, override_zone_id in ipairs(zone.overrides) do
                -- print("  override "..override_zone_id)
                self.touch_zone_dg:addNodeDep(override_zone_id, zone.id)
            end
        end
    end
    -- print("ordering:")
    self._ordered_touch_zones = {}
    for _, zone_id in ipairs(self.touch_zone_dg:serialize()) do
        table.insert(self._ordered_touch_zones, self._zones[zone_id])
        -- print("  "..zone_id)
    end
end

function InputContainer:unRegisterTouchZones(zones)
    if self.touch_zone_dg then
        for i, zone in ipairs(zones) do
            if self._zones[zone.id] then
                self.touch_zone_dg:removeNode(zone.id)
                if zone.overrides then
                    for _, override_zone_id in ipairs(zone.overrides) do
                        --self.touch_zone_dg:removeNodeDep(override_zone_id, zone.id)
                        self.touch_zone_dg:removeNodeDep(override_zone_id, zone.id)
                    end
                end
                for _, id in ipairs(self._ordered_touch_zones) do
                    if id.def.id == zone.id then
                        table.remove(self._ordered_touch_zones, i)
                        break
                    end
                end
            end
        end
        self._ordered_touch_zones = {}
        if self.touch_zone_dg then
            for _, zone_id in ipairs(self.touch_zone_dg:serialize()) do
                table.insert(self._ordered_touch_zones, self._zones[zone_id])
            end
        end
    end
end

function InputContainer:checkRegisterTouchZone(id)
    if self.touch_zone_dg then
        return self.touch_zone_dg:checkNode(id)
    else
        return false
    end
end

--[[--
Updates touch zones based on new screen dimensions.

@tparam ui.geometry.Geom new_screen_dimen new screen dimensions
]]
function InputContainer:updateTouchZonesOnScreenResize(new_screen_dimen)
    for _, tzone in ipairs(self._ordered_touch_zones) do
        local range = tzone.gs_range.range
        range.x = new_screen_dimen.w * tzone.def.screen_zone.ratio_x
        range.y = new_screen_dimen.h * tzone.def.screen_zone.ratio_y
        range.w = new_screen_dimen.w * tzone.def.screen_zone.ratio_w
        range.h = new_screen_dimen.h * tzone.def.screen_zone.ratio_h
    end
end

--[[
Handles keypresses and checks if they lead to a command.
If this is the case, we retransmit another event within ourselves.
--]]
function InputContainer:onKeyPress(key)
    for name, seq in pairs(self.key_events) do
        if not seq.is_inactive then
            for _, oneseq in ipairs(seq) do
                -- NOTE: key is a device/key object, this isn't string.match!
                if key:match(oneseq) then
                    local eventname = seq.event or name
                    return self:handleEvent(Event:new(eventname, seq.args, key))
                end
            end
        end
    end
end

-- NOTE: Currently a verbatim copy of onKeyPress ;).
function InputContainer:onKeyRepeat(key)
    for name, seq in pairs(self.key_events) do
        if not seq.is_inactive then
            for _, oneseq in ipairs(seq) do
                if key:match(oneseq) then
                    local eventname = seq.event or name
                    return self:handleEvent(Event:new(eventname, seq.args, key))
                end
            end
        end
    end
end

function InputContainer:onGesture(ev)
    for _, tzone in ipairs(self._ordered_touch_zones) do
        if tzone.gs_range:match(ev) and tzone.handler(ev) then
            return true
        end
    end
    for name, gsseq in pairs(self.ges_events) do
        for _, gs_range in ipairs(gsseq) do
            if gs_range:match(ev) then
                local eventname = gsseq.event or name
                if self:handleEvent(Event:new(eventname, gsseq.args, ev)) then
                    return true
                end
            end
        end
    end
    if self.stop_events_propagation then
        return true
    end
end

-- Will be overloaded by the Gestures plugin, if enabled, for use in _onGestureFiltered
function InputContainer:_isGestureAlwaysActive(ges, multiswipe_directions)
    -- If the plugin isn't enabled, IgnoreTouchInput can still be emitted by Dispatcher (e.g., via Profile or QuickMenu).
    -- Regardless of that, we still want to block all gestures anyway, as our own onResume handler will ensure
    -- that the standard onGesture handler is restored on the next resume cycle,
    -- allowing one to restore input handling automatically.
    return false
end
InputContainer.isGestureAlwaysActive = InputContainer._isGestureAlwaysActive

-- Filtered variant that only lets specific touch zones marked as "always active" through.
-- (This is used by the "toggle_touch_input" Dispatcher action).
function InputContainer:_onGestureFiltered(ev)
    for _, tzone in ipairs(self._ordered_touch_zones) do
        if self:isGestureAlwaysActive(tzone.def.id, ev.multiswipe_directions) and tzone.gs_range:match(ev) and tzone.handler(ev) then
            return true
        end
    end
    -- No ges_events at all, although if the need ever arises, we could also support an "always active" marker for those ;).
    if self.stop_events_propagation then
        return true
    end
end

-- NOTE: Monkey-patching InputContainer.onGesture allows us to effectively disable touch input,
--       because barely any InputContainer subclasses implement onGesture, meaning they all inherit this one,
--       making this specific method in this specific widget the only piece of code that handles the Gesture
--       Events sent by GestureDetector.
--       We would need to be slightly more creative if subclassed widgets did overload it in in any meaningful way[1].
--       (i.e., use a broadcast Event, don't stop its propagation, and swap self.onGesture in every instance
--       while still only swapping Input.onGesture once...).
--
--       [1] The most common implementation you'll see is a NOP for ReaderUI modules that defer gesture handling to ReaderUI.
--           Notification also implements a simple one to dismiss notifications on any user input,
--           which is something that doesn't impede our goal, which is why we don't need to deal with it.
function InputContainer:setIgnoreTouchInput(state)
    logger.dbg("InputContainer:setIgnoreTouchInput", state)
    if state == true then
        -- Replace the onGesture handler w/ the minimal one if that's not already the case
        if not InputContainer._onGesture then
            InputContainer._onGesture = InputContainer.onGesture
            InputContainer.onGesture = InputContainer._onGestureFiltered
            -- Notify UIManager so it knows what to do if a random popup shows up
            UIManager._input_gestures_disabled = true
            logger.dbg("Disabled InputContainer gesture handler")

            -- Notify our caller that the state changed
            return true
        end
    elseif state == false then
        -- Restore the proper onGesture handler if we disabled it
        if InputContainer._onGesture then
            InputContainer.onGesture = InputContainer._onGesture
            InputContainer._onGesture = nil
            UIManager._input_gestures_disabled = false
            logger.dbg("Restored InputContainer gesture handler")

            return true
        end
    end

    -- We did not actually change the state
    return false
end

-- And the matching Event handler
function InputContainer:onIgnoreTouchInput(toggle)
    local Notification = require("ui/widget/notification")
    if toggle == true then
        if self:setIgnoreTouchInput(true) then
            Notification:notify(_("Disabled touch input"))
        end
    elseif toggle == false then
        if self:setIgnoreTouchInput(false) then
            Notification:notify(_("Restored touch input"))
        end
    else
        -- Toggle the current state
        return self:onIgnoreTouchInput(not UIManager._input_gestures_disabled)
    end

    -- We only affect the base class, once is enough ;).
    return true
end

function InputContainer:onInput(input, ignore_first_hold_release)
    local InputDialog = require("ui/widget/inputdialog")
    self.input_dialog = InputDialog:new{
        title = input.title or "",
        input = input.input_func and input.input_func() or input.input,
        input_hint = input.hint_func and input.hint_func() or input.hint or "",
        input_type = input.type or "number",
        buttons = input.buttons or {
            {
                {
                    text = input.cancel_text or _("Cancel"),
                    id = "close",
                    callback = function()
                        self:closeInputDialog()
                    end,
                },
                {
                    text = input.ok_text or _("OK"),
                    is_enter_default = true,
                    callback = function()
                        if input.deny_blank_input and self.input_dialog:getInputText() == "" then return end
                        input.callback(self.input_dialog:getInputText())
                        self:closeInputDialog()
                    end,
                },
            },
        },
    }
    UIManager:show(self.input_dialog)
    self.input_dialog:onShowKeyboard(ignore_first_hold_release)
end

function InputContainer:closeInputDialog()
    UIManager:close(self.input_dialog)
end

function InputContainer:onPhysicalKeyboardDisconnected()
    -- Clear the key bindings if Device no longer has keys
    -- NOTE: hasKeys is the lowest common denominator of key-related Device caps,
    --       hasDPad/hasFewKeys/hasKeyboard all imply hasKeys ;).
    if not Device:hasKeys() then
        self.key_events = {}
    end
end

return InputContainer
