---------------------------------------------------------------------------
--- Algorithms used to place various drawables.
--
-- The functions provided by this module all follow the same arguments
-- conventions. This allow:
--
-- * To use them in various other module as
--   [visitor objects](https://en.wikipedia.org/wiki/Visitor_pattern)
-- * Turn each function into an API with various common customization parameters.
-- * Re-use the same functions for the `mouse`, `client`s, `screen`s and `wibox`es
-- 
--
-- <h3>Compositing</h3>
--
-- It is possible to compose placement function using the `+` or `*` operator:
--
--@DOC_awful_placement_compose_EXAMPLE@
--
--@DOC_awful_placement_compose2_EXAMPLE@
--
-- <h3>Common arguments</h3>
--
-- **pretend** (*boolean*):
--
-- Do not apply the new geometry. This is useful if only the return values is
-- necessary.
--
-- **honor_workarea** (*boolean*):
--
--  Take workarea into account when placing the drawable (default: false)
--
-- **honor_padding** (*boolean*):
--
--  Take the screen padding into account (see `screen.padding`)
--
-- **tag** (*tag*):
--
--  Use a tag geometry
--
-- **margins** (*number* or *table*):
--
--  A table with left, right, top, bottom keys or a number
--
-- **parent** (client, wibox, mouse or screen):
--
--  A parent drawable to use a base geometry
--
-- **bounding_rect** (table):
--
-- A bounding rectangle
--
-- **attach** (*boolean*):
--
-- When the parent geometry (like the screen) changes, re-apply the placement
-- function. This will add a `detach_callback` function to the drawable. Call
-- this to detach the function. This will be called automatically when a new
-- attached function is set.
--
-- **offset** (*table or number*):
--
-- The offset(s) to apply to the new geometry.
--
-- **store_geometry** (*boolean*):
--
-- Keep a single history of each type of placement. It can be restored using
-- `awful.placement.restore` by setting the right `context` argument.
--
-- When either the parent or the screen geometry change, call the placement
-- function again.
--
-- **update_workarea** (*boolean*):
--
-- If *attach* is true, also update the screen workarea.
--
-- @author Emmanuel Lepage Vallee &lt;elv1313@gmail.com&gt;
-- @author Julien Danjou &lt;julien@danjou.info&gt;
-- @copyright 2008 Julien Danjou, Emmanuel Lepage Vallee 2016
-- @release @AWESOME_VERSION@
-- @module awful.placement
---------------------------------------------------------------------------

-- Grab environment we need
local ipairs = ipairs
local pairs = pairs
local math = math
local table = table
local capi =
{
    screen = screen,
    mouse = mouse,
    client = client
}
local client = require("awful.client")
local layout = require("awful.layout")
local a_screen = require("awful.screen")
local grect = require("gears.geometry").rectangle
local util = require("awful.util")
local dpi = require("beautiful").xresources.apply_dpi

local function get_screen(s)
    return s and capi.screen[s]
end

local wrap_client = nil
local placement

-- Store function -> keys
local reverse_align_map = {}

-- Forward declarations
local area_common
local wibox_update_strut
local attach

--- Allow multiple placement functions to be daisy chained.
-- This also allow the functions to be aware they are being chained and act
-- upon the previous nodes results to avoid unnecessary processing or deduce
-- extra paramaters/arguments.
local function compose(...)
    local queue = {}

    local nodes = {...}

    -- Allow placement.foo + (var == 42 and placement.bar)
    if not nodes[2] then
        return nodes[1]
    end

    -- nodes[1] == self, nodes[2] == other
    for _, w in ipairs(nodes) do
        -- Build an execution queue
        if w.context and w.context == "compose" then
            for _, elem in ipairs(w.queue or {}) do
                table.insert(queue, elem)
            end
        else
            table.insert(queue, w)
        end
    end

    local ret
    ret = wrap_client(function(d, args, ...)
        local rets = {}
        local last_geo = nil

        -- As some functions may have to take into account results from
        -- previously execued ones, add the `composition_results` hint.
        args = setmetatable({composition_results=rets}, {__index=args})

        -- Only apply the geometry once, not once per chain node, to do this,
        -- Force the "pretend" argument and restore the original value for
        -- the last node.
        local attach_real = args.attach
        args.pretend      = true
        args.attach       = false
        args.offset       = {}

        for k, f in ipairs(queue) do
            if k == #queue then
                -- Let them fallback to the parent table
                args.pretend = nil
                args.offset  = nil
            end

            local r = {f(d, args, ...)}
            last_geo = r[1] or last_geo
            args.override_geometry = last_geo

            -- Keep the return value, store one per context
            if f.context then
                -- When 2 composition queue are executed, merge the return values
                if f.context == "compose" then
                    for k2,v in pairs(r) do
                        rets[k2] = v
                    end
                else
                    rets[f.context] = r
                end
            end
        end

        if attach_real then
            args.attach = true
            attach(d, ret, args)
        end

        return last_geo, rets
    end, "compose")

    ret.queue = queue

    return ret
end

wrap_client = function(f, context)
    return setmetatable(
        {
            is_placement= true,
            context     = context,
        },
        {
            __call = function(_,...) return f(...) end,
            __add  = compose, -- Composition is usually defined as +
            __mul  = compose  -- Make sense if you think of the functions as matrices
        }
    )
end

local placement_private = {}

-- The module is a proxy in front of the "real" functions.
-- This allow syntax like:
--
--    (awful.placement.no_overlap + awful.placement.no_offscreen)(c)
--
placement = setmetatable({}, {
    __index = placement_private,
    __newindex = function(_, k, f)
        placement_private[k] = wrap_client(f, k)
    end
})

-- 3x3 matrix of the valid sides and corners
local corners3x3 = {{"top_left"   ,   "top"   , "top_right"   },
                    {"left"       ,    nil    , "right"       },
                    {"bottom_left",  "bottom" , "bottom_right"}}

-- 2x2 matrix of the valid sides and corners
local corners2x2 = {{"top_left"   ,            "top_right"   },
                    {"bottom_left",            "bottom_right"}}

-- Compute the new `x` and `y`.
-- The workarea position need to be applied by the caller
local align_map = {
    top_left          = function(_ , _ , _ , _ ) return {x=0        , y=0        } end,
    top_right         = function(sw, _ , dw, _ ) return {x=sw-dw    , y=0        } end,
    bottom_left       = function(_ , sh, _ , dh) return {x=0        , y=sh-dh    } end,
    bottom_right      = function(sw, sh, dw, dh) return {x=sw-dw    , y=sh-dh    } end,
    left              = function(_ , sh, _ , dh) return {x=0        , y=sh/2-dh/2} end,
    right             = function(sw, sh, dw, dh) return {x=sw-dw    , y=sh/2-dh/2} end,
    top               = function(sw, _ , dw, _ ) return {x=sw/2-dw/2, y=0        } end,
    bottom            = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=sh-dh    } end,
    centered          = function(sw, sh, dw, dh) return {x=sw/2-dw/2, y=sh/2-dh/2} end,
    center_vertical   = function(_ , sh, _ , dh) return {x= nil     , y=sh-dh    } end,
    center_horizontal = function(sw, _ , dw, _ ) return {x=sw/2-dw/2, y= nil     } end,
}

-- Some parameters to correctly compute the final size
local resize_to_point_map = {
    -- Corners
    top_left     = {p1= nil  , p2={1,1}, x_only=false, y_only=false, align="bottom_right"},
    top_right    = {p1={0,1} , p2= nil , x_only=false, y_only=false, align="bottom_left" },
    bottom_left  = {p1= nil  , p2={1,0}, x_only=false, y_only=false, align="top_right"   },
    bottom_right = {p1={0,0} , p2= nil , x_only=false, y_only=false, align="top_left"    },

    -- Sides
    left         = {p1= nil  , p2={1,1}, x_only=true , y_only=false, align="top_right"   },
    right        = {p1={0,0} , p2= nil , x_only=true , y_only=false, align="top_left"    },
    top          = {p1= nil  , p2={1,1}, x_only=false, y_only=true , align="bottom_left" },
    bottom       = {p1={0,0} , p2= nil , x_only=false, y_only=true , align="top_left"    },
}

--- Add a context to the arguments.
-- This function extend the argument table. The context is used by some
-- internal helper methods. If there already is a context, it has priority and
-- is kept.
local function add_context(args, context)
    return setmetatable({context = (args or {}).context or context }, {__index=args})
end

local data = setmetatable({}, { __mode = 'k' })

--- Store a drawable geometry (per context) in a weak table.
-- @param d The drawin
-- @tparam string reqtype The context.
local function store_geometry(d, reqtype)
    if not data[d] then data[d] = {} end
    if not data[d][reqtype] then data[d][reqtype] = {} end
    data[d][reqtype] = d:geometry()
    data[d][reqtype].screen = d.screen
end

--- Get the margins and offset
-- @tparam table args The arguments
-- @treturn table The margins
-- @treturn table The offsets
local function get_decoration(args)
    local offset = args.offset

    -- Offset are "blind" values added to the output
    offset = type(offset) == "number" and {
        x      = offset,
        y      = offset,
        width  = offset,
        height = offset,
    } or args.offset or {}

    -- Margins are distances on each side to substract from the area`
    local m = type(args.margins) == "table" and args.margins or {
        left = args.margins or 0 , right  = args.margins or 0,
        top  = args.margins or 0 , bottom = args.margins or 0
    }

    return m, offset
end

--- Apply some modifications before applying the new geometry.
-- @tparam table new_geo The new geometry
-- @tparam table args The common arguments
-- @tparam boolean force Always ajust the geometry, even in pretent mode. This
--  should only be used when returning the final geometry as it would otherwise
--  mess the pipeline.
-- @treturn table|nil The new geometry
local function fix_new_geometry(new_geo, args, force)
    if (args.pretend and not force) or not new_geo then return nil end

    local m, offset = get_decoration(args)

    return {
        x      = new_geo.x      and (new_geo.x + (offset.x or 0) + (m.left or 0) ),
        y      = new_geo.y      and (new_geo.y + (offset.y or 0) + (m.top  or 0) ),
        width  = new_geo.width  and math.max(
            1, (new_geo.width  + (offset.width  or 0) - (m.left or 0) - (m.right or 0)  )
        ),
        height = new_geo.height and math.max(
            1, (new_geo.height + (offset.height or 0) - (m.top  or 0) - (m.bottom or 0) )
        ),
    }
end

-- Get the area covered by a drawin.
-- @param d The drawin
-- @tparam[opt=nil] table new_geo A new geometry
-- @tparam[opt=false] boolean ignore_border_width Ignore the border
-- @tparam table args the method arguments
-- @treturn The drawin's area.
area_common = function(d, new_geo, ignore_border_width, args)
    -- The C side expect no arguments, nil isn't valid
    local geometry = new_geo and d:geometry(new_geo) or d:geometry()
    local border = ignore_border_width and 0 or d.border_width or 0

    -- When using the placement composition along with the "pretend"
    -- option, it is necessary to keep a "virtual" geometry.
    if args and args.override_geometry then
        geometry = util.table.clone(args.override_geometry)
    end

    geometry.width = geometry.width + 2 * border
    geometry.height = geometry.height + 2 * border
    return geometry
end

--- Get (and optionally set) an object geometry.
-- Some elements, such as `mouse` and `screen` don't have a `:geometry()`
-- methods.
-- @param obj An object
-- @tparam table args the method arguments
-- @tparam[opt=nil] table new_geo A new geometry to replace the existing one
-- @tparam[opt=false] boolean ignore_border_width Ignore the border
-- @treturn table A table with *x*, *y*, *width* and *height*.
local function geometry_common(obj, args, new_geo, ignore_border_width)
    -- Store the current geometry in a singleton-memento
    if args.store_geometry and new_geo and args.context then
        store_geometry(obj, args.context)
    end

    -- It's a mouse
    if obj.coords then
        local coords = fix_new_geometry(new_geo, args)
            and obj.coords(new_geo) or obj.coords()
        return {x=coords.x, y=coords.y, width=0, height=0}
    elseif obj.geometry then
        local geo = obj.geometry

        -- It is either a drawable or something that implement its API
        if type(geo) == "function" then
            local dgeo = area_common(
                obj, fix_new_geometry(new_geo, args), ignore_border_width, args
            )

            -- Apply the margins
            if args.margins then
                local delta = get_decoration(args)

                return {
                    x      = dgeo.x      - (delta.left or 0),
                    y      = dgeo.y      - (delta.top  or 0),
                    width  = dgeo.width  + (delta.left or 0) + (delta.right  or 0),
                    height = dgeo.height + (delta.top  or 0) + (delta.bottom or 0),
                }
            end

            return dgeo
        end

        -- It is a screen, it doesn't support setting new sizes.
        return obj:get_bounding_geometry(args)
    else
        assert(false, "Invalid object")
    end
end

--- Get the parent geometry from the standardized arguments API shared by all
-- `awful.placement` methods.
-- @param obj A screen or a drawable
-- @tparam table args the method arguments
-- @treturn table A table with *x*, *y*, *width* and *height*.
local function get_parent_geometry(obj, args)
    -- Didable override_geometry, context and other to avoid mutating the state
    -- or using the wrong geo.

    if args.bounding_rect then
        return args.bounding_rect
    elseif args.parent then
        return geometry_common(args.parent, {})
    elseif obj.screen then
        return geometry_common(obj.screen, {
            honor_padding  = args.honor_padding,
            honor_workarea = args.honor_workarea
        })
    else
        return geometry_common(capi.screen[capi.mouse.screen], args)
    end
end

--- Move a point into an area.
-- This doesn't change the *width* and *height* values, allowing the target
-- area to be smaller than the source one.
-- @tparam table source The (larger) geometry to move `target` into
-- @tparam table target The area to move into `source`
-- @treturn table A table with *x* and *y* keys
local function move_into_geometry(source, target)
    local ret = {x = target.x, y = target.y}

    -- Horizontally
    if ret.x < source.x then
        ret.x = source.x
    elseif ret.x > source.x + source.width then
        ret.x = source.x + source.width - 1
    end

    -- Vertically
    if ret.y < source.y then
        ret.y = source.y
    elseif ret.y > source.y + source.height then
        ret.y = source.y + source.height - 1
    end

    return ret
end

-- Update the workarea
wibox_update_strut = function(d, position, args)
    -- If the drawable isn't visible, remove the struts
    if not d.visible then
        d:struts { left = 0, right = 0, bottom = 0, top = 0 }
        return
    end

    -- Detect horizontal or vertical drawables
    local geo      = area_common(d)
    local vertical = geo.width < geo.height

    -- Look into the `position` string to find the relevants sides to crop from
    -- the workarea
    local struts = { left = 0, right = 0, bottom = 0, top = 0 }

    local m = get_decoration(args)

    if vertical then
        for _, v in ipairs {"right", "left"} do
            if (not position) or position:match(v) then
                struts[v] = geo.width + m[v]
            end
        end
    else
        for _, v in ipairs {"top", "bottom"} do
            if (not position) or position:match(v) then
                struts[v] = geo.height + m[v]
            end
        end
    end

    -- Update the workarea
    d:struts(struts)
end

-- Pin a drawable to a placement function.
-- Automatically update the position when the size change.
-- All other arguments will be passed to the `position` function (if any)
-- @tparam[opt=client.focus] drawable d A drawable (like `client`, `mouse`
--   or `wibox`)
-- @param position_f A position name (see `align`) or a position function
-- @tparam[opt={}] table args Other arguments
attach = function(d, position_f, args)
    args = args or {}

    if args.pretend then return end

    if not args.attach then return end

    -- Avoid a connection loop
    args = setmetatable({attach=false}, {__index=args})

    d = d or capi.client.focus
    if not d then return end

    if type(position_f) == "string" then
        position_f = placement[position_f]
    end

    if not position_f then return end

    -- If there is multiple attached function, there is an high risk of infinite
    -- loop. While some combinaisons are harmless, other are very hard to debug.
    --
    -- Use the placement composition to build explicit multi step attached
    -- placement functions.
    if d.detach_callback then
        d.detach_callback()
        d.detach_callback = nil
    end

    local function tracker()
        position_f(d, args)
    end

    d:connect_signal("property::width"       , tracker)
    d:connect_signal("property::height"      , tracker)
    d:connect_signal("property::border_width", tracker)

    local function tracker_struts()
        --TODO this is too fragile and doesn't work with all methods.
        wibox_update_strut(d, d.position or reverse_align_map[position_f], args)
    end

    local parent = args.parent or d.screen

    if args.update_workarea then
        d:connect_signal("property::geometry" , tracker_struts)
        d:connect_signal("property::visible"  , tracker_struts)
        capi.client.connect_signal("property::struts", tracker_struts)

        tracker_struts()
    elseif parent == d.screen then
        if args.honor_workarea then
            parent:connect_signal("property::workarea", tracker)
        end

        if args.honor_padding then
            parent:connect_signal("property::padding", tracker)
        end
    end

    -- If there is a parent drawable, screen or mouse, also track it
    if parent then
        parent:connect_signal("property::geometry" , tracker)
    end

    -- Create a way to detach a placement function
    d:add_signal("property::detach_callback")
    function d.detach_callback()
        d:disconnect_signal("property::width"       , tracker)
        d:disconnect_signal("property::height"      , tracker)
        d:disconnect_signal("property::border_width", tracker)
        if parent then
            parent:disconnect_signal("property::geometry" , tracker)

            if parent == d.screen then
                if args.honor_workarea then
                    parent:disconnect_signal("property::workarea", tracker)
                end

                if args.honor_padding then
                    parent:disconnect_signal("property::padding", tracker)
                end
            end
        end

        if args.update_workarea then
            d:disconnect_signal("property::geometry" , tracker_struts)
            d:disconnect_signal("property::visible"  , tracker_struts)
            capi.client.disconnect_signal("property::struts", tracker_struts)
        end
    end
end

-- Convert 2 points into a rectangle
local function rect_from_points(p1x, p1y, p2x, p2y)
    return {
        x      = p1x,
        y      = p1y,
        width  = p2x - p1x,
        height = p2y - p1y,
    }
end

-- Convert a rectangle and matrix info into a point
local function rect_to_point(rect, corner_i, corner_j)
    return {
        x = rect.x + corner_i * math.floor(rect.width ),
        y = rect.y + corner_j * math.floor(rect.height),
    }
end

--- Move a drawable to the closest corner of the parent geometry (such as the
-- screen).
--
-- Valid arguments include the common ones and:
--
-- * **include_sides**: Also include the left, right, top and bottom positions
--
--@DOC_awful_placement_closest_mouse_EXAMPLE@
-- @tparam[opt=client.focus] drawable d A drawable (like `client`, `mouse`
--   or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
-- @treturn string The corner name
function placement.closest_corner(d, args)
    args = add_context(args, "closest_corner")
    d = d or capi.client.focus

    local sgeo = get_parent_geometry(d, args)
    local dgeo = geometry_common(d, args)

    local pos  = move_into_geometry(sgeo, dgeo)

    local corner_i, corner_j, n

    -- Use the product of 3 to get the closest point in a NxN matrix
    local function f(_n, mat)
        n        = _n
        -- The +1 is required to avoid a rounding error when
        --    pos.x == sgeo.x+sgeo.width
        corner_i = -math.ceil( ( (sgeo.x - pos.x) * n) / (sgeo.width  + 1))
        corner_j = -math.ceil( ( (sgeo.y - pos.y) * n) / (sgeo.height + 1))
        return mat[corner_j + 1][corner_i + 1]
    end

    -- Turn the area into a grid and snap to the cloest point. This size of the
    -- grid will increase the accuracy. A 2x2 matrix only include the corners,
    -- at 3x3, this include the sides too technically, a random size would work,
    -- but without corner names.
    local grid_size = args.include_sides and 3 or 2

    -- If the point is in the center, use the closest corner
    local corner = grid_size == 3 and f(3, corners3x3) or f(2, corners2x2)

    -- Transpose the corner back to the original size
    local new_args = setmetatable({position = corner}, {__index=args})
    local ngeo = placement_private.align(d, new_args)

    return fix_new_geometry(ngeo, args, true), corner
end

--- Place the client so no part of it will be outside the screen (workarea).
--@DOC_awful_placement_no_offscreen_EXAMPLE@
-- @client c The client.
-- @tparam[opt=client's screen] integer screen The screen.
-- @treturn table The new client geometry.
function placement.no_offscreen(c, screen)
    --HACK necessary for composition to work. The API will be changed soon
    if type(screen) == "table" then
        screen = nil
    end

    c = c or capi.client.focus
    local geometry = area_common(c)
    screen = get_screen(screen or c.screen or a_screen.getbycoord(geometry.x, geometry.y))
    local screen_geometry = screen.workarea

    if geometry.x + geometry.width > screen_geometry.x + screen_geometry.width then
        geometry.x = screen_geometry.x + screen_geometry.width - geometry.width
    end
    if geometry.x < screen_geometry.x then
        geometry.x = screen_geometry.x
    end

    if geometry.y + geometry.height > screen_geometry.y + screen_geometry.height then
        geometry.y = screen_geometry.y + screen_geometry.height - geometry.height
    end
    if geometry.y < screen_geometry.y then
        geometry.y = screen_geometry.y
    end

    return c:geometry {
        x = geometry.x,
        y = geometry.y
    }
end

--- Place the client where there's place available with minimum overlap.
--@DOC_awful_placement_no_overlap_EXAMPLE@
-- @param c The client.
-- @treturn table The new geometry
function placement.no_overlap(c)
    c = c or capi.client.focus
    local geometry = area_common(c)
    local screen   = get_screen(c.screen or a_screen.getbycoord(geometry.x, geometry.y))
    local cls = client.visible(screen)
    local curlay = layout.get()
    local areas = { screen.workarea }
    for _, cl in pairs(cls) do
        if cl ~= c and cl.type ~= "desktop" and (cl.floating or curlay == layout.suit.floating) then
            areas = grect.area_remove(areas, area_common(cl))
        end
    end

    -- Look for available space
    local found = false
    local new = { x = geometry.x, y = geometry.y, width = 0, height = 0 }
    for _, r in ipairs(areas) do
        if r.width >= geometry.width
           and r.height >= geometry.height
           and r.width * r.height > new.width * new.height then
            found = true
            new = r
            -- Check if the client's current position is available
            -- and prefer that one (why move it around pointlessly?)
            if     geometry.x >= r.x
               and geometry.y >= r.y
               and geometry.x + geometry.width <= r.x + r.width
               and geometry.y + geometry.height <= r.y + r.height then
                new.x = geometry.x
                new.y = geometry.y
            end
        end
    end

    -- We did not find an area with enough space for our size:
    -- just take the biggest available one and go in.
    -- This makes sure to have the whole screen's area in case it has been
    -- removed.
    if not found then
        if #areas == 0 then
            areas = { screen.workarea }
        end
        for _, r in ipairs(areas) do
            if r.width * r.height > new.width * new.height then
                new = r
            end
        end
    end

    -- Restore height and width
    new.width = geometry.width
    new.height = geometry.height

    return c:geometry({ x = new.x, y = new.y })
end

--- Place the client under the mouse.
--@DOC_awful_placement_under_mouse_EXAMPLE@
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.under_mouse(d, args)
    args = add_context(args, "under_mouse")
    d = d or capi.client.focus

    local m_coords = capi.mouse.coords()

    local ngeo = geometry_common(d, args)
    ngeo.x = m_coords.x - ngeo.width  / 2
    ngeo.y = m_coords.y - ngeo.height / 2

    local bw = d.border_width or 0
    ngeo.width  = ngeo.width  - 2*bw
    ngeo.height = ngeo.height - 2*bw

    geometry_common(d, args, ngeo)

    return fix_new_geometry(ngeo, args, true)
end

--- Place the client next to the mouse.
--
-- It will place `c` next to the mouse pointer, trying the following positions
-- in this order: right, left, above and below.
--@DOC_awful_placement_next_to_mouse_EXAMPLE@
-- @client[opt=focused] c The client.
-- @tparam[opt=apply_dpi(5)] integer offset The offset from the mouse position.
-- @treturn table The new geometry
function placement.next_to_mouse(c, offset)
    c = c or capi.client.focus
    offset = offset or dpi(5)
    local c_geometry = area_common(c)
    local c_width = c_geometry.width
    local c_height = c_geometry.height
    local m_coords = capi.mouse.coords()
    local screen_geometry = capi.screen[capi.mouse.screen].workarea

    local x, y

    -- Prefer it to be on the right.
    x = m_coords.x + offset
    if x + c_width > screen_geometry.width then
        -- Then to the left.
        x = m_coords.x - c_width - offset
    end
    if x < screen_geometry.x then
        -- Then above.
        x = m_coords.x - math.ceil(c_width / 2)
        y = m_coords.y - c_height - offset
        if y < screen_geometry.y then
            -- Finally below.
            y = m_coords.y + offset
        end
    else
        y = m_coords.y - math.ceil(c_height / 2)
    end
    return c:geometry({ x = x, y = y })
end

--- Resize the drawable to the cursor.
--
-- Valid args:
--
-- * *axis*: The axis (vertical or horizontal). If none is
--    specified, then the drawable will be resized on both axis.
--
--@DOC_awful_placement_resize_to_mouse_EXAMPLE@
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.resize_to_mouse(d, args)
    d    = d or capi.client.focus
    args = add_context(args, "resize_to_mouse")

    local coords = capi.mouse.coords()
    local ngeo   = geometry_common(d, args)
    local h_only = args.axis == "horizontal"
    local v_only = args.axis == "vertical"

    -- To support both growing and shrinking the drawable, it is necessary
    -- to decide to use either "north or south" and "east or west" directions.
    -- Otherwise, the result will always be 1x1
    local _, closest_corner = placement.closest_corner(capi.mouse, {
        parent        = d,
        pretend       = true,
        include_sides = args.include_sides or false,
    })

    -- Given "include_sides" wasn't set, it will always return a name
    -- with the 2 axis. If only one axis is needed, adjust the result
    if h_only then
        closest_corner = closest_corner:match("left") or closest_corner:match("right")
    elseif v_only then
        closest_corner = closest_corner:match("top")  or closest_corner:match("bottom")
    end

    -- Use p0 (mouse), p1 and p2 to create a rectangle
    local pts = resize_to_point_map[closest_corner]
    local p1  = pts.p1 and rect_to_point(ngeo, pts.p1[1], pts.p1[2]) or coords
    local p2  = pts.p2 and rect_to_point(ngeo, pts.p2[1], pts.p2[2]) or coords

    -- Create top_left and bottom_right points, convert to rectangle
    ngeo = rect_from_points(
        pts.y_only and ngeo.x               or math.min(p1.x, p2.x),
        pts.x_only and ngeo.y               or math.min(p1.y, p2.y),
        pts.y_only and ngeo.x + ngeo.width  or math.max(p2.x, p1.x),
        pts.x_only and ngeo.y + ngeo.height or math.max(p2.y, p1.y)
    )

    local bw = d.border_width or 0

    for _, a in ipairs {"width", "height"} do
        ngeo[a] = ngeo[a] - 2*bw
    end

    -- Now, correct the geometry by the given size_hints offset
    if d.apply_size_hints then
        local w, h = d:apply_size_hints(
            ngeo.width,
            ngeo.height
        )
        local offset = align_map[pts.align](w, h, ngeo.width, ngeo.height)
        ngeo.x = ngeo.x - offset.x
        ngeo.y = ngeo.y - offset.y
    end

    geometry_common(d, args, ngeo)

    return fix_new_geometry(ngeo, args, true)
end

--- Move the drawable (client or wibox) `d` to a screen position or side.
--
-- Supported args.positions are:
--
-- * top_left
-- * top_right
-- * bottom_left
-- * bottom_right
-- * left
-- * right
-- * top
-- * bottom
-- * centered
-- * center_vertical
-- * center_horizontal
--
--@DOC_awful_placement_align_EXAMPLE@
-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`)
-- @tparam[opt={}] table args Other arguments
-- @treturn table The new geometry
function placement.align(d, args)
    args = add_context(args, "align")
    d    = d or capi.client.focus

    if not d or not args.position then return end

    local sgeo = get_parent_geometry(d, args)
    local dgeo = geometry_common(d, args)
    local bw   = d.border_width or 0

    local pos  = align_map[args.position](
        sgeo.width ,
        sgeo.height,
        dgeo.width ,
        dgeo.height
    )

    local ngeo = {
        x      = (pos.x and math.ceil(sgeo.x + pos.x) or dgeo.x)       ,
        y      = (pos.y and math.ceil(sgeo.y + pos.y) or dgeo.y)       ,
        width  =            math.ceil(dgeo.width    )            - 2*bw,
        height =            math.ceil(dgeo.height   )            - 2*bw,
    }

    geometry_common(d, args, ngeo)

    attach(d, placement[args.position], args)

    return fix_new_geometry(ngeo, args, true)
end

-- Add the alias functions
for k in pairs(align_map) do
    placement[k] = function(d, args)
        args = add_context(args, k)
        args.position = k
        return placement_private.align(d, args)
    end
    reverse_align_map[placement[k]] = k
end

-- Add the documentation for align alias

---@DOC_awful_placement_top_left_EXAMPLE@

---@DOC_awful_placement_top_right_EXAMPLE@

---@DOC_awful_placement_bottom_left_EXAMPLE@

---@DOC_awful_placement_bottom_right_EXAMPLE@

---@DOC_awful_placement_left_EXAMPLE@

---@DOC_awful_placement_right_EXAMPLE@

---@DOC_awful_placement_top_EXAMPLE@

---@DOC_awful_placement_bottom_EXAMPLE@

---@DOC_awful_placement_centered_EXAMPLE@

---@DOC_awful_placement_center_vertical_EXAMPLE@

---@DOC_awful_placement_center_horizontal_EXAMPLE@

--- Stretch a drawable in a specific direction.
-- Valid args:
--
-- * **direction**: The stretch direction (*left*, *right*, *up*, *down*) or
--  a table with multiple directions.
--
--@DOC_awful_placement_stretch_EXAMPLE@
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
function placement.stretch(d, args)
    args = add_context(args, "stretch")

    d    = d or capi.client.focus
    if not d or not args.direction then return end

    -- In case there is multiple directions, call `stretch` for each of them
    if type(args.direction) == "table" then
        for _, dir in ipairs(args.direction) do
            args.direction = dir
            placement_private.stretch(dir, args)
        end
        return
    end

    local sgeo = get_parent_geometry(d, args)
    local dgeo = geometry_common(d, args)
    local ngeo = geometry_common(d, args, nil, true)
    local bw   = d.border_width or 0

    if args.direction == "left" then
        ngeo.x      = sgeo.x
        ngeo.width  = dgeo.width + (dgeo.x - ngeo.x)
    elseif args.direction == "right" then
        ngeo.width  = sgeo.width - ngeo.x - 2*bw
    elseif args.direction == "up" then
        ngeo.y      = sgeo.y
        ngeo.height = dgeo.height + (dgeo.y - ngeo.y)
    elseif args.direction == "down" then
        ngeo.height = sgeo.height - dgeo.y - 2*bw
    else
        assert(false)
    end

    -- Avoid negative sizes if args.parent isn't compatible
    ngeo.width  = math.max(args.minimim_width  or 1, ngeo.width )
    ngeo.height = math.max(args.minimim_height or 1, ngeo.height)

    geometry_common(d, args, ngeo)

    attach(d, placement["stretch_"..args.direction], args)

    return fix_new_geometry(ngeo, args, true)
end

-- Add the alias functions
for _,v in ipairs {"left", "right", "up", "down"} do
    placement["stretch_"..v] =  function(d, args)
        args = add_context(args, "stretch_"..v)
        args.direction = v
        return placement_private.stretch(d, args)
    end
end

---@DOC_awful_placement_stretch_left_EXAMPLE@

---@DOC_awful_placement_stretch_right_EXAMPLE@

---@DOC_awful_placement_stretch_up_EXAMPLE@

---@DOC_awful_placement_stretch_down_EXAMPLE@

--- Maximize a drawable horizontally, vertically or both.
-- Valid args:
--
-- * *axis*:The axis (vertical or horizontal). If none is
--    specified, then the drawable will be maximized on both axis.
--
--@DOC_awful_placement_maximize_EXAMPLE@
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
function placement.maximize(d, args)
    args = add_context(args, "maximize")
    d    = d or capi.client.focus

    if not d then return end

    local sgeo = get_parent_geometry(d, args)
    local ngeo = geometry_common(d, args, nil, true)
    local bw   = d.border_width or 0

    if (not args.axis) or args.axis :match "vertical" then
        ngeo.y      = sgeo.y
        ngeo.height = sgeo.height - 2*bw
    end

    if (not args.axis) or args.axis :match "horizontal" then
        ngeo.x      = sgeo.x
        ngeo.width  = sgeo.width - 2*bw
    end

    geometry_common(d, args, ngeo)

    attach(d, placement.maximize, args)

    return fix_new_geometry(ngeo, args, true)
end

-- Add the alias functions
for _, v in ipairs {"vertically", "horizontally"} do
    placement["maximize_"..v] = function(d2, args)
        args = add_context(args, "maximize_"..v)
        args.axis = v
        return placement_private.maximize(d2, args)
    end
end

--- Scale the drawable by either a relative or absolute percent.
--
-- Valid args:
--
-- **to_percent** : A number between 0 and 1. It represent a percent related to
--  the parent geometry.
-- **by_percent** : A number between 0 and 1. It represent a percent related to
--  the current size.
-- **direction**: Nothing or "left", "right", "up", "down".
--
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn table The new geometry
function placement.scale(d, args)
    args = add_context(args, "scale_to_percent")
    d    = d or capi.client.focus

    local to_percent = args.to_percent
    local by_percent = args.by_percent

    local percent = to_percent or by_percent

    local direction = args.direction

    local sgeo = get_parent_geometry(d, args)
    local ngeo = geometry_common(d, args, nil)

    local old_area = {width = ngeo.width, height = ngeo.height}

    if (not direction) or direction == "left" or direction == "right" then
        ngeo.width = (to_percent and sgeo or ngeo).width*percent

        if direction == "left" then
            ngeo.x = ngeo.x - (ngeo.width - old_area.width)
        end
    end

    if (not direction) or direction == "up" or direction == "down" then
        ngeo.height = (to_percent and sgeo or ngeo).height*percent

        if direction == "up" then
            ngeo.y = ngeo.y - (ngeo.height - old_area.height)
        end
    end

    local bw = d.border_width or 0
    ngeo.width  = ngeo.width  - 2*bw
    ngeo.height = ngeo.height - 2*bw

    geometry_common(d, args, ngeo)

    attach(d, placement.maximize, args)

    return fix_new_geometry(ngeo, args, true)
end

---@DOC_awful_placement_maximize_vertically_EXAMPLE@

---@DOC_awful_placement_maximize_horizontally_EXAMPLE@

--- Restore the geometry.
-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`)
-- @tparam[opt={}] table args The arguments
-- @treturn boolean If the geometry was restored
function placement.restore(d, args)
    if not args or not args.context then return false end
    d = d or capi.client.focus

    if not data[d] then return false end

    local memento = data[d][args.context]

    if not memento then return false end

    memento.screen = nil --TODO use it
    d:geometry(memento)
    return true
end

return placement

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80