local U = {}

function U.trim(s)
  return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end

-- Reads a { ... } group, handling nesting and \{ \} escapes.
-- `open` points at the opening brace; returns the inner content and the
-- position just after the closing brace.
function U.read_group(s, open)
  local depth, i, n = 0, open, #s
  while i <= n do
    local c = s:sub(i, i)
    if c == "\\" then
      i = i + 2
    elseif c == "{" then
      depth = depth + 1; i = i + 1
    elseif c == "}" then
      depth = depth - 1
      if depth == 0 then return s:sub(open + 1, i - 1), i + 1 end
      i = i + 1
    else
      i = i + 1
    end
  end
  error("scholatex: missing closing brace from position " .. open)
end

-- Raw { minus } balance on a line (escapes ignored).
-- Used to track depth inside for/if/while bodies.
function U.raw_brace_delta(line)
  local delta, i, n = 0, 1, #line
  while i <= n do
    local c = line:sub(i, i)
    if c == "\\" then i = i + 2
    else
      if c == "{" then delta = delta + 1
      elseif c == "}" then delta = delta - 1 end
      i = i + 1
    end
  end
  return delta
end

-- Splits a top-level comma-separated list, honouring nested {} [] and
-- escapes. Used by the for-in-list loop.
function U.split_commas(s)
  local items, depth, start, i, n = {}, 0, 1, 1, #s
  while i <= n do
    local c = s:sub(i, i)
    if c == "\\" then i = i + 2
    else
      if c == "{" or c == "[" then depth = depth + 1
      elseif c == "}" or c == "]" then depth = depth - 1
      elseif c == "," and depth == 0 then
        items[#items + 1] = U.trim(s:sub(start, i - 1))
        start = i + 1
      end
      i = i + 1
    end
  end
  items[#items + 1] = U.trim(s:sub(start))
  return items
end

-- Splits text into paragraphs at top-level newlines, i.e. newlines that
-- are not inside a { } group or $ $ math span. Escapes are honoured.
-- A newline nested in a sub-group stays within its paragraph.
function U.split_top_newlines(s)
  local paras, depth, inmath, start, i, n = {}, 0, false, 1, 1, #s
  while i <= n do
    local c = s:sub(i, i)
    if c == "\\" then
      i = i + 2
    elseif c == "$" then
      inmath = not inmath; i = i + 1
    elseif c == "{" and not inmath then
      depth = depth + 1; i = i + 1
    elseif c == "}" and not inmath then
      depth = depth - 1; i = i + 1
    elseif c == "\n" and depth == 0 and not inmath then
      paras[#paras + 1] = s:sub(start, i - 1)
      start = i + 1; i = i + 1
    else
      i = i + 1
    end
  end
  paras[#paras + 1] = s:sub(start)
  return paras
end

local PLACE_V = {t = "top", m = "center", b = "bottom"}
local PLACE_H = {l = "left", c = "center", r = "right"}

function U.place_code(w)
  if type(w) ~= "string" or #w ~= 2 then return nil end
  local v, h = PLACE_V[w:sub(1, 1)], PLACE_H[w:sub(2, 2)]
  if v and h then return v, h end
  return nil
end

function U.split_opts(s)
  local toks, i, n = {}, 1, #s
  while i <= n do
    while i <= n and s:sub(i, i):match("%s") do i = i + 1 end
    if i > n then break end
    local start = i
    while i <= n and not s:sub(i, i):match("%s") do
      if s:sub(i, i) == "{" then
        local _, after = U.read_group(s, i)
        i = after
      else
        i = i + 1
      end
    end
    toks[#toks + 1] = s:sub(start, i - 1)
  end
  return toks
end

return U
