----------------------------------------------------------------------------
--- A notification popup widget.
--
-- By default, the box is composed of many other widgets:
--
--@DOC_wibox_nwidget_default_EXAMPLE@
--
-- @author Emmanuel Lepage Vallee &lt;elv1313@gmail.com&gt;
-- @copyright 2017 Emmanuel Lepage Vallee
-- @popupmod naughty.layout.box
-- @supermodule awful.popup
----------------------------------------------------------------------------

local capi       = {screen=screen}
local beautiful  = require("beautiful")
local gtimer     = require("gears.timer")
local gtable     = require("gears.table")
local wibox      = require("wibox")
local popup      = require("awful.popup")
local awcommon   = require("awful.widget.common")
local placement  = require("awful.placement")
local abutton    = require("awful.button")
local ascreen    = require("awful.screen")
local gpcall     = require("gears.protected_call")
local dpi        = require("beautiful").xresources.apply_dpi
local cst        = require("naughty.constants")

local default_widget = require("naughty.widget._default")

local box, by_position = {}, {}

-- Init the weak tables for each positions. It is done ahead of time rather
-- than when notifications are added to simplify the code.

local function init_screen(s)
    if not s.valid then return end

    if by_position[s] then return by_position[s] end

    by_position[s] = setmetatable({},{__mode = "k"})

    for _, pos in ipairs { "top_left"   , "top_middle"   , "top_right",
                           "bottom_left", "bottom_middle", "bottom_right" } do
        by_position[s][pos] = setmetatable({},{__mode = "v"})
    end

    return by_position[s]
end

local function disconnect(self)
    local n = self._private.notification[1]

    if n then
        n:disconnect_signal("destroyed",
            self._private.destroy_callback)

        n:disconnect_signal("property::margin",
            self._private.update)

        n:disconnect_signal("property::suspended",
            self._private.hide)
    end
end

ascreen.connect_for_each_screen(init_screen)

-- Manually cleanup to help the GC.
capi.screen.connect_signal("removed", function(scr)
    -- By that time, all direct events should have been handled. Cleanup the
    -- leftover. Being a weak table doesn't help Lua 5.1.
    gtimer.delayed_call(function()
        by_position[scr] = nil
    end)
end)

local function get_spacing()
    local margin = beautiful.notification_spacing or dpi(2)
    return {top = margin, bottom = margin}
end

local function get_offset(position, preset)
    preset = preset or {}
    local margin = preset.padding or beautiful.notification_spacing or dpi(4)
    if position:match('_right') then
        return {x = -margin}
    elseif position:match('_left') then
        return {x = margin}
    end
    return {}
end

-- Leverage `awful.placement` to create the stacks.
local function update_position(position, preset)
    local pref  = position:match("top_") and "bottom" or "top"
    local align = position:match("_(.*)")
        :gsub("left", "front"):gsub("right", "back")

    for _, pos in pairs(by_position) do
        for k, wdg in ipairs(pos[position]) do
            local args = {
                geometry            = pos[position][k-1],
                preferred_positions = {pref },
                preferred_anchors   = {align},
                margins             = get_spacing(),
                honor_workarea      = true,
            }
            if k == 1 then
                args.offset = get_offset(position, preset)
            end

            -- The first entry is aligned to the workarea, then the following to the
            -- previous widget.
            placement[k==1 and position:gsub("_middle", "") or "next_to"](wdg, args)
        end
    end
end

local function finish(self)
    self.visible = false
    assert(init_screen(self.screen)[self.position])

    for k, v in ipairs(init_screen(self.screen)[self.position]) do
        if v == self then
            table.remove(init_screen(self.screen)[self.position], k)
            break
        end
    end

    local preset = (self._private.notification[1] or {}).preset

    update_position(self.position, preset)

    disconnect(self)

    self._private.notification = {}
end

-- It isn't a good idea to use the `attach` `awful.placement` property. If the
-- screen is resized or the notification is moved, it causes side effects.
-- Better listen to geometry changes and reflow.
capi.screen.connect_signal("property::geometry", function(s)
    for pos, notifs in pairs(by_position[s]) do
        if #notifs > 0 then
            update_position(pos, notifs[1].preset)
        end
    end
end)

--- The maximum notification width.
-- @beautiful beautiful.notification_max_width
-- @tparam[opt=500] number notification_max_width

--- The maximum notification position.
--
-- Valid values are:
--
-- * top_left
-- * top_middle
-- * top_right
-- * bottom_left
-- * bottom_middle
-- * bottom_right
--
-- @beautiful beautiful.notification_position
-- @tparam[opt="top_right"] string notification_position

--- The widget notification object.
--
-- @property notification
-- @tparam naughty.notification notification
-- @propertydefault This must be provided by the constructor.
-- @propemits true false

--- The widget template to construct the box content.
--
--@DOC_wibox_nwidget_default_EXAMPLE@
--
-- The default template is (less or more):
--
--    {
--        {
--            {
--                {
--                    {
--                        naughty.widget.icon,
--                        {
--                            naughty.widget.title,
--                            naughty.widget.message,
--                            spacing = 4,
--                            layout  = wibox.layout.fixed.vertical,
--                        },
--                        fill_space = true,
--                        spacing    = 4,
--                        layout     = wibox.layout.fixed.horizontal,
--                    },
--                    naughty.list.actions,
--                    spacing = 10,
--                    layout  = wibox.layout.fixed.vertical,
--                },
--                margins = beautiful.notification_margin,
--                widget  = wibox.container.margin,
--            },
--            id     = "background_role",
--            widget = naughty.container.background,
--        },
--        strategy = "max",
--        width    = width(beautiful.notification_max_width
--            or beautiful.xresources.apply_dpi(500)),
--        widget   = wibox.container.constraint,
--    }
--
-- @property widget_template
-- @tparam[opt=nil] template|nil widget_template
-- @usebeautiful beautiful.notification_max_width The maximum width for the
--  resulting widget.

local function generate_widget(args, n)
    local w = gpcall(wibox.widget.base.make_widget_from_value,
        args.widget_template or (n and n.widget_template) or default_widget
    )

    -- This will happen if the user-provided widget_template is invalid and/or
    -- got unexpected notifications.
    if not w then
        w = gpcall(wibox.widget.base.make_widget_from_value, default_widget)

        -- In case this happens in an error message itself, make sure the
        -- private error popup code knowns it and can revert to the fallback
        -- popup.
        if not w then
            n._private.widget_template_failed = true
        end

        return nil
    end

    if w.set_width then
        w:set_width(n.max_width or beautiful.notification_max_width or dpi(500))
    end

    -- Call `:set_notification` on all children
    awcommon._set_common_property(w, "notification", n)

    return w
end

local function init(self, notification)
    local preset = notification.preset or {}

    local position = self._private.position or notification.position or
        preset.position or beautiful.notification_position or "top_right"

    if not self.widget then
        self.widget = generate_widget(self._private, notification)
    end

    local bg = self._private.widget:get_children_by_id( "background_role" )[1]

    -- Make sure the border isn't set twice, favor the widget one since it is
    -- shared by the notification list and the notification box.
    if bg then
        if bg.set_notification then
            bg:set_notification(notification)
            self.border_width = 0
        else
            bg:set_bg(notification.bg)
            self.border_width = notification.border_width
        end
    end

    local s = notification.screen
    assert(s)

    -- Add the notification to the active list
    assert(init_screen(s)[position], "Invalid position "..position)

    self:_apply_size_now()

    table.insert(init_screen(s)[position], self)

    self._private.update = function() update_position(position, preset) end
    self._private.hide = function(_, value)
        if value then
            finish(self)
        end
    end

    self:connect_signal("property::geometry", self._private.update)
    notification:weak_connect_signal("property::margin", self._private.update)
    notification:weak_connect_signal("property::suspended", self._private.hide)
    notification:weak_connect_signal("destroyed", self._private.destroy_callback)

    update_position(position, preset)

    self.visible = true
end

function box:set_notification(notif)
    if self._private.notification[1] == notif then return end

    disconnect(self)

    init(self, notif)

    self._private.notification = setmetatable({notif}, {__mode="v"})

    self:emit_signal("property::notification", notif)
end

function box:get_notification()
    return self._private.notification[1]
end

function box:get_position()
    local n = self._private.notification[1]

    if n then
        return n:get_position()
    end

    return "top_right"
end

--- Create a notification popup box.
--
-- @constructorfct naughty.layout.box
-- @tparam[opt=nil] table args
-- @tparam table args.widget_template A widget definition template which will
--  be instantiated for each box.
-- @tparam naughty.notification args.notification The notification object.
-- @tparam string args.position The position. See `naughty.notification.position`.
--@DOC_wibox_constructor_COMMON@
-- @usebeautiful beautiful.notification_position If `position` is not defined
-- in the notification object (or in this constructor).

local function new(args)
    args = args or {}

    -- Set the default wibox values
    local new_args = {
        ontop        = true,
        visible      = false,
        bg           = args.bg or beautiful.notification_bg,
        fg           = args.fg or beautiful.notification_fg,
        shape        = args.shape or beautiful.notification_shape,
        border_width = args.border_width or beautiful.notification_border_width or 1,
        border_color = args.border_color or beautiful.notification_border_color,
    }

    -- The C code needs `pairs` to work, so a full copy is required.
    gtable.crush(new_args, args, true)

    -- Add a weak-table layer for the screen.
    local weak_args = setmetatable({
        screen = args.notification and args.notification.screen or nil
    }, {__mode="v"})

    setmetatable(new_args, {__index = weak_args})

    -- Generate the box before the popup is created to avoid the size changing
    new_args.widget = generate_widget(new_args, new_args.notification)

    -- It failed, request::fallback will be used, there is nothing left to do.
    if not new_args.widget then return nil end

    local ret = popup(new_args)
    ret._private.notification = {}
    ret._private.widget_template = args.widget_template
    ret._private.position = args.position

    gtable.crush(ret, box, true)

    function ret._private.destroy_callback()
        finish(ret)
    end

    if new_args.notification then
        ret:set_notification(new_args.notification)
    end

    --TODO remove
    local function hide(reason)
        return function ()
            local n = ret._private.notification[1]

            if n then
                n:destroy(reason)
            end
        end
    end

    -- On right click, close the notification without triggering the default action
    ret:buttons(gtable.join(
        abutton({ }, 1, hide(cst.notification_closed_reason.dismissed_by_user)),
        abutton({ }, 3, hide(cst.notification_closed_reason.silent))
    ))

    gtable.crush(ret, box, false)

    return ret
end

return setmetatable(box, {__call = function(_, args) return new(args) end})
