---@class snacks.zen
---@overload fun(opts: snacks.zen.Config): snacks.win
local M = setmetatable({}, {
  __call = function(M, ...)
    return M.zen(...)
  end,
})

M.meta = {
  desc = "Zen mode • distraction-free coding",
}

---@class snacks.zen.Config
local defaults = {
  -- You can add any `Snacks.toggle` id here.
  -- Toggle state is restored when the window is closed.
  -- Toggle config options are NOT merged.
  ---@type table<string, boolean>
  toggles = {
    dim = true,
    git_signs = false,
    mini_diff_signs = false,
    -- diagnostics = false,
    -- inlay_hints = false,
  },
  show = {
    statusline = false, -- can only be shown when using the global statusline
    tabline = false,
  },
  ---@type snacks.win.Config
  win = { style = "zen" },
  --- Callback when the window is opened.
  ---@param win snacks.win
  on_open = function(win) end,
  --- Callback when the window is closed.
  ---@param win snacks.win
  on_close = function(win) end,
  --- Options for the `Snacks.zen.zoom()`
  ---@type snacks.zen.Config
  zoom = {
    toggles = {},
    show = { statusline = true, tabline = true },
    win = {
      backdrop = false,
      width = 0, -- full width
    },
  },
}

Snacks.config.style("zen", {
  enter = true,
  fixbuf = false,
  minimal = false,
  width = 120,
  height = 0,
  backdrop = { transparent = true, blend = 40 },
  keys = { q = false },
  zindex = 40,
  wo = {
    winhighlight = "NormalFloat:Normal",
  },
  w = {
    snacks_main = true,
  },
})

-- fullscreen indicator
-- only shown when the window is maximized
Snacks.config.style("zoom_indicator", {
  text = "▍ zoom  󰊓  ",
  minimal = true,
  enter = false,
  focusable = false,
  height = 1,
  row = 0,
  col = -1,
  backdrop = false,
})

Snacks.util.set_hl({
  Icon = "DiagnosticWarn",
}, { prefix = "SnacksZen", default = true })

---@param opts? {statusline: boolean, tabline: boolean}
local function get_main(opts)
  opts = opts or {}
  local bottom = vim.o.cmdheight + (opts.statusline and vim.o.laststatus == 3 and 1 or 0)
  local top = opts.tabline
      and ((vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)) and 1 or 0)
    or 0
  ---@class snacks.zen.Main values are 0-indexed
  local ret = {
    width = vim.o.columns,
    row = top,
    height = vim.o.lines - top - bottom,
  }
  return ret
end

M.win = nil ---@type snacks.win?

---@param opts? snacks.zen.Config
function M.zen(opts)
  local toggles = opts and opts.toggles
  opts = Snacks.config.get("zen", defaults, opts)
  opts.toggles = toggles or opts.toggles

  -- close if already open
  if M.win and M.win:valid() then
    M.win:close()
    M.win = nil
    return
  end

  local parent_win = vim.api.nvim_get_current_win()
  local parent_zindex = vim.api.nvim_win_get_config(parent_win).zindex

  local buf = vim.api.nvim_get_current_buf()
  local win_opts = Snacks.win.resolve({ style = "zen" }, opts.win, { buf = buf })
  win_opts.zindex = parent_zindex and parent_zindex + 1 or win_opts.zindex

  if Snacks.util.is_transparent() and type(win_opts.backdrop) == "table" then
    win_opts.backdrop.transparent = false
  end

  local zoom_indicator ---@type snacks.win?
  local show_indicator = false

  -- calculate window size
  if win_opts.height == 0 and (opts.show.statusline or opts.show.tabline or vim.o.cmdheight > 0) then
    local main = get_main(opts.show)
    win_opts.row = main.row
    win_opts.height = function()
      return get_main(opts.show).height
    end
    if type(win_opts.backdrop) == "table" then
      win_opts.backdrop.win = win_opts.backdrop.win or {}
      win_opts.backdrop.win.row = win_opts.row
      win_opts.backdrop.win.height = win_opts.height
    end
    if win_opts.width == 0 then
      show_indicator = true
    end
  end

  -- create window
  local win = Snacks.win(win_opts)
  vim.cmd([[norm! zz]])
  M.win = win

  if show_indicator then
    zoom_indicator = Snacks.win({
      show = false,
      style = "zoom_indicator",
      zindex = win.opts.zindex + 1,
      wo = { winhighlight = "NormalFloat:SnacksZenIcon" },
    })
    zoom_indicator:open_buf()
    local lines = vim.api.nvim_buf_get_lines(zoom_indicator.buf, 0, -1, false)
    zoom_indicator.opts.width = vim.api.nvim_strwidth(lines[1] or "")
    zoom_indicator:show()
  end

  -- set toggle states
  ---@type {toggle: snacks.toggle.Class, state: unknown}[]
  local states = {}
  for id, state in pairs(opts.toggles) do
    local toggle = Snacks.toggle.get(id)
    if toggle then
      table.insert(states, { toggle = toggle, state = toggle:get() })
      toggle:set(state)
    end
  end
  opts.on_open(win)

  -- sync cursor with the parent window
  vim.api.nvim_create_autocmd("CursorMoved", {
    group = win.augroup,
    callback = function()
      if win:win_valid() and vim.api.nvim_win_is_valid(parent_win) then
        vim.api.nvim_win_set_cursor(parent_win, vim.api.nvim_win_get_cursor(win.win))
      end
    end,
  })

  -- restore toggle states when window is closed
  win:on("WinClosed", function()
    if zoom_indicator then
      zoom_indicator:close()
    end
    for _, state in ipairs(states) do
      state.toggle:set(state.state)
    end
    opts.on_close(win)
  end, { win = true })

  -- update the buffer of the parent window
  -- when the zen buffer changes
  win:on("BufWinEnter", function()
    vim.api.nvim_win_set_buf(parent_win, win.buf)
  end)

  -- close when entering another window
  win:on("WinEnter", function()
    local w = vim.api.nvim_get_current_win()
    if w == win.win then
      return
    end
    -- exit if other window is not a floating window
    if vim.api.nvim_win_get_config(w).relative == "" then
      -- schedule so that WinClosed is properly triggered
      vim.schedule(function()
        win:close()
      end)
    end
  end)
  return win
end

---@param opts? snacks.zen.Config
function M.zoom(opts)
  opts = Snacks.config.get("zen", defaults, opts)
  return M.zen(opts and opts.zoom or nil)
end

return M
