-- ─────────────────────────────────────────────────────────────────────────────
-- Monitors
--
-- Kuycon P20 (matched by description): 4500x3000 @ 60Hz, 1.5x fractional scale.
-- 4500/1.5 = 3000 and 3000/1.5 = 2000, both integers, so this is a "clean"
-- fractional scale and Hyprland will not complain.
--
-- On HDR --------------------------------------------------------------------
-- GNOME runs this panel in bt2100/HDR. Hyprland CAN do that (cm = "hdr"), but
-- on 0.56.x it currently breaks screencopy: grim screenshots come back empty,
-- and OBS/Sunshine capture and hyprlock's blurred background go with them.
-- Root cause is still open upstream. Since screen capture matters more here
-- than an HDR desktop, the desktop runs SDR at 10-bit and HDR is handed to
-- fullscreen games only, via render.cm_auto_hdr in looks.lua.
--
-- To try full-time HDR anyway, set shipped_cm = "hdr" below (or pick HDR for
-- this display in Settings, which saves it as the display's colorProfile and
-- wins over the shipped value) and read the notes in overrides.lua first.
-- ─────────────────────────────────────────────────────────────────────────────

local prefs = require("prefs")

-- Per-output overrides written by Panama Settings, keyed by output name:
--   { ["DP-2"] = {
--       mode = "3840x2160@60", scale = 2, transform = 0,
--       x = 0, y = 0, primary = true,
--       vrrMode = -1, colorProfile = "auto", bitdepth = 10,
--       sdrBrightness = 1, sdrSaturation = 1, mirrorOf = "",
--   } }
--
-- The fields past `primary` are optional: records written before they existed
-- carry none of them, and the shipped values below stand instead. That is the
-- relationship in both directions -- what is written here is what a saved
-- record inherits, and what Settings saves is what overrides it, so the two
-- stop fighting over the same monitor rule.
--
-- Geometry and colour fail differently on purpose. A half-written position is
-- refused outright (below), because guessing one can strand an output where
-- nothing can reach it. An unreadable colour, VRR or mirror value is dropped
-- on its own and the shipped default stands: the worst it costs is a wrong
-- shade, and taking the whole record down with it would cost the arrangement.
local displays = prefs.get("displays", {})
if type(displays) ~= "table" then
    displays = {}
end

local function mode_dimensions(mode)
    if type(mode) ~= "string" then
        return nil, nil
    end
    local width, height, refresh = mode:match("^(%d+)x(%d+)@(%d+%.%d+)$")
    if width == nil then
        width, height, refresh = mode:match("^(%d+)x(%d+)@(%d+)$")
    end
    width, height, refresh = tonumber(width), tonumber(height), tonumber(refresh)
    if width == nil or height == nil or refresh == nil
        or width <= 0 or height <= 0 or refresh <= 0 then
        return nil, nil
    end
    return width, height
end

local function valid_mode(mode)
    local width = mode_dimensions(mode)
    return width ~= nil
end

local function valid_scale(mode, scale)
    local width, height = mode_dimensions(mode)
    if width == nil or type(scale) ~= "number" or scale ~= scale
        or scale <= 0 or scale > 4 then
        return false
    end
    local logical_width = width / scale
    local logical_height = height / scale
    return math.abs(logical_width - math.floor(logical_width + 0.5)) < 0.0001
        and math.abs(logical_height - math.floor(logical_height + 0.5)) < 0.0001
end

local function valid_transform(transform)
    return type(transform) == "number"
        and transform == math.floor(transform)
        and transform >= 0
        and transform <= 3
end

local function valid_coordinate(value)
    return type(value) == "number"
        and value == value
        and value == math.floor(value)
        and value >= -100000
        and value <= 100000
end

local function valid_position(entry)
    return valid_coordinate(entry.x) and valid_coordinate(entry.y)
end

local function valid_primary(entry)
    return type(entry.primary) == "boolean"
end

local function has_layout_fields(entry)
    return entry.x ~= nil or entry.y ~= nil or entry.primary ~= nil
end

local function display_entry(output)
    if type(output) ~= "string" or output == ""
        or output:match("^[%w_.-]+$") == nil then
        return nil
    end
    local entry = displays[output]
    if type(entry) ~= "table" then
        return nil
    end
    if not valid_mode(entry.mode)
        or not valid_scale(entry.mode, entry.scale)
        or not valid_transform(entry.transform) then
        return nil
    end
    -- Legacy records have none of the layout fields and keep automatic
    -- placement. A partially written extended record is unsafe: accepting its
    -- mode but guessing its position could overlap or strand another output.
    if has_layout_fields(entry)
        and (not valid_position(entry) or not valid_primary(entry)) then
        return nil
    end
    return entry
end

local function display_position(entry, fallback)
    if entry ~= nil and has_layout_fields(entry) then
        return string.format("%dx%d", entry.x, entry.y)
    end
    return fallback
end

local color_profiles = { auto = true, srgb = true, wide = true, hdr = true }

local function color_profile(entry, fallback)
    local value = entry ~= nil and entry.colorProfile or nil
    if type(value) == "string" and color_profiles[value] then
        return value
    end
    return fallback
end

local function bitdepth_value(entry, fallback)
    local value = entry ~= nil and entry.bitdepth or nil
    if value == 8 or value == 10 then
        return value
    end
    return fallback
end

-- -1 means the display follows the global misc.vrr policy, which is said by
-- leaving the key out. 3 is the global policy's own value and not a per-display
-- choice, so it is not accepted here either.
local function vrr_value(entry)
    local value = entry ~= nil and entry.vrrMode or nil
    if type(value) ~= "number" or value ~= math.floor(value)
        or value < 0 or value > 2 then
        return nil
    end
    return value
end

-- Neutral is 1.0, and the neutral value is left out rather than written: a rule
-- that names it pins the display to it, which is not the same as leaving the
-- trim alone.
local function sdr_value(value, minimum, maximum)
    if type(value) ~= "number" or value ~= value
        or value < minimum or value > maximum
        or math.abs(value - 1) < 0.001 then
        return nil
    end
    return value
end

-- A mirror needs a target that is not itself and not another mirror -- Hyprland
-- has no chain to follow -- and the primary may not mirror at all, since the
-- arrangement is anchored on it.
local function mirror_value(entry, output)
    local value = entry ~= nil and entry.mirrorOf or nil
    if type(value) ~= "string" or value == "" or value == output
        or value:match("^[%w_.-]+$") == nil
        or entry.primary == true then
        return nil
    end
    local target = displays[value]
    if type(target) == "table" and type(target.mirrorOf) == "string"
        and target.mirrorOf ~= "" then
        return nil
    end
    return value
end

-- Colour, VRR and mirroring layered onto a rule that already carries
-- mode/position/scale/transform. A monitor rule replaces the previous rule for
-- that output whole, so the shipped defaults are passed in here rather than
-- written in a rule of their own. `connector` is the output name the record was
-- saved under, which is not always the rule's own output: the Kuycon rule
-- matches by description.
local function with_display_fields(rule, entry, connector, default_bitdepth, default_cm)
    rule.bitdepth = bitdepth_value(entry, default_bitdepth)
    rule.cm = color_profile(entry, default_cm)
    rule.vrr = vrr_value(entry)
    rule.sdrbrightness = entry ~= nil and sdr_value(entry.sdrBrightness, 0.8, 2.0) or nil
    rule.sdrsaturation = entry ~= nil and sdr_value(entry.sdrSaturation, 0.8, 1.2) or nil

    -- A mirror shows its target's picture in its target's place, so the saved
    -- position is not the compositor's to honour or ours to ask for.
    local mirror = mirror_value(entry, connector)
    if mirror ~= nil then
        rule.mirror = mirror
        rule.position = "auto"
    end
    return rule
end

-- Every connected output uses the same validated per-output store. Automatic
-- placement and the compositor's normal color policy unless the entry says
-- otherwise.
for output, _ in pairs(displays) do
    local entry = display_entry(output)
    if entry ~= nil then
        hl.monitor(with_display_fields({
            output = output,
            mode = entry.mode,
            position = display_position(entry, "auto"),
            scale = entry.scale,
            transform = entry.transform,
        }, entry, output, nil, nil))
    end
end

-- The Kuycon P20, matched by what it is rather than where it is plugged in.
-- This used to be a rule for connector DP-2 outright, which handed the panel's
-- 4500x3000 mode and 1.5 scale to whatever monitor a stranger's machine had on
-- its most common DisplayPort connector. Emitted after the prefs loop so a
-- saved entry for its connector still carries the mode/scale/position, while
-- this rule holds the shipped defaults and the panel-specific color policy.
local shipped_mode = "4500x3000@60"
local shipped_scale = 1.5
local shipped_transform = 0

-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
-- HBR3, so this relies on DSC. If the display fails to light up or falls back to
-- a lower mode, drop this to 8 first.
local shipped_bitdepth = 10

-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
local shipped_cm = "auto"

local kuycon = display_entry("DP-2")
hl.monitor(with_display_fields({
    output   = "desc:GVT Kuycon P20",
    mode     = kuycon and kuycon.mode or shipped_mode,
    position = display_position(kuycon, "0x0"),
    scale    = kuycon and kuycon.scale or shipped_scale,
    transform = kuycon and kuycon.transform or shipped_transform,
}, kuycon, "DP-2", shipped_bitdepth, shipped_cm))

-- Any monitor not named above: sane defaults rather than nothing.
hl.monitor({
    output   = "",
    mode     = "preferred",
    position = "auto",
    scale    = "auto",
})

-- ── Workspaces on the primary display only ──────────────────────────────────
--
-- GNOME offered one workspace choice worth reproducing: whether the other
-- screens join in. Off, every monitor has its own workspaces and switching
-- affects whichever one has focus -- Hyprland's own behaviour, so it needs no
-- rules at all. On, workspaces 1-10 are pinned to the primary display and a
-- second screen keeps a workspace of its own that stays put.
--
-- Ten because that is how many the keybinds reach: ALT+1 through ALT+0 in
-- keybinds.lua. Binding more would pin workspaces nothing can navigate to, and
-- binding fewer would leave the last few behaving differently from the rest for
-- no reason a person could see.
--
-- The rules are emitted here rather than written live because Hyprland reads
-- them at config time and offers no way to remove one afterwards: writing an
-- empty monitor leaves the previous binding in place. So the config is the only
-- honest source, and applying a change is a reload.
if prefs.get("workspacesOnPrimaryOnly", false) == true then
    -- Only a record display_entry accepts counts. A half-written entry is one
    -- the monitor rules above already refuse, so pinning ten workspaces to it on
    -- the strength of a `primary` flag nothing else trusts would put them on a
    -- screen that never got a rule of its own.
    local primaries = {}
    for output, _ in pairs(displays) do
        local entry = display_entry(output)
        if entry ~= nil and entry.primary == true then
            primaries[#primaries + 1] = output
        end
    end

    -- Without a primary there is nothing to pin to, and guessing one would move
    -- every workspace onto whichever screen happened to sort first. Two records
    -- both claiming primary is the same problem wearing a different hat: pairs()
    -- has no order, so picking one of them would pin the workspaces to a
    -- different screen from one reload to the next. Neither case guesses.
    if #primaries == 1 then
        local primary = primaries[1]
        for i = 1, 10 do
            hl.workspace_rule({ workspace = tostring(i), monitor = primary })
        end
    end
end

return true
