--[[--
Plugin for automatic dimming of the frontlight after an idle period.

@module koplugin.autodim
--]]--

local Device = require("device")
local Event = require("ui/event")
local FFIUtil = require("ffi/util")
local SpinWidget = require("ui/widget/spinwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local TrapWidget = require("ui/widget/trapwidget")
local time = require("ui/time")
local util = require("util")
local _ = require("gettext")
local C_ = _.pgettext
local Powerd = Device.powerd
local T = FFIUtil.template

local DEFAULT_AUTODIM_STARTTIME_M = 5
local DEFAULT_AUTODIM_DURATION_S = 5
local DEFAULT_AUTODIM_FRACTION = 20
local AUTODIM_EVENT_FREQUENCY = 2 -- in Hz; Frequenzy for FrontlightChangedEvent on E-Ink devices

local AutoDim = WidgetContainer:extend{
    name = "autodim",
}

function AutoDim:init()
    self.autodim_starttime_m = G_reader_settings:readSetting("autodim_starttime_minutes", -1)
    self.autodim_duration_s = G_reader_settings:readSetting("autodim_duration_seconds", DEFAULT_AUTODIM_DURATION_S)
    self.autodim_fraction = G_reader_settings:readSetting("autodim_fraction", DEFAULT_AUTODIM_FRACTION)

    self.last_action_time = UIManager:getElapsedTimeSinceBoot()

    self.ui.menu:registerToMainMenu(self)
    UIManager.event_hook:registerWidget("InputEvent", self)

    self:_schedule_autodim_task()
    self.isCurrentlyDimming = false -- true during or after the dimming ramp
    self.last_ramp_scheduling_time = nil -- holds start time of the next scheduled ramp task
    self.trap_widget = nil

    self.top_widget_before_dim = nil
end

function AutoDim:addToMainMenu(menu_items)
    menu_items.autodim = self:getAutoDimMenu()
end

function AutoDim:getAutoDimMenu()
    return {
        text = _("Automatic dimmer"),
        checked_func = function() return self.autodim_starttime_m > 0 end,
        sub_item_table = {
            {
                text_func = function()
                    return self.autodim_starttime_m <= 0 and _("Idle time for dimmer") or
                    T(_("Idle time for dimmer: %1"),
                        util.secondsToClockDuration("modern", self.autodim_starttime_m * 60, false, true, false, true))
                end,
                checked_func = function() return self.autodim_starttime_m > 0 end,
                callback = function(touchmenu_instance)
                    local idle_dialog = SpinWidget:new{
                        title_text = _("Automatic dimmer idle time"),
                        info_text = _("Start the dimmer after the designated period of inactivity."),
                        value = self.autodim_starttime_m >=0 and self.autodim_starttime_m or 0.5,
                        default_value = DEFAULT_AUTODIM_STARTTIME_M,
                        value_min = 0.5,
                        value_max = 60,
                        value_step = 0.5,
                        value_hold_step = 5,
                        unit = C_("Time", "min"),
                        precision = "%0.1f",
                        ok_always_enabled = true,
                        callback = function(spin)
                            if not spin then return end
                            self.autodim_starttime_m = spin.value
                            G_reader_settings:saveSetting("autodim_starttime_minutes", spin.value)
                            self:_schedule_autodim_task()
                            if touchmenu_instance then touchmenu_instance:updateItems() end
                        end,
                        extra_text = _("Disable"),
                        extra_callback = function()
                            self.autodim_starttime_m = -1
                            G_reader_settings:saveSetting("autodim_starttime_minutes", -1)
                            self:_schedule_autodim_task()
                            if touchmenu_instance then touchmenu_instance:updateItems() end
                        end,
                    }
                    UIManager:show(idle_dialog)
                    if touchmenu_instance then touchmenu_instance:updateItems() end
                end,
                keep_menu_open = true,
            },
            {
                text_func = function()
                    return T(_("Dimmer duration: %1"),
                        util.secondsToClockDuration("modern", self.autodim_duration_s, false, true, false, true))
                end,
                enabled_func = function() return self.autodim_starttime_m > 0 end,
                callback = function(touchmenu_instance)
                    local dimmer_dialog = SpinWidget:new{
                        title_text = _("Automatic dimmer duration"),
                        info_text = _("Delay to reach the lowest brightness."),
                        value = self.autodim_duration_s,
                        default_value = DEFAULT_AUTODIM_DURATION_S,
                        value_min = 0,
                        value_max = 300,
                        value_step = 1,
                        value_hold_step = 10,
                        precision = "%1d",
                        unit = C_("Time", "s"),
                        callback = function(spin)
                            if not spin then return end
                            self.autodim_duration_s = spin.value
                            G_reader_settings:saveSetting("autodim_duration_seconds", spin.value)
                            self:_schedule_autodim_task()
                            if touchmenu_instance then touchmenu_instance:updateItems() end
                        end,
                    }
                    UIManager:show(dimmer_dialog)
                    if touchmenu_instance then touchmenu_instance:updateItems() end
                end,
                keep_menu_open = true,
            },
            {
                text_func = function()
                    return T(_("Dim to %1 % of the regular brightness"), self.autodim_fraction)
                end,
                enabled_func = function() return self.autodim_starttime_m > 0 end,
                callback = function(touchmenu_instance)
                    local percentage_dialog = SpinWidget:new{
                        title_text = _("Dim to percentage"),
                        info_text = _("The lowest brightness as a percentage of the regular brightness."),
                        value = self.autodim_fraction,
                        value_default = DEFAULT_AUTODIM_FRACTION,
                        value_min = 0,
                        value_max = 100,
                        value_hold_step = 10,
                        unit = "%",
                        callback = function(spin)
                            self.autodim_fraction = spin.value
                            G_reader_settings:saveSetting("autodim_fraction", spin.value)
                            self:_schedule_autodim_task()
                            if touchmenu_instance then touchmenu_instance:updateItems() end
                        end,
                    }
                    UIManager:show(percentage_dialog)
                    if touchmenu_instance then touchmenu_instance:updateItems() end
                end,
                keep_menu_open = true,
            },
        }
    }
end

-- Schedules the first idle task, the consecutive ones are scheduled by the `autodim_task` itself.
-- `seconds` the initial scheduling delay of the first task
function AutoDim:_schedule_autodim_task(seconds)
    UIManager:unschedule(self.autodim_task)
    if self.autodim_starttime_m < 0 then
        if self.onResume then
            self:clearEventHandlers()
        end
        return
    else
        if not self.onResume then
            self:setEventHandlers()
        end
    end

    seconds = seconds or self.autodim_starttime_m * 60
    UIManager:scheduleIn(seconds, self.autodim_task, self)
end

function AutoDim:_unschedule_autodim_task()
    UIManager:unschedule(self.autodim_task)
end

function AutoDim:restoreFrontlight()
    if self.autodim_save_fl then
        Powerd:setIntensity(self.autodim_save_fl)
        self:updateFooter(true)
        self.autodim_save_fl = nil
    end
end

function AutoDim:onInputEvent()
    self.last_action_time = UIManager:getElapsedTimeSinceBoot()
    -- Make sure the next scheduled autodim check is as much in the future
    -- as possible, to reduce wakes from standby.
    self:_unschedule_ramp_task()
    self:_schedule_autodim_task()
end

function AutoDim:_onSuspend()
    self:_unschedule_autodim_task()
    if self.isCurrentlyDimming then
        self:_unschedule_ramp_task()
        self.isCurrentlyDimming = true -- message to self:onResume to go on with restoring
    end
end

function AutoDim:_onResume()
    self.last_action_time = UIManager:getElapsedTimeSinceBoot()
    if self.trap_widget then
        UIManager:scheduleIn(1, function()
            UIManager:close(self.trap_widget)
            self.trap_widget = nil
            self:restoreFrontlight()
        end)
        self.isCurrentlyDimming = false
    end
    self:_schedule_autodim_task()
end

function AutoDim:_onEnterStandby()
    self:_unschedule_autodim_task()
    -- don't unschedule ramp task, as this is done in onLeaveStandby if necessary
end

function AutoDim:_onLeaveStandby()
    if self.isCurrentlyDimming then
        if self.last_ramp_scheduling_time then
            -- we are during the ramp down
            local now = UIManager:getElapsedTimeSinceBoot()
            local next_ramp_time_s = self.last_ramp_scheduling_time + time.s(self.autodim_step_time_s) - now
            self:_unschedule_ramp_task() -- self.last_ramp_scheduling_time gets deleted with this call
            self.isCurrentlyDimming = true -- as this gets deleted by `_unschedule_ramp_task()`
            if next_ramp_time_s <= 0 then -- only happens, when standby is ended by a scheduled ramp_task()
                self:ramp_task()
            else
                self:_schedule_ramp_task(time.to_s(next_ramp_time_s))
            end
        else
            self:_unschedule_ramp_task()
        end
    else
        self:autodim_task() -- check times and reschedule autodim_task if necessary
    end
end

function AutoDim:setEventHandlers()
    self.onResume = self._onResume
    self.onSuspend = self._onSuspend
    self.onEnterStandby = self._onEnterStandby
    self.onLeaveStandby = self._onLeaveStandby
end

function AutoDim:clearEventHandlers()
    self.onResume = nil
    self.onSuspend = nil
    self.onEnterStandby = nil
    self.onLeaveStandby = nil
end

function AutoDim:onFrontlightTurnedOff()
    -- This might be happening through autowarmth during a ramp down.
    -- Set original intensity, but don't turn fl on actually.
    self.isCurrentlyDimming = false
    self.last_ramp_scheduling_time = nil
    UIManager:unschedule(self.ramp_task)

    Powerd.fl_intensity = self.autodim_save_fl or Powerd.fl_intensity
    self.autodim_save_fl = nil
    if self.trap_widget then
        UIManager:close(self.trap_widget) -- don't swallow input events from now
        self.trap_widget = nil
    end
    self:_schedule_autodim_task() -- reschedule
end

function AutoDim:updateFooter(clear)
    -- update footer only if it is not covered by another widget
    if self.top_widget_before_dim == "ReaderUI" or
        (self.top_widget_before_dim ~= "ConfigDialog" and self.top_widget_before_dim ~= "ScreenSaver"
        and self.top_widget_before_dim ~= "VirtualKeyboard") then

        UIManager:broadcastEvent(Event:new("UpdateFooter", self.view and self.view.footer_visible or false))
    end
    if clear then
        self.top_widget_before_dim = nil
    end
end

function AutoDim:autodim_task()
    if self.isCurrentlyDimming then return end
    if Powerd:isFrontlightOff() then
        self:_schedule_autodim_task()
    end
    local now = UIManager:getElapsedTimeSinceBoot()
    local idle_duration = now - self.last_action_time
    local check_delay = time.s(self.autodim_starttime_m * 60) - idle_duration
    if check_delay <= 0 then
        self.autodim_save_fl = self.autodim_save_fl or Powerd:frontlightIntensity()
        self.autodim_end_fl = math.floor(self.autodim_save_fl * self.autodim_fraction * (1/100) + 0.5)
        -- Clamp `self.autodim_end_fl` to 1 if `self.autodim_fraction` ~= 0
        if self.autodim_fraction ~= 0 and self.autodim_end_fl == 0 then
            self.autodim_end_fl = 1
        end
        local fl_diff = self.autodim_save_fl - self.autodim_end_fl
        if fl_diff > 0 then
            self.trap_widget = TrapWidget:new{
                dismiss_callback = function()
                    self:restoreFrontlight()
                    self.trap_widget = nil
                end
            }

            -- This is the active widget before showing self.trap_widget
            self.top_widget_before_dim = UIManager:getTopWidget()

            UIManager:show(self.trap_widget) -- suppress taps during dimming

            -- calculate time until the next decrease step
            self.autodim_step_time_s = math.max(self.autodim_duration_s / fl_diff, 0.001)
            self.ramp_event_countdown_startvalue = Device:hasEinkScreen() and
                math.floor((1/AUTODIM_EVENT_FREQUENCY) / self.autodim_step_time_s + 0.5) or 0
            self.ramp_event_countdown = self.ramp_event_countdown_startvalue

            self:ramp_task() -- which schedules itself
            -- Don't schedule `autodim_task` here, as this is done in `trap_widget.dismiss_callback` or in `onResume`
        else
            self.autodim_save_fl = nil
            self:_schedule_autodim_task()
        end
    else
        self:_schedule_autodim_task(time.to_s(check_delay))
    end
end

function AutoDim:ramp_task()
    self.isCurrentlyDimming = true -- this will disable rescheduling of the `autodim_task`
    local fl_level = Powerd:frontlightIntensity()
    if fl_level > self.autodim_end_fl then
        fl_level = fl_level - 1
        Powerd:setIntensity(fl_level)
        self.ramp_event_countdown = self.ramp_event_countdown - 1
        if self.ramp_event_countdown <= 0 then
            -- Update footer on every self.ramp_event_countdown call
            self:updateFooter()
            self.ramp_event_countdown = self.ramp_event_countdown_startvalue
            self.last_ramp_scheduling_time = nil
        end
        self:_schedule_ramp_task(self.autodim_step_time_s) -- Reschedule only if ramp is not finished
        -- `isCurrentlyDimming` stays true, to flag we have a dimmed FL.
    end
end

function AutoDim:_schedule_ramp_task(next_time_s)
    self.last_ramp_scheduling_time = UIManager:getElapsedTimeSinceBoot()
    UIManager:scheduleIn(next_time_s, self.ramp_task, self)
end

function AutoDim:_unschedule_ramp_task()
    if self.isCurrentlyDimming then
        UIManager:unschedule(self.ramp_task)
        self.isCurrentlyDimming = false
        self.last_ramp_scheduling_time = nil
    end
end

return AutoDim
