---------------------------------------------------------------------------
--- An auto-resized, free floating or modal wibox built around a widget.
--
-- This type of widget box (wibox) is auto closed when being clicked on and is
-- automatically resized to the size of its main widget.
--
-- Note that the widget itself should have a finite size. If something like a
-- `wibox.layout.flex` is used, then the size would be unlimited and an error
-- will be printed. The `wibox.layout.fixed`, `wibox.container.constraint`,
-- `forced_width` and `forced_height` are recommended.
--
--@DOC_awful_popup_simple_EXAMPLE@
--
-- Here is an example of how to create an alt-tab like dialog by leveraging
-- the `awful.widget.tasklist`.
--
--@DOC_awful_popup_alttab_EXAMPLE@
--
-- @author Emmanuel Lepage Vallee
-- @copyright 2016 Emmanuel Lepage Vallee
-- @popupmod awful.popup
-- @supermodule wibox
---------------------------------------------------------------------------
local wibox     = require( "wibox"           )
local gtable    = require( "gears.table"     )
local placement = require( "awful.placement" )
local xresources= require("beautiful.xresources")
local timer     = require( "gears.timer"     )
local capi      = {mouse = mouse}


local module = {}

local main_widget = {}

-- Get the optimal direction for the wibox
-- This (try to) avoid going offscreen
local function set_position(self)
    -- First, if there is size to be applied, do it
    if self._private.next_width then
        self.width = self._private.next_width
        self._private.next_width = nil
    end

    if self._private.next_height then
        self.height = self._private.next_height
        self._private.next_height = nil
    end

    local pf = self._private.placement

    if pf == false then return end

    if pf then
        pf(self, {bounding_rect = self.screen.geometry})
        return
    end

    local geo = self._private.widget_geo

    if not geo then return end

    local _, pos_name, anchor_name = placement.next_to(self, {
        preferred_positions = self._private.preferred_directions,
        geometry            = geo,
        preferred_anchors   = self._private.preferred_anchors,
        offset              = self._private.offset or { x = 0, y = 0},
    })

    if pos_name ~= self._private.current_position then
        local old = self._private.current_position
        self._private.current_position = pos_name
        self:emit_signal("property::current_position", pos_name, old)
    end

    if anchor_name ~= self._private.current_anchor then
        local old = self._private.current_anchor
        self._private.current_anchor = anchor_name
        self:emit_signal("property::current_anchor", anchor_name, old)
    end
end

-- Set the wibox size taking into consideration the limits
local function apply_size(self, width, height, set_pos)
    local prev_geo = self:geometry()

    width  = math.max(self._private.minimum_width  or 1, math.ceil(width  or 1))
    height = math.max(self._private.minimum_height or 1, math.ceil(height or 1))

    if self._private.maximum_width then
        width = math.min(self._private.maximum_width, width)
    end

    if self._private.maximum_height then
        height = math.min(self._private.maximum_height, height)
    end

    self._private.next_width, self._private.next_height = width, height

    if set_pos or width ~= prev_geo.width or height ~= prev_geo.height then
        set_position(self)
    end
end

-- Layout this widget
function main_widget:layout(context, width, height)
    if self._private.widget then
        local w, h = wibox.widget.base.fit_widget(
            self,
            context,
            self._private.widget,
            self._wb._private.maximum_width  or 9999,
            self._wb._private.maximum_height or 9999
        )
        timer.delayed_call(function()
            apply_size(self._wb, w, h, true)
        end)
        return { wibox.widget.base.place_widget_at(self._private.widget, 0, 0, width, height) }
    end
end

-- Set the widget that is drawn on top of the background
function main_widget:set_widget(widget)
    if widget then
        wibox.widget.base.check_widget(widget)
    end
    self._private.widget = widget
    self:emit_signal("widget::layout_changed")
    self:emit_signal("property::widget")
end

function main_widget:get_widget()
    return self._private.widget
end

function main_widget:get_children_by_id(name)
    return self._wb:get_children_by_id(name)
end

local popup = {}

--- Set the preferred popup position relative to its parent.
--
-- This allows, for example, to have a submenu that goes on the right of the
-- parent menu. If there is no space on the right, it tries on the left and so
-- on.
--
-- The basic use case for this method is to give it a parent wibox:
--
-- @DOC_awful_popup_position1_EXAMPLE@
--
-- As demonstrated by this second example, it is also possible to use a widget
-- as a parent object:
--
-- @DOC_awful_popup_position2_EXAMPLE@
--
-- @property preferred_positions
-- @tparam table|string preferred_positions
-- @propertydefault `{ "right", "left", "top", "bottom" }`
-- @propertytype string A single position with no fallback. It will be used
--  even if it doesn't fit.
-- @propertytype table A list of possible positions. The first one to fit will be
--  used.
-- @propertyvalue "left"
-- @propertyvalue "right"
-- @propertyvalue "top"
-- @propertyvalue "bottom"
-- @see awful.placement.next_to
-- @see awful.popup.preferred_anchors
-- @propemits true false

function popup:set_preferred_positions(pref_pos)
    self._private.preferred_directions = pref_pos
    set_position(self)
    self:emit_signal("property::preferred_positions", pref_pos)
end

--- Set the preferred popup anchors relative to the parent.
--
-- For details information, see the `awful.placement.next_to` documentation.
--
-- In this example, it is possible to see the effect of having a fallback
-- preferred anchors when the popup would otherwise not fit:
--
-- @DOC_awful_popup_anchors_EXAMPLE@
--
-- @property preferred_anchors
-- @tparam[opt="back"] table|string preferred_anchors
--  ordered by priority.
-- @propertytype string A single anchor value with no fallback.
-- @propertytype table A list of possible anchor, the first one has the higher
--  priority, but will fallback if it doesn't fit.
-- @propertyvalue "front"
-- @propertyvalue "middle"
-- @propertyvalue "back"
-- @propemits true false
-- @see awful.placement.next_to
-- @see awful.popup.preferred_positions

function popup:set_preferred_anchors(pref_anchors)
    self._private.preferred_anchors = pref_anchors
    set_position(self)
    self:emit_signal("property::preferred_anchors", pref_anchors)
end

--- The current position relative to the parent object.
--
-- If there is a parent object (widget, wibox, wibar, client or the mouse), then
-- this property returns the current position. This is determined using
-- `preferred_positions`. It is usually the preferred position, but when there
-- isn't enough space, it can also be one of the fallback.
--
-- @property current_position
-- @tparam string current_position
-- @propertydefault This depends on where the popup was displayed.
-- @propertyvalue "left"
-- @propertyvalue "right"
-- @propertyvalue "top"
-- @propertyvalue "bottom"
-- @readonly
-- @see awful.popup.preferred_positions

function popup:get_current_position()
    return self._private.current_position
end

--- Get the current anchor relative to the parent object.
--
-- If there is a parent object (widget, wibox, wibar, client or the mouse), then
-- this property returns the current anchor. The anchor is the "side" of the
-- parent object on which the popup is based on. It will "grow" in the
-- opposite direction from the anchor.
--
-- @property current_anchor
-- @tparam string current_anchor
-- @propertydefault This depends on where the popup was displayed.
-- @propertyvalue "front"
-- @propertyvalue "middle"
-- @propertyvalue "back"
-- @readonly
-- @see awful.popup.preferred_anchors

function popup:get_current_anchor()
    return self._private.current_anchor
end

--- Move the wibox to a position relative to `geo`.
-- This will try to avoid overlapping the source wibox and auto-detect the right
-- direction to avoid going off-screen.
--
-- @param[opt=mouse] obj An object such as a wibox, client or a table entry
--  returned by `wibox:find_widgets()`.
-- @see awful.placement.next_to
-- @see awful.popup.preferred_positions
-- @see awful.popup.preferred_anchors
-- @treturn table The new geometry
-- @method move_next_to
function popup:move_next_to(obj)
    if self._private.is_relative == false then return end

    self._private.widget_geo = obj

    obj = obj or capi.mouse

    if obj._apply_size_now then
        obj:_apply_size_now(false)
    end

    self.visible = true

    self:_apply_size_now(true)

    self._private.widget_geo = nil
end

--- Bind the popup to a widget button press.
--
-- @tparam widget widget The widget
-- @tparam[opt=1] number button The button index
-- @method bind_to_widget
-- @noreturn
function popup:bind_to_widget(widget, button)
    if not self._private.button_for_widget then
        self._private.button_for_widget = {}
    end

    self._private.button_for_widget[widget] = button or 1
    widget:connect_signal("button::press", self._private.show_fct)
end

--- Unbind the popup from a widget button.
-- @tparam widget widget
-- @propertytype widget A widget or declarative widget construct.
-- @method unbind_to_widget
-- @noreturn
function popup:unbind_to_widget(widget)
    widget:disconnect_signal("button::press", self._private.show_fct)
end

--- Hide the popup when right clicked.
--
-- @property hide_on_right_click
-- @tparam[opt=false] boolean hide_on_right_click
-- @propemits true false

function popup:set_hide_on_right_click(value)
    self[value and "connect_signal" or "disconnect_signal"](
        self, "button::press", self._private.hide_fct
    )
    self:emit_signal("property::hide_on_right_click", value)
end

--- The popup minimum width.
--
-- @property minimum_width
-- @tparam[opt=1] integer minimum_width
-- @propertyunit pixel
-- @rangestart 1
-- @propemits true false

--- The popup minimum height.
--
-- @property minimum_height
-- @tparam[opt=1] integer minimum_height
-- @propertyunit pixel
-- @rangestart 1
-- @propemits true false

--- The popup maximum width.
--
-- @property maximum_width
-- @tparam[opt=1] integer maximum_width
-- @propertyunit pixel
-- @rangestart 1
-- @propemits true false

--- The popup maximum height.
--
-- @property maximum_height
-- @tparam[opt=1] integer maximum_height
-- @propertyunit pixel
-- @rangestart 1
-- @propemits true false

for _, orientation in ipairs {"_width", "_height"} do
    for _, limit in ipairs {"minimum", "maximum"} do
        local prop = limit..orientation
        popup["set_"..prop] = function(self, value)
            self._private[prop] = value
            self._private.container:emit_signal("widget::layout_changed")
            self:emit_signal("property::"..prop, value)
        end
        popup["get_"..prop] = function(self)
            return self._private[prop]
        end
    end
end

--- The distance between the popup and its parent (if any).
--
-- Here is an example of 5 popups stacked one below the other with an y axis
-- offset (spacing).
--
-- @DOC_awful_popup_position3_EXAMPLE@
-- @property offset
-- @tparam[opt=0] table|integer offset
-- @tparam[opt=0] integer offset.x The horizontal offset.
-- @tparam[opt=0] integer offset.y The vertical offset.
-- @propertytype integer A value for both `x` and `y` simultaneously.
-- @propertytype table Specify values for `x` and `y` individually.
-- @negativeallowed true
-- @propertyunit pixel
-- @propemits true false

function popup:set_offset(offset)

    if type(offset) == "number" then
        offset = {
            x = offset or 0,
            y = offset or 0,
        }
    end

    local oldoff = self._private.offset or {x=0, y=0}

    if oldoff.x == offset.x and oldoff.y == offset.y then return end

    offset.x, offset.y = offset.x or oldoff.x or 0, offset.y or oldoff.y or 0

    self._private.offset = offset

    self:_apply_size_now(false)

    self:emit_signal("property::offset", offset)
end

--- Set the placement function.
--
-- @property placement
-- @tparam[opt=awful.placement.next_to] placement|string|boolean placement
-- @propertytype boolean Use `false` to disable automatic placement.
-- @propertytype string The name of an `awful.placement` function, like `"next_to"`.
-- @propemits true false
-- @see awful.placement

function popup:set_placement(f)
    if type(f) == "string" then
        f = placement[f]
    end

    self._private.placement = f
    self:_apply_size_now(false)
    self:emit_signal("property::placement")
end

-- For the tests and the race condition when 2 popups are placed next to each
-- other.
function popup:_apply_size_now(skip_set)
    if not self.widget then return end

    local w, h = wibox.widget.base.fit_widget(
        self.widget,
        {dpi= self.screen.dpi or xresources.get_dpi()},
        self.widget,
        self._private.maximum_width  or 9999,
        self._private.maximum_height or 9999
    )

    -- It is important to do it for the obscure reason that calling `w:geometry()`
    -- is actually mutating the state due to quantum determinism thanks to XCB
    -- async nature... It is only true the very first time `w:geometry()` is
    -- called
    self.width  = math.max(1, math.ceil(w or 1))
    self.height = math.max(1, math.ceil(h or 1))

    apply_size(self, w, h, skip_set ~= false)
end

function popup:set_widget(wid)
    self._private.widget = wid
    self._private.container:set_widget(wid)
    self:emit_signal("property::widget", wid)
end

function popup:get_widget()
    return self._private.widget
end

--- Create a new popup build around a passed in widget.
-- @tparam[opt=nil] table args
--@DOC_wibox_constructor_COMMON@
-- @tparam function args.placement The `awful.placement` function
-- @tparam string|table args.preferred_positions
-- @tparam string|table args.preferred_anchors
-- @tparam table|number args.offset The X and Y offset compared to the parent object
-- @tparam boolean args.hide_on_right_click Whether or not to hide the popup on
--  right clicks.
-- @constructorfct awful.popup
local function create_popup(_, args)
    assert(args)

    -- Temporarily remove the widget
    local original_widget = args.widget
    args.widget = nil

    assert(original_widget, "The `awful.popup` requires a `widget` constructor argument")

    local child_widget = wibox.widget.base.make_widget_from_value(original_widget)

    local ii = wibox.widget.base.make_widget(child_widget, "awful.popup", {
        enable_properties = true
    })

    gtable.crush(ii, main_widget, true)

    -- Create a wibox to host the widget
    local w = wibox(args or {})

    rawset(w, "_private", {
        container            = ii,
        preferred_directions = { "right", "left", "top", "bottom" },
        preferred_anchors    = { "back", "front", "middle" },
        widget = child_widget
    })

    gtable.crush(w, popup)

    ii:set_widget(child_widget)

    -- Create the signal handlers
    function w._private.show_fct(wdg, _, _, button, _, geo)
        if button == w._private.button_for_widget[wdg] then
            w:move_next_to(geo)
        end
    end
    function w._private.hide_fct()
        w.visible = false
    end

    -- Restore
    args.widget = original_widget

    -- Cross-link the wibox and widget
    ii._wb = w
    wibox.set_widget(w, ii)

    --WARNING The order is important
    -- First, apply the limits to avoid a flicker with large width or height
    -- when set_position is called before the limits
    for _,v in ipairs{"minimum_width", "minimum_height", "maximum_height",
      "maximum_width", "offset", "placement","preferred_positions",
      "preferred_anchors", "hide_on_right_click"} do
        if args[v] ~= nil then
            w["set_"..v](w, args[v])
        end
    end

    -- Default to visible
    if args.visible ~= false then
        w.visible = true
    end

    return w
end

return setmetatable(module, {__call = create_popup})
