---------------------------------------------------------------------------
--- Mouse snapping related functions
--
-- @author Julien Danjou <julien@danjou.info>
-- @copyright 2008 Julien Danjou
-- @release @AWESOME_VERSION@
-- @submodule mouse
---------------------------------------------------------------------------

local aclient   = require("awful.client")
local resize    = require("awful.mouse.resize")
local aplace    = require("awful.placement")
local wibox     = require("wibox")
local beautiful = require("beautiful")
local color     = require("gears.color")
local shape     = require("gears.shape")
local cairo     = require("lgi").cairo

local capi = {
    root = root,
    mouse = mouse,
    screen = screen,
    client = client,
    mousegrabber = mousegrabber,
}

local module = {
    default_distance = 8
}

local placeholder_w = nil

local function show_placeholder(geo)
    if not geo then
        if placeholder_w then
            placeholder_w.visible = false
        end
        return
    end

    placeholder_w = placeholder_w or wibox {
        ontop = true,
        bg    = color(beautiful.snap_bg or beautiful.bg_urgent or "#ff0000"),
    }

    placeholder_w:geometry(geo)

    local img = cairo.ImageSurface(cairo.Format.A1, geo.width, geo.height)
    local cr = cairo.Context(img)

    cr:set_operator(cairo.Operator.CLEAR)
    cr:set_source_rgba(0,0,0,1)
    cr:paint()
    cr:set_operator(cairo.Operator.SOURCE)
    cr:set_source_rgba(1,1,1,1)

    local line_width = beautiful.snap_border_width or 5
    cr:set_line_width(beautiful.xresources.apply_dpi(line_width))

    local f = beautiful.snap_shape or function()
        cr:translate(line_width,line_width)
        shape.rounded_rect(cr,geo.width-2*line_width,geo.height-2*line_width, 10)
    end

    f(cr, geo.width, geo.height)

    cr:stroke()

    placeholder_w.shape_bounding = img._native

    placeholder_w.visible = true
end

local function build_placement(snap, axis)
    return aplace.scale
        + aplace[snap]
        + (
            axis and aplace["maximize_"..axis] or nil
          )
end

local function detect_screen_edges(c, snap)
    local coords = capi.mouse.coords()

    local sg = c.screen.geometry

    local v, h = nil

    if math.abs(coords.x) <= snap + sg.x and coords.x >= sg.x then
        h = "left"
    elseif math.abs((sg.x + sg.width) - coords.x) <= snap then
        h = "right"
    end

    if math.abs(coords.y) <= snap + sg.y and coords.y >= sg.y then
        v = "top"
    elseif math.abs((sg.y + sg.height) - coords.y) <= snap then
        v = "bottom"
    end

    return v, h
end

local current_snap, current_axis = nil

local function detect_areasnap(c, distance)
    local old_snap = current_snap
    local v, h = detect_screen_edges(c, distance)

    if v and h then
        current_snap = v.."_"..h
    else
        current_snap = v or h or nil
    end

    if old_snap == current_snap then return end

    current_axis = ((v and not h) and "horizontally")
        or ((h and not v) and "vertically")
        or nil

    -- Show the expected geometry outline
    show_placeholder(
        current_snap and build_placement(current_snap, current_axis)(c, {
            to_percent     = 0.5,
            honor_workarea = true,
            pretend        = true
        }) or nil
    )

end

local function apply_areasnap(c, args)
    if not current_snap then return end

    -- Remove the move offset
    args.offset = {}

    placeholder_w.visible = false

    return build_placement(current_snap, current_axis)(c,{
        to_percent     = 0.5,
        honor_workarea = true,
    })
end

local function snap_outside(g, sg, snap)
    if g.x < snap + sg.x + sg.width and g.x > sg.x + sg.width then
        g.x = sg.x + sg.width
    elseif g.x + g.width < sg.x and g.x + g.width > sg.x - snap then
        g.x = sg.x - g.width
    end
    if g.y < snap + sg.y + sg.height and g.y > sg.y + sg.height then
        g.y = sg.y + sg.height
    elseif g.y + g.height < sg.y and g.y + g.height > sg.y - snap then
        g.y = sg.y - g.height
    end
    return g
end

local function snap_inside(g, sg, snap)
    local edgev = 'none'
    local edgeh = 'none'
    if math.abs(g.x) < snap + sg.x and g.x > sg.x then
        edgev = 'left'
        g.x = sg.x
    elseif math.abs((sg.x + sg.width) - (g.x + g.width)) < snap then
        edgev = 'right'
        g.x = sg.x + sg.width - g.width
    end
    if math.abs(g.y) < snap + sg.y and g.y > sg.y then
        edgeh = 'top'
        g.y = sg.y
    elseif math.abs((sg.y + sg.height) - (g.y + g.height)) < snap then
        edgeh = 'bottom'
        g.y = sg.y + sg.height - g.height
    end

    -- What is the dominant dimension?
    if g.width > g.height then
        return g, edgeh
    else
        return g, edgev
    end
end

--- Snap a client to the closest client or screen edge.
-- @function awful.mouse.snap
-- @param c The client to snap.
-- @param snap The pixel to snap clients.
-- @param x The client x coordinate.
-- @param y The client y coordinate.
-- @param fixed_x True if the client isn't allowed to move in the x direction.
-- @param fixed_y True if the client isn't allowed to move in the y direction.
function module.snap(c, snap, x, y, fixed_x, fixed_y)
    snap = snap or module.default_distance
    c = c or capi.client.focus
    local cur_geom = c:geometry()
    local geom = c:geometry()
    geom.width = geom.width + (2 * c.border_width)
    geom.height = geom.height + (2 * c.border_width)
    local edge
    geom.x = x or geom.x
    geom.y = y or geom.y

    geom, edge = snap_inside(geom, capi.screen[c.screen].geometry, snap)
    geom = snap_inside(geom, capi.screen[c.screen].workarea, snap)

    -- Allow certain windows to snap to the edge of the workarea.
    -- Only allow docking to workarea for consistency/to avoid problems.
    if c.dockable then
        local struts = c:struts()
        struts['left'] = 0
        struts['right'] = 0
        struts['top'] = 0
        struts['bottom'] = 0
        if edge ~= "none" and c.floating then
            if edge == "left" or edge == "right" then
                struts[edge] = cur_geom.width
            elseif edge == "top" or edge == "bottom" then
                struts[edge] = cur_geom.height
            end
        end
        c:struts(struts)
    end

    for _, snapper in ipairs(aclient.visible(c.screen)) do
        if snapper ~= c then
            local snapper_geom = snapper:geometry()
            snapper_geom.width = snapper_geom.width + (2 * snapper.border_width)
            snapper_geom.height = snapper_geom.height + (2 * snapper.border_width)
            geom = snap_outside(geom, snapper_geom, snap)
        end
    end

    geom.width = geom.width - (2 * c.border_width)
    geom.height = geom.height - (2 * c.border_width)

    -- It's easiest to undo changes afterwards if they're not allowed
    if fixed_x then geom.x = cur_geom.x end
    if fixed_y then geom.y = cur_geom.y end

    return geom
end

-- Enable edge snapping
resize.add_move_callback(function(c, geo, args)
    -- Screen edge snapping (areosnap)
    if (module.edge_enabled ~= false)
      and args and (args.snap == nil or args.snap) then
        detect_areasnap(c, 16)
    end

    -- Snapping between clients
    if (module.client_enabled ~= false)
      and args and (args.snap == nil or args.snap) then
        return module.snap(c, args.snap, geo.x, geo.y)
    end
end, "mouse.move")

-- Apply the aerosnap
resize.add_leave_callback(function(c, _, args)
    if module.edge_enabled == false then return end
    return apply_areasnap(c, args)
end, "mouse.move")

return setmetatable(module, {__call = function(_, ...) return module.snap(...) end})