hl.config({
  master = {
    orientation = "left",
    mfact = 0.60,
    new_status = "slave",
    new_on_top = true,
    new_on_active = "after",
    allow_small_split = true,
    special_scale_factor = 0.8,
    drop_at_cursor = true
  },
  scrolling = {
    fullscreen_on_one_column = true,
    focus_fit_method = 0,
    follow_focus = true,
    follow_min_visible = 0.1,
    column_width = 0.7
  }
})

-- ─── Scrolling slave column helper ───────────────────────────────────────────
-- Shared by all master-scroll variants.
-- state        = { visible, peek, offset, peek_top_addr, peek_bottom_addr }
-- slave_area   = HL.Box for this column
-- targets      = ctx.targets
-- slave_indices = ordered list of indices into `targets` for this column
local function place_scroll_col(state, slave_area, targets, slave_indices)
  local n = #slave_indices
  state.peek_top_addr    = nil
  state.peek_bottom_addr = nil
  if n == 0 then return end

  local max_off = math.max(0, n - state.visible)
  state.offset  = math.max(0, math.min(state.offset, max_off))

  if n <= state.visible then
    local h = slave_area.h / n
    for j = 1, n do
      targets[slave_indices[j]]:place({
        x = slave_area.x, y = slave_area.y + (j - 1) * h,
        w = slave_area.w, h = h,
      })
    end
    return
  end

  local has_top = state.offset > 0
  local has_bot = state.offset < max_off
  local top_f   = has_top and state.peek or 0
  local bot_f   = has_bot and state.peek or 0
  -- h chosen so top_peek + visible*h + bot_peek == slave_area.h exactly
  local h       = slave_area.h / (state.visible + top_f + bot_f)

  if has_top then
    local w = targets[slave_indices[state.offset]].window
    if w then state.peek_top_addr = w.address end
  end
  if has_bot then
    local ti = slave_indices[state.offset + state.visible + 1]
    if ti then
      local w = targets[ti].window
      if w then state.peek_bottom_addr = w.address end
    end
  end

  for j = 1, n do
    local t = targets[slave_indices[j]]
    if has_top and j == state.offset then
      -- Peek at top: extends above slave_area; safe because master is in a different x-range
      t:place({ x=slave_area.x, y=slave_area.y - h*(1-state.peek), w=slave_area.w, h=h })
    elseif has_bot and j == state.offset + state.visible + 1 then
      -- Peek at bottom: extends below the last visible slot
      t:place({ x=slave_area.x, y=slave_area.y + (top_f + state.visible)*h, w=slave_area.w, h=h })
    elseif j >= state.offset + 1 and j <= state.offset + state.visible then
      local k = j - state.offset - 1
      t:place({ x=slave_area.x, y=slave_area.y + (top_f + k)*h, w=slave_area.w, h=h })
    else
      -- Fully off-screen: park below work area
      t:set_box({ x=slave_area.x, y=slave_area.y + slave_area.h + h, w=slave_area.w, h=h })
    end
  end
end

-- ─── Layout states ────────────────────────────────────────────────────────────
local shared = require("state")  -- shared ws_layouts table (written by keybinds.lua)

local mfact   = 0.60  -- master width fraction (horizontal single-master variants)
local mfact_v = 0.55  -- master height fraction (vertical variants)

-- mfact setters: clamp and write back to the shared upvalue.
local function set_mfact(v)   if v then mfact   = math.max(0.1, math.min(0.95, v)) end end
local function set_mfact_v(v) if v then mfact_v = math.max(0.1, math.min(0.95, v)) end end

-- layout_msg builder for single-column/row scroll layouts (master + N slaves).
local function scroll_msg(s, set_mf)
  return function(ctx, msg)
    local max_off = math.max(0, #ctx.targets - 1 - s.visible)
    if msg == "scrolldown"  then s.offset = math.min(s.offset + 1, max_off); return true
    elseif msg == "scrollup" then s.offset = math.max(s.offset - 1, 0);      return true
    elseif msg == "reset"    then s.offset = 0;                              return true
    else
      local v = msg:match("^mfact exact (.+)$")
      if v then set_mf(tonumber(v)); return true end
    end
  end
end

-- layout_msg builder for swap-on-focus layouts (recalc + mfact only).
local function swap_msg(set_mf)
  return function(_, msg)
    if msg == "recalc" then return true end
    local v = msg:match("^mfact exact (.+)$")
    if v then set_mf(tonumber(v)); return true end
  end
end

local ms = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local sm = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }

local cm = { side_w=0.20, visible=2, peek=0.10 }
local cm_left  = { visible=cm.visible, peek=cm.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local cm_right = { visible=cm.visible, peek=cm.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local cm_left_addrs  = {}
local cm_right_addrs = {}

-- ─── master-scroll: master left, slaves right ─────────────────────────────────
hl.layout.register("master-scroll", {
  recalculate = function(ctx)
    local targets = ctx.targets
    local n = #targets
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end

    local slave_area  = ctx:split(ctx.area, "right", 1.0 - mfact)
    local master_area = { x=ctx.area.x, y=ctx.area.y, w=slave_area.x-ctx.area.x, h=ctx.area.h }
    targets[1]:place(master_area)

    local idx = {}
    for i = 2, n do idx[#idx+1] = i end
    place_scroll_col(ms, slave_area, targets, idx)
  end,

  layout_msg = scroll_msg(ms, set_mfact),
})

-- ─── slave-master-scroll: slaves left, master right ──────────────────────────
hl.layout.register("slave-master-scroll", {
  recalculate = function(ctx)
    local targets = ctx.targets
    local n = #targets
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end

    local slave_area  = ctx:split(ctx.area, "left", 1.0 - mfact)
    local master_area = {
      x = slave_area.x + slave_area.w, y = ctx.area.y,
      w = ctx.area.w - slave_area.w,   h = ctx.area.h,
    }
    targets[1]:place(master_area)

    local idx = {}
    for i = 2, n do idx[#idx+1] = i end
    place_scroll_col(sm, slave_area, targets, idx)
  end,

  layout_msg = scroll_msg(sm, set_mfact),
})

-- ─── center-master-scroll: center master, both columns scroll ─────────────────
-- Slaves alternate: odd (1,3,5…) → left column, even (2,4,6…) → right column.
-- scrolldown/scrollup applies to whichever column the active window is in.
hl.layout.register("center-master-scroll", {
  recalculate = function(ctx)
    local targets = ctx.targets
    local n = #targets
    cm_left_addrs  = {}
    cm_right_addrs = {}
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end

    local left_area  = ctx:split(ctx.area, "left",  cm.side_w)
    local right_area = ctx:split(ctx.area, "right", cm.side_w)
    local master_area = {
      x = left_area.x + left_area.w, y = ctx.area.y,
      w = right_area.x - (left_area.x + left_area.w), h = ctx.area.h,
    }
    targets[1]:place(master_area)

    local left_idx, right_idx = {}, {}
    for i = 2, n do
      local slave_pos = i - 1  -- 1-indexed slave number
      if slave_pos % 2 == 1 then
        left_idx[#left_idx+1] = i
      else
        right_idx[#right_idx+1] = i
      end
      local w = targets[i].window
      if w then
        if slave_pos % 2 == 1 then cm_left_addrs[w.address]  = true
        else                       cm_right_addrs[w.address] = true
        end
      end
    end

    place_scroll_col(cm_left,  left_area,  targets, left_idx)
    place_scroll_col(cm_right, right_area, targets, right_idx)
  end,

  layout_msg = function(ctx, msg)
    local aw   = hl.get_active_window()
    local addr = aw and aw.address

    local function scroll_col(state, delta)
      -- Count windows in this column from targets
      local col_n = 0
      for i = 2, #ctx.targets do
        local w = ctx.targets[i].window
        if w and ((state == cm_left and cm_left_addrs[w.address])
               or (state == cm_right and cm_right_addrs[w.address])) then
          col_n = col_n + 1
        end
      end
      local col_max = math.max(0, col_n - state.visible)
      if delta > 0 then state.offset = math.min(state.offset + 1, col_max)
      else              state.offset = math.max(state.offset - 1, 0)
      end
      return true
    end

    if msg == "scrolldown" then
      if addr and cm_left_addrs[addr]  then return scroll_col(cm_left,  1) end
      if addr and cm_right_addrs[addr] then return scroll_col(cm_right, 1) end
    elseif msg == "scrollup" then
      if addr and cm_left_addrs[addr]  then return scroll_col(cm_left,  -1) end
      if addr and cm_right_addrs[addr] then return scroll_col(cm_right, -1) end
    elseif msg == "reset" then
      cm_left.offset = 0; cm_right.offset = 0; return true
    else
      local v = msg:match("^mfact exact (.+)$")
      if v then set_mfact(tonumber(v)); return true end
    end
  end,
})

-- ─── Scrolling slave row helper (horizontal) ─────────────────────────────────
-- Mirror of place_scroll_col but scrolls left↔right instead of up↔down.
-- state fields: visible, peek, offset, peek_top_addr (left peek), peek_bottom_addr (right peek)
local function place_scroll_row(state, slave_area, targets, slave_indices)
  local n = #slave_indices
  state.peek_top_addr    = nil
  state.peek_bottom_addr = nil
  if n == 0 then return end

  local max_off = math.max(0, n - state.visible)
  state.offset  = math.max(0, math.min(state.offset, max_off))

  if n <= state.visible then
    local w = slave_area.w / n
    for j = 1, n do
      targets[slave_indices[j]]:place({
        x = slave_area.x + (j - 1) * w, y = slave_area.y,
        w = w,                           h = slave_area.h,
      })
    end
    return
  end

  local has_left = state.offset > 0
  local has_right = state.offset < max_off
  local left_f   = has_left  and state.peek or 0
  local right_f  = has_right and state.peek or 0
  -- w chosen so left_peek + visible*w + right_peek == slave_area.w exactly
  local w        = slave_area.w / (state.visible + left_f + right_f)

  if has_left then
    local wi = targets[slave_indices[state.offset]].window
    if wi then state.peek_top_addr = wi.address end
  end
  if has_right then
    local ti = slave_indices[state.offset + state.visible + 1]
    if ti then
      local wi = targets[ti].window
      if wi then state.peek_bottom_addr = wi.address end
    end
  end

  for j = 1, n do
    local t = targets[slave_indices[j]]
    if has_left and j == state.offset then
      t:place({ x=slave_area.x - w*(1-state.peek), y=slave_area.y, w=w, h=slave_area.h })
    elseif has_right and j == state.offset + state.visible + 1 then
      t:place({ x=slave_area.x + (left_f + state.visible)*w, y=slave_area.y, w=w, h=slave_area.h })
    elseif j >= state.offset + 1 and j <= state.offset + state.visible then
      local k = j - state.offset - 1
      t:place({ x=slave_area.x + (left_f + k)*w, y=slave_area.y, w=w, h=slave_area.h })
    else
      t:set_box({ x=slave_area.x + slave_area.w + w, y=slave_area.y, w=w, h=slave_area.h })
    end
  end
end

-- ─── Vertical layout states ───────────────────────────────────────────────────
local tm = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }

local cm_v = { side_h=0.20, visible=2, peek=0.10 }
local cm_vtop = { visible=cm_v.visible, peek=cm_v.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local cm_vbot = { visible=cm_v.visible, peek=cm_v.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local cm_vtop_addrs = {}
local cm_vbot_addrs = {}

-- ─── top-master-scroll: master top, slaves bottom row ────────────────────────
hl.layout.register("top-master-scroll", {
  recalculate = function(ctx)
    local targets = ctx.targets
    local n = #targets
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end

    local slave_area  = ctx:split(ctx.area, "bottom", 1.0 - mfact_v)
    local master_area = { x=ctx.area.x, y=ctx.area.y, w=ctx.area.w, h=slave_area.y-ctx.area.y }
    targets[1]:place(master_area)

    local idx = {}
    for i = 2, n do idx[#idx+1] = i end
    place_scroll_row(tm, slave_area, targets, idx)
  end,

  layout_msg = scroll_msg(tm, set_mfact_v),
})

-- ─── center-master-scroll-v: center master, top and bottom slave rows ────────
-- Slaves alternate: odd (1,3,5…) → top row, even (2,4,6…) → bottom row.
-- scrolldown/scrollup scrolls whichever row the active window is in.
hl.layout.register("center-master-scroll-v", {
  recalculate = function(ctx)
    local targets = ctx.targets
    local n = #targets
    cm_vtop_addrs = {}
    cm_vbot_addrs = {}
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end

    local top_area = ctx:split(ctx.area, "top",    cm_v.side_h)
    local bot_area = ctx:split(ctx.area, "bottom", cm_v.side_h)
    local master_area = {
      x = ctx.area.x, y = top_area.y + top_area.h,
      w = ctx.area.w, h = bot_area.y - (top_area.y + top_area.h),
    }
    targets[1]:place(master_area)

    local top_idx, bot_idx = {}, {}
    for i = 2, n do
      local slave_pos = i - 1
      if slave_pos % 2 == 1 then
        top_idx[#top_idx+1] = i
      else
        bot_idx[#bot_idx+1] = i
      end
      local w = targets[i].window
      if w then
        if slave_pos % 2 == 1 then cm_vtop_addrs[w.address] = true
        else                       cm_vbot_addrs[w.address] = true
        end
      end
    end

    place_scroll_row(cm_vtop, top_area, targets, top_idx)
    place_scroll_row(cm_vbot, bot_area, targets, bot_idx)
  end,

  layout_msg = function(ctx, msg)
    local aw   = hl.get_active_window()
    local addr = aw and aw.address

    local function scroll_row(state, col_addrs, delta)
      local col_n = 0
      for i = 2, #ctx.targets do
        local w = ctx.targets[i].window
        if w and col_addrs[w.address] then col_n = col_n + 1 end
      end
      local col_max = math.max(0, col_n - state.visible)
      if delta > 0 then state.offset = math.min(state.offset + 1, col_max)
      else              state.offset = math.max(state.offset - 1, 0)
      end
      return true
    end

    if msg == "scrolldown" then
      if addr and cm_vtop_addrs[addr] then return scroll_row(cm_vtop, cm_vtop_addrs, 1) end
      if addr and cm_vbot_addrs[addr] then return scroll_row(cm_vbot, cm_vbot_addrs, 1) end
    elseif msg == "scrollup" then
      if addr and cm_vtop_addrs[addr] then return scroll_row(cm_vtop, cm_vtop_addrs, -1) end
      if addr and cm_vbot_addrs[addr] then return scroll_row(cm_vbot, cm_vbot_addrs, -1) end
    elseif msg == "reset" then
      cm_vtop.offset = 0; cm_vbot.offset = 0; return true
    else
      local v = msg:match("^mfact exact (.+)$")
      if v then set_mfact_v(tonumber(v)); return true end
    end
  end,
})

-- ─── Swap-on-focus layouts ────────────────────────────────────────────────────
-- Focusing any window promotes it to master. recalculate reads the active
-- window directly via hl.get_active_window() — no external address tracking.

local function swap_master_idx(targets)
  local aw = hl.get_active_window()
  if aw and aw.address then
    for i = 1, #targets do
      local w = targets[i].window
      if w and w.address == aw.address then return i end
    end
  end
  return 1
end

local function swap_place_slaves(targets, midx, slave_area, vertical)
  local sidx = {}
  for i = 1, #targets do if i ~= midx then sidx[#sidx+1] = i end end
  if #sidx == 0 then return end
  if vertical then
    local h = slave_area.h / #sidx
    for j, si in ipairs(sidx) do
      targets[si]:place({ x=slave_area.x, y=slave_area.y+(j-1)*h, w=slave_area.w, h=h })
    end
  else
    local w = slave_area.w / #sidx
    for j, si in ipairs(sidx) do
      targets[si]:place({ x=slave_area.x+(j-1)*w, y=slave_area.y, w=w, h=slave_area.h })
    end
  end
end

-- ─── master-swap: master left, slaves right ───────────────────────────────────
hl.layout.register("master-swap", {
  recalculate = function(ctx)
    local targets, n = ctx.targets, #ctx.targets
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end
    local midx       = swap_master_idx(targets)
    local slave_area = ctx:split(ctx.area, "right", 1.0 - mfact)
    local master_area = { x=ctx.area.x, y=ctx.area.y, w=slave_area.x-ctx.area.x, h=ctx.area.h }
    targets[midx]:place(master_area)
    swap_place_slaves(targets, midx, slave_area, true)
  end,
  layout_msg = swap_msg(set_mfact),
})

-- ─── slave-master-swap: slaves left, master right ─────────────────────────────
hl.layout.register("slave-master-swap", {
  recalculate = function(ctx)
    local targets, n = ctx.targets, #ctx.targets
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end
    local midx       = swap_master_idx(targets)
    local slave_area = ctx:split(ctx.area, "left", 1.0 - mfact)
    local master_area = { x=slave_area.x+slave_area.w, y=ctx.area.y, w=ctx.area.w-slave_area.w, h=ctx.area.h }
    targets[midx]:place(master_area)
    swap_place_slaves(targets, midx, slave_area, true)
  end,
  layout_msg = swap_msg(set_mfact),
})

-- ─── top-master-swap: master top, slaves bottom row ───────────────────────────
hl.layout.register("top-master-swap", {
  recalculate = function(ctx)
    local targets, n = ctx.targets, #ctx.targets
    if n == 0 then return end
    if n == 1 then targets[1]:place(ctx.area); return end
    local midx       = swap_master_idx(targets)
    local slave_area = ctx:split(ctx.area, "bottom", 1.0 - mfact_v)
    local master_area = { x=ctx.area.x, y=ctx.area.y, w=ctx.area.w, h=slave_area.y-ctx.area.y }
    targets[midx]:place(master_area)
    swap_place_slaves(targets, midx, slave_area, false)
  end,
  layout_msg = swap_msg(set_mfact_v),
})

-- ─── Auto-scroll on focus + swap-on-focus ────────────────────────────────────
local all_col_states = { ms, sm, cm_left, cm_right, tm, cm_vtop, cm_vbot }

hl.on("window.active", function(w)
  if w == nil or w.address == nil then return end

  -- Auto-scroll: if the focused window is a peek slot, slide it into view
  for _, state in ipairs(all_col_states) do
    if w.address == state.peek_bottom_addr then
      hl.dispatch(hl.dsp.layout("scrolldown")); return
    elseif w.address == state.peek_top_addr then
      hl.dispatch(hl.dsp.layout("scrollup")); return
    end
  end

  -- Trigger recalc for swap layouts so the focused window becomes master.
  -- shared.ws_layouts is written by keybinds.lua and tracks per-workspace layout.
  local ws_id = w.workspace and w.workspace.id
  local cur   = ws_id and shared.ws_layouts[ws_id]
  if cur == "lua:master-swap" or cur == "lua:slave-master-swap" or cur == "lua:top-master-swap" then
    hl.dispatch(hl.dsp.layout("recalc"))
  end
end)
