local machi = {
    layout = require((...):match("(.-)[^%.]+$") .. "layout"),
    engine = require((...):match("(.-)[^%.]+$") .. "engine"),
}

local capi = {
    client = client
}

local beautiful = require("beautiful")
local wibox = require("wibox")
local awful = require("awful")
local gears = require("gears")
local lgi = require("lgi")
local dpi = require("beautiful.xresources").apply_dpi
local gtimer = require("gears.timer")

local ERROR = 2
local WARNING = 1
local INFO = 0
local DEBUG = -1

local module = {
    log_level = WARNING,
}

local function log(level, msg)
    if level > module.log_level then
        print(msg)
    end
end

local function min(a, b)
    if a < b then return a else return b end
end

local function max(a, b)
    if a < b then return b else return a end
end

local function with_alpha(col, alpha)
    local r, g, b
    _, r, g, b, _ = col:get_rgba()
    return lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
end

function module.start(c, exit_keys)
    local tablist_font_desc = beautiful.get_merged_font(
        beautiful.font, dpi(10))
    local font_color = with_alpha(gears.color(beautiful.fg_normal), 1)
    local font_color_hl = with_alpha(gears.color(beautiful.fg_focus), 1)
    local label_size = dpi(30)
    local border_color = with_alpha(
        gears.color(beautiful.machi_switcher_border_color or beautiful.border_focus),
        beautiful.machi_switcher_border_opacity or 0.25)
    local border_color_hl = with_alpha(
        gears.color(beautiful.machi_switcher_border_hl_color or beautiful.border_focus),
        beautiful.machi_switcher_border_hl_opacity or 0.75)
    local fill_color = with_alpha(
        gears.color(beautiful.machi_switcher_fill_color or beautiful.bg_normal),
        beautiful.machi_switcher_fill_opacity or 0.25)
    local box_bg = with_alpha(
        gears.color(beautiful.machi_switcher_box_bg or beautiful.bg_normal),
        beautiful.machi_switcher_box_opacity or 0.85)
    local fill_color_hl = with_alpha(
        gears.color(beautiful.machi_switcher_fill_color_hl or beautiful.bg_focus),
        beautiful.machi_switcher_fill_hl_opacity or 1)
    -- for comparing floats
    local threshold = 0.1
    local traverse_radius = dpi(5)

    local screen = c and c.screen or awful.screen.focused()
    local tag = screen.selected_tag
    local layout = tag.layout
    local gap = tag.gap
    local start_x = screen.workarea.x
    local start_y = screen.workarea.y

    if (c ~= nil and c.floating) or layout.machi_get_instance_data == nil then return end

    local cd, td, areas, _new_placement_cb = layout.machi_get_instance_data(screen, screen.selected_tag)
    if areas == nil or #areas == 0 then
        return
    end

    local infobox = wibox({
            screen = screen,
            x = screen.workarea.x,
            y = screen.workarea.y,
            width = screen.workarea.width,
            height = screen.workarea.height,
            bg = "#ffffff00",
            opacity = 1,
            ontop = true,
            type = "dock",
    })
    infobox.visible = true

    local tablist = nil
    local tablist_index = nil

    local traverse_x, traverse_y
    if c then
        traverse_x = c.x + traverse_radius
        traverse_y = c.y + traverse_radius
    else
        traverse_x = screen.workarea.x + screen.workarea.width / 2
        traverse_y = screen.workarea.y + screen.workarea.height / 2
    end

    local selected_area_ = nil
    local function set_selected_area(area)
        selected_area_ = area
        if area then
            traverse_x = max(areas[area].x + traverse_radius, min(areas[area].x + areas[area].width - traverse_radius, traverse_x))
            traverse_y = max(areas[area].y + traverse_radius, min(areas[area].y + areas[area].height - traverse_radius, traverse_y))
        end
    end

    local function selected_area()
        if selected_area_ == nil then
            local min_dis = nil
            for i, a in ipairs(areas) do
                if a.habitable then
                    local dis =
                        math.abs(a.x + traverse_radius - traverse_x) + math.abs(a.x + a.width - traverse_radius - traverse_x) - a.width +
                        math.abs(a.y + traverse_radius - traverse_y) + math.abs(a.y + a.height - traverse_radius - traverse_y) - a.height +
                        traverse_radius * 4
                    if min_dis == nil or min_dis > dis then
                        min_dis = dis
                        selected_area_ = i
                    end
                end
            end

            set_selected_area(selected_area_)
        end
        return selected_area_
    end

    local parent_stack = {}

    local function maintain_tablist()
        if tablist == nil then
            tablist = {}

            local active_area = selected_area()
            for _, tc in ipairs(screen.tiled_clients) do
                if not (tc.floating or tc.immobilized)
                then
                    if areas[active_area].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= areas[active_area].x + areas[active_area].width and
                        areas[active_area].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= areas[active_area].y + areas[active_area].height
                    then
                        tablist[#tablist + 1] = tc
                    end
                end
            end

            tablist_index = 1

        else

            local j = 0
            for i = 1, #tablist do
                if tablist[i].valid then
                    j = j + 1
                    tablist[j] = tablist[i]
                elseif i <= tablist_index and tablist_index > 0 then
                    tablist_index = tablist_index - 1
                end
            end

            for i = #tablist, j + 1, -1 do
                table.remove(tablist, i)
            end
        end

        if c and not c.valid then c = nil end
        if c == nil and #tablist > 0 then
            c = tablist[tablist_index]
        end
    end

    local function draw_info(context, cr, width, height)
        maintain_tablist()

        cr:set_source_rgba(0, 0, 0, 0)
        cr:rectangle(0, 0, width, height)
        cr:fill()

        local msg, ext
        local active_area = selected_area()
        for i, a in ipairs(areas) do
            if a.habitable or i == active_area then
                cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
                cr:clip()
                cr:set_source(fill_color)
                cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
                cr:fill()
                cr:set_source(i == active_area and border_color_hl or border_color)
                cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
                cr:set_line_width(10.0)
                cr:stroke()
                cr:reset_clip()
            end
        end

        if #tablist > 0 then
            local a = areas[active_area]
            local pl = lgi.Pango.Layout.create(cr)
            pl:set_font_description(tablist_font_desc)

            local vpadding = dpi(10)
            local list_height = vpadding
            local list_width = 2 * vpadding
            local exts = {}

            for index, tc in ipairs(tablist) do
                local label = tc.name or "<unnamed>"
                pl:set_text(label)
                local w, h
                w, h = pl:get_size()
                w = w / lgi.Pango.SCALE
                h = h / lgi.Pango.SCALE
                local ext = { width = w, height = h, x_bearing = 0, y_bearing = 0 }
                exts[#exts + 1] = ext
                list_height = list_height + ext.height + vpadding
                list_width = max(list_width, w + 2 * vpadding)
            end

            local x_offset = a.x + a.width / 2 - start_x
            local y_offset = a.y + a.height / 2 - list_height / 2 + vpadding - start_y

            -- cr:rectangle(a.x - start_x, y_offset - vpadding - start_y, a.width, list_height)
            -- cover the entire area
            cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
            cr:set_source(fill_color)
            cr:fill()

            cr:rectangle(a.x + (a.width - list_width) / 2 - start_x, a.y + (a.height - list_height) / 2 - start_y, list_width, list_height)
            cr:set_source(box_bg)
            cr:fill()

            for index, tc in ipairs(tablist) do
                local label = tc.name or "<unnamed>"
                local ext = exts[index]
                if index == tablist_index then
                    cr:rectangle(x_offset - ext.width / 2 - vpadding / 2, y_offset - vpadding / 2, ext.width + vpadding, ext.height + vpadding)
                    cr:set_source(fill_color_hl)
                    cr:fill()
                    pl:set_text(label)
                    cr:move_to(x_offset - ext.width / 2 - ext.x_bearing, y_offset - ext.y_bearing)
                    cr:set_source(font_color_hl)
                    cr:show_layout(pl)
                else
                    pl:set_text(label)
                    cr:move_to(x_offset - ext.width / 2 - ext.x_bearing, y_offset - ext.y_bearing)
                    cr:set_source(font_color)
                    cr:show_layout(pl)
                end

                y_offset = y_offset + ext.height + vpadding
            end
        end

        -- show the traverse point
        cr:rectangle(traverse_x - start_x - traverse_radius, traverse_y - start_y - traverse_radius, traverse_radius * 2, traverse_radius * 2)
        cr:set_source_rgba(1, 1, 1, 1)
        cr:fill()
    end

    infobox.bgimage = draw_info

    local key_translate_tab = {
        ["w"] = "Up",
        ["a"] = "Left",
        ["s"] = "Down",
        ["d"] = "Right",
    }

    awful.client.focus.history.disable_tracking()

    local kg
    local function exit()
        awful.client.focus.history.enable_tracking()
        if capi.client.focus then
            capi.client.emit_signal("focus", capi.client.focus)
        end
        infobox.visible = false
        awful.keygrabber.stop(kg)
    end

    local function handle_key(mod, key, event)
        if event == "release" then
            if exit_keys and exit_keys[key] then
                exit()
            end
            return
        end
        if key_translate_tab[key] ~= nil then
            key = key_translate_tab[key]
        end

        maintain_tablist()
        assert(tablist ~= nil)

        local shift = false
        local ctrl = false
        for i, m in ipairs(mod) do
            if m == "Shift" then shift = true
            elseif m == "Control" then ctrl = true
            end
        end

        if key == "Tab" then
            if #tablist > 0 then
                tablist_index = tablist_index % #tablist + 1
                c = tablist[tablist_index]
                c:emit_signal("request::activate", "mouse.move", {raise=false})
                c:raise()

                infobox.bgimage = draw_info
            end
        elseif key == "Up" or key == "Down" or key == "Left" or key == "Right" then
            local current_area = selected_area()

            if c and (shift or ctrl) then
                if shift then
                    if current_area == nil or
                        areas[current_area].x ~= c.x or
                        areas[current_area].y ~= c.y
                    then
                        traverse_x = c.x + traverse_radius
                        traverse_y = c.y + traverse_radius
                        set_selected_area(nil)
                    end
                elseif ctrl then
                    local ex = c.x + c.width + c.border_width * 2
                    local ey = c.y + c.height + c.border_width * 2
                    if current_area == nil or
                        areas[current_area].x + areas[current_area].width ~= ex or
                        areas[current_area].y + areas[current_area].height ~= ey
                    then
                        traverse_x = ex - traverse_radius
                        traverse_y = ey - traverse_radius
                        set_selected_area(nil)
                    end
                end
            end

            local choice = nil
            local choice_value

            current_area = selected_area()

            for i, a in ipairs(areas) do
                if not a.habitable then goto continue end

                local v
                if key == "Up" then
                    if a.x < traverse_x + threshold
                        and traverse_x < a.x + a.width + threshold then
                        v = traverse_y - a.y - a.height
                    else
                        v = -1
                    end
                elseif key == "Down" then
                    if a.x < traverse_x + threshold
                        and traverse_x < a.x + a.width + threshold then
                        v = a.y - traverse_y
                    else
                        v = -1
                    end
                elseif key == "Left" then
                    if a.y < traverse_y + threshold
                        and traverse_y < a.y + a.height + threshold then
                        v = traverse_x - a.x - a.width
                    else
                        v = -1
                    end
                elseif key == "Right" then
                    if a.y < traverse_y + threshold
                        and traverse_y < a.y + a.height + threshold then
                        v = a.x - traverse_x
                    else
                        v = -1
                    end
                end

                if (v > threshold) and (choice_value == nil or choice_value > v) then
                    choice = i
                    choice_value = v
                end
                ::continue::
            end

            if choice == nil then
                choice = current_area
                if key == "Up" then
                    traverse_y = screen.workarea.y
                elseif key == "Down" then
                    traverse_y = screen.workarea.y + screen.workarea.height
                elseif key == "Left" then
                    traverse_x = screen.workarea.x
                else
                    traverse_x = screen.workarea.x + screen.workarea.width
                end
            end

            if choice ~= nil then
                tablist = nil
                set_selected_area(choice)

                if c and ctrl and cd[c].draft ~= false then
                    local lu = cd[c].lu or cd[c].area
                    local rd = cd[c].rd or cd[c].area

                    if shift then
                        lu = choice
                        if areas[rd].x + areas[rd].width <= areas[lu].x or
                            areas[rd].y + areas[rd].height <= areas[lu].y
                        then
                            rd = nil
                        end
                    else
                        rd = choice
                        if areas[rd].x + areas[rd].width <= areas[lu].x or
                            areas[rd].y + areas[rd].height <= areas[lu].y
                        then
                            lu = nil
                        end
                    end

                    if lu ~= nil and rd ~= nil then
                        machi.layout.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
                    elseif lu ~= nil then
                        machi.layout.set_geometry(c, areas[lu], nil, 0, c.border_width)
                    elseif rd ~= nil then
                        c.x = min(c.x, areas[rd].x)
                        c.y = min(c.y, areas[rd].y)
                        machi.layout.set_geometry(c, nil, areas[rd], 0, c.border_width)
                    end

                    if lu == rd and cd[c].draft ~= true then
                        cd[c].lu = nil
                        cd[c].rd = nil
                        cd[c].area = lu
                    else
                        cd[c].lu = lu
                        cd[c].rd = rd
                        cd[c].area = nil
                    end

                    c:emit_signal("request::activate", "mouse.move", {raise=false})
                    c:raise()
                    awful.layout.arrange(screen)
                elseif c and shift then
                    -- move the window
                    local in_draft = cd[c].draft
                    if cd[c].draft ~= nil then
                        in_draft = cd[c].draft
                    elseif cd[c].lu then
                        in_draft = true
                    elseif cd[c].area then
                        in_draft = false
                    else
                        log(ERROR, "Assuming in_draft for unhandled client "..tostring(c))
                        in_draft = true
                    end
                    if in_draft then
                        c.x = areas[choice].x
                        c.y = areas[choice].y
                    else
                        machi.layout.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
                        cd[c].lu = nil
                        cd[c].rd = nil
                        cd[c].area = choice
                    end
                    c:emit_signal("request::activate", "mouse.move", {raise=false})
                    c:raise()
                    awful.layout.arrange(screen)

                    tablist = nil
                else
                    maintain_tablist()
                    -- move the focus
                    if #tablist > 0 and tablist[1] ~= c then
                        c = tablist[1]
                        capi.client.focus = c
                    end
                end

                infobox.bgimage = draw_info
            end
        elseif (key == "q" or key == "Prior") then
            local current_area = selected_area()
            if areas[current_area].parent_id == nil then
                return
            end

            tablist = nil
            set_selected_area(areas[current_area].parent_id)
            if #parent_stack == 0 or
                parent_stack[#parent_stack] ~= current_area then
                parent_stack = {current_area}
            end
            parent_stack[#parent_stack + 1] = areas[current_area].parent_id
            current_area = parent_stack[#parent_stack]

            if c and ctrl and cd[c].draft ~= false then
                if cd[c].area then
                    cd[c].lu, cd[c].rd, cd[c].area = cd[c].area, cd[c].area, nil
                end
                machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
                awful.layout.arrange(screen)
            end

            infobox.bgimage = draw_info
        elseif (key =="e" or key == "Next") then
            local current_area = selected_area()
            if #parent_stack <= 1 or parent_stack[#parent_stack] ~= current_area then
                return
            end

            tablist = nil
            set_selected_area(parent_stack[#parent_stack - 1])
            table.remove(parent_stack, #parent_stack)
            current_area = parent_stack[#parent_stack]

            if c and ctrl then
                if areas[current_area].habitable and cd[c].draft ~= true then
                    cd[c].lu, cd[c].rd, cd[c].area = nil, nil, current_area
                end
                machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
                awful.layout.arrange(screen)
            end

            infobox.bgimage = draw_info
        elseif key == "/" then
            local current_area = selected_area()
            local original_cmd = machi.engine.areas_to_command(areas, true, current_area)
            areas[current_area].hole = true
            local prefix, suffix = machi.engine.areas_to_command(
                areas, false):match("(.*)|(.*)")
            areas[current_area].hole = nil

            workarea = {
                x = areas[current_area].x - gap * 2,
                y = areas[current_area].y - gap * 2,
                width = areas[current_area].width + gap * 4,
                height = areas[current_area].height + gap * 4,
            }
            gtimer.delayed_call(
                function ()
                    layout.machi_editor.start_interactive(
                        screen,
                        {
                            workarea = workarea,
                            original_cmd = original_cmd,
                            cmd_prefix = prefix,
                            cmd_suffix = suffix,
                        }
                    )
                end
            )
            exit()
        elseif (key == "f" or key == ".") and c then
            if cd[c].draft == nil then
                cd[c].draft = true
            elseif cd[c].draft == true then
                cd[c].draft = false
            else
                cd[c].draft = nil
            end
            awful.layout.arrange(screen)
        elseif key == "Escape" or key == "Return" then
            exit()
        else
            log(DEBUG, "Unhandled key " .. key)
        end
    end

    kg = awful.keygrabber.run(
        function (...)
            ok, _ = pcall(handle_key, ...)
            if not ok then exit() end
        end
    )
end

return module
