---------------------------------------------------------------------------
-- A layout with widgets added at specific positions.
--
-- Use cases include desktop icons, complex custom composed widgets, a floating
-- client layout and fine grained control over the output.
--
--@DOC_wibox_layout_defaults_manual_EXAMPLE@
-- @author Emmanuel Lepage Vallee
-- @copyright 2016 Emmanuel Lepage Vallee
-- @layoutmod wibox.layout.manual
-- @supermodule wibox.widget.base
---------------------------------------------------------------------------
local gtable = require("gears.table")
local base = require("wibox.widget.base")
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)

local manual_layout = {}

--- Add some widgets to the given stack layout.
--
-- @method add
-- @tparam widget ... Widgets that should be added
-- @interface layout
-- @noreturn

--- Remove a widget from the layout.
--
-- @method remove
-- @tparam number index The widget index to remove
-- @treturn boolean index If the operation is successful
-- @interface layout

--- Insert a new widget in the layout at position `index`.
--
-- @method insert
-- @tparam number index The position
-- @tparam widget widget The widget
-- @treturn boolean If the operation is successful.
-- @emits widget::inserted
-- @emitstparam widget::inserted widget self The fixed layout.
-- @emitstparam widget::inserted widget widget index The inserted widget.
-- @emitstparam widget::inserted number count The widget count.
-- @interface layout
function manual_layout:insert(index, widget)
    table.insert(self._private.widgets, index, widget)

    -- Add the point
    if widget.point then
        table.insert(self._private.pos, index, widget.point)
    end

    self:emit_signal("widget::inserted", widget, #self._private.widgets)

    self:emit_signal("widget::layout_changed")
end

--- Remove one or more widgets from the layout.
--
-- The last parameter can be a boolean, forcing a recursive seach of the
-- widget(s) to remove.
--
-- @method remove_widgets
-- @tparam widget ... Widgets that should be removed (must at least be one)
-- @treturn boolean If the operation is successful


function manual_layout:fit(_, width, height)
    return width, height
end

local function geometry(self, new)
    self._new_geo = new
    return self._new_geo or self
end

function manual_layout:layout(context, width, height)
    local res = {}

    for k, v in ipairs(self._private.widgets) do
        local pt = self._private.pos[k] or {x=0,y=0}
        local w, h = base.fit_widget(self, context, v, width, height)

        -- Make sure the signature is compatible with `awful.placement`. `Wibox`,
        -- doesn't depend on `awful`, but it is still nice not to have to code
        -- geometry functions again and again.
        if type(pt) == "function" or (getmetatable(pt) or {}).__call then
            local geo = {
                x      = 0,
                y      = 0,
                width  = w,
                height = h,
                geometry = geometry,
            }
            pt = pt(geo, {
                parent = {
                    x=0, y=0, width = width, height = height, geometry = geometry
                }
            })
            -- Trick to ensure compatibility with `awful.placement`
            gtable.crush(pt, geo._new_geo or {})
        end

        assert(pt.x)
        assert(pt.y)

        table.insert(res, base.place_widget_at(
            v, pt.x, pt.y, pt.width or w, pt.height or h
        ))
    end

    return res
end

function manual_layout:add(...)
    local wdgs = {}
    local old_count = #self._private.widgets

    for _, v in ipairs {...} do
        local w = base.make_widget_from_value(v)
        base.check_widget(w)
        table.insert(wdgs, w)
    end

    gtable.merge(self._private.widgets, wdgs)

    -- Add the points
    for k, v in ipairs(wdgs) do
        if v.point then
            self._private.pos[old_count+k] = v.point
        end
    end

    self:emit_signal("widget::layout_changed")
end

--- Add a widget at a specific point.
--
-- The point can either be a function or a table. The table follow the generic
-- geometry format used elsewhere in Awesome.
--
-- * *x*: The horizontal position.
-- * *y*: The vertical position.
-- * *width*: The width.
-- * *height*: The height.
--
-- If a function is used, it follows the same prototype as `awful.placement`
-- functions.
--
-- * *geo*:
--     * *x*: The horizontal position (always 0).
--     * *y*: The vertical position (always 0).
--     * *width*: The width.
--     * *height*: The height.
--     * *geometry*: A function to get or set the geometry (for compatibility).
--       The function is compatible with the `awful.placement` prototype.
-- * *args*:
--     * *parent* The layout own geometry
--         * *x*: The horizontal position (always 0).
--         * *y*: The vertical position (always 0).
--         * *width*: The width.
--         * *height*: The height.
--         * *geometry*: A function to get or set the geometry (for compatibility)
--           The function is compatible with the `awful.placement` prototype.
--
--@DOC_wibox_layout_manual_add_at_EXAMPLE@
--
-- @method add_at
-- @tparam widget widget The widget.
-- @tparam table|function point Either an `{x=x,y=y}` table or a function
--  returning the new geometry.
-- @noreturn
function manual_layout:add_at(widget, point)
    assert(not widget.point, "2 points are specified, only one is supported")

    -- Check is the point function is valid
    if type(point) == "function" or (getmetatable(point) or {}).__call then
        local fake_geo = {x=0,y=0,width=1,height=1,geometry=geometry}
        local pt = point(fake_geo, {
            parent = {
                x=0, y=0, width = 10, height = 10, geometry = geometry
            }
        })
        assert(pt and pt.x and pt.y, "The point function doesn't seem to be valid")
    end

    self._private.pos[#self._private.widgets+1] = point
    self:add(base.make_widget_from_value(widget))
end

--- Move a widget (by index).
--
-- @method move
-- @tparam number index The widget index.
-- @tparam table|function point A new point value.
-- @noreturn
-- @see add_at
function manual_layout:move(index, point)
    assert(self._private.pos[index])
    self._private.pos[index] = point
    self:emit_signal( "widget::layout_changed" )
end

--- Move a widget.
--
--@DOC_wibox_layout_manual_move_widget_EXAMPLE@
--
-- @method move_widget
-- @tparam widget widget The widget.
-- @tparam table|function point A new point value.
-- @noreturn
-- @see add_at
function manual_layout:move_widget(widget, point)
    local idx, l = self:index(widget, false)

    if idx then
        l:move(idx, point)
    end
end

function manual_layout:get_children()
    return self._private.widgets
end

function manual_layout:set_children(children)
    self:reset()
    self:add(unpack(children))
end

function manual_layout:reset()
    self._private.widgets = {}
    self._private.pos     = {}
    self:emit_signal( "widget::layout_changed" )
end

--- Create a manual layout.
--
-- @constructorfct wibox.layout.manual
-- @tparam table ... Widgets to add to the layout.

local function new_manual(...)
    local ret = base.make_widget(nil, nil, {enable_properties = true})

    gtable.crush(ret, manual_layout, true)
    ret._private.widgets = {}
    ret._private.pos = {}

    ret:add(...)

    return ret
end

--@DOC_fixed_COMMON@

return setmetatable(manual_layout, {__call=function(_,...) return new_manual(...) end})
