local U = require("texecole-util")

local M = {}

local function fmt(v)
  return (("%.4f"):format(v)):gsub("0+$", ""):gsub("%.$", "")
end

local function projector(attrs)
  local alpha = math.rad(tonumber(attrs.angle or "") or 45)
  local k     = tonumber(attrs.ratio or "") or 0.5
  local c, s  = k * math.cos(alpha), k * math.sin(alpha)
  return function(p) return p[1] + c * p[2], p[3] + s * p[2] end
end

local function shoelace(pts)
  local a = 0
  for i = 1, #pts do
    local p, q = pts[i], pts[i % #pts + 1]
    a = a + p[1] * q[2] - q[1] * p[2]
  end
  return a / 2
end

local function poly_render(verts, faces, names, proj, ox)

  local P = {}
  for i, v in ipairs(verts) do
    local x, y = proj(v)
    P[i] = { x + ox, y }
  end

  local vis = {}
  for fi, f in ipairs(faces) do
    local pts = {}
    for _, vi in ipairs(f) do pts[#pts+1] = P[vi] end
    vis[fi] = shoelace(pts) > 1e-9
  end

  local edges = {}
  local function ekey(i, j) if i > j then i, j = j, i end return i .. "-" .. j end
  for fi, f in ipairs(faces) do
    for k = 1, #f do
      local i, j = f[k], f[k % #f + 1]
      local key = ekey(i, j)
      local e = edges[key]
      if not e then e = { i = math.min(i,j), j = math.max(i,j), faces = {} }; edges[key] = e end
      e.faces[#e.faces+1] = fi
    end
  end

  local out, xmin, xmax = {}, math.huge, -math.huge
  for _, p in ipairs(P) do
    if p[1] < xmin then xmin = p[1] end
    if p[1] > xmax then xmax = p[1] end
  end

  local solidseg, dashseg = {}, {}
  for _, e in pairs(edges) do
    local anyvis = false
    for _, fi in ipairs(e.faces) do if vis[fi] then anyvis = true end end
    local seg = ("(%s,%s) -- (%s,%s)"):format(
      fmt(P[e.i][1]), fmt(P[e.i][2]), fmt(P[e.j][1]), fmt(P[e.j][2]))
    if anyvis then solidseg[#solidseg+1] = seg else dashseg[#dashseg+1] = seg end
  end
  for _, s in ipairs(dashseg)  do out[#out+1] = "\\draw[dashed] " .. s .. ";" end
  for _, s in ipairs(solidseg) do out[#out+1] = "\\draw " .. s .. ";" end

  if names then
    local cx, cy = 0, 0
    for _, p in ipairs(P) do cx = cx + p[1]; cy = cy + p[2] end
    cx, cy = cx / #P, cy / #P

    local inc = {}
    for _, e in pairs(edges) do
      local a = math.atan(P[e.j][2]-P[e.i][2], P[e.j][1]-P[e.i][1])
      inc[e.i] = inc[e.i] or {}; inc[e.i][#inc[e.i]+1] = a
      inc[e.j] = inc[e.j] or {}; inc[e.j][#inc[e.j]+1] = a + math.pi
    end
    for i, nm in ipairs(names) do
      if nm and nm ~= "" then
        local rx, ry = P[i][1] - cx, P[i][2] - cy
        local rn = math.sqrt(rx*rx + ry*ry)
        local radial = (rn > 1e-9) and math.atan(ry, rx) or (math.pi/2)
        local ang = radial
        local dirs = inc[i]
        if dirs and #dirs > 0 then
          table.sort(dirs, function(a, b) return a % (2*math.pi) < b % (2*math.pi) end)
          local bestgap, bestang, bestrad = -1, radial, math.huge
          for k = 1, #dirs do
            local a1 = dirs[k] % (2*math.pi)
            local a2 = (k < #dirs) and dirs[k+1] % (2*math.pi)
                                    or (dirs[1] % (2*math.pi) + 2*math.pi)
            local gap = a2 - a1
            local mid = (a1 + a2) / 2
            local dr = math.abs(((mid - radial) + math.pi) % (2*math.pi) - math.pi)
            if gap > bestgap + 1e-6
               or (gap > bestgap - 1e-6 and dr < bestrad) then
              bestgap, bestang, bestrad = gap, mid, dr
            end
          end
          ang = bestang
        end
        out[#out+1] = ("\\node[font=\\footnotesize] at (%s,%s) {$%s$};")
          :format(fmt(P[i][1] + 0.3 * math.cos(ang)),
                  fmt(P[i][2] + 0.3 * math.sin(ang)), nm)
      end
    end
  end
  return table.concat(out, "\n"), xmin, xmax
end

local function box_geometry(a, b, c)
  local verts = {
    {0,0,0}, {a,0,0}, {a,b,0}, {0,b,0},
    {0,0,c}, {a,0,c}, {a,b,c}, {0,b,c},
  }
  local faces = {
    {1,4,3,2},
    {5,6,7,8},
    {1,2,6,5},
    {3,4,8,7},
    {1,5,8,4},
    {2,3,7,6},
  }
  return verts, faces
end

local function pyramid_geometry(b, h)
  local verts = {
    {b/2, b/2, h},
    {0,0,0}, {b,0,0}, {b,b,0}, {0,b,0},
  }
  local faces = {
    {2,5,4,3},
    {2,3,1},
    {3,4,1},
    {4,5,1},
    {5,2,1},
  }
  return verts, faces
end

local N = 36

local function ellipse_path(r, z, proj, ox, t0, t1)
  local pts = {}
  for i = 0, N do
    local t = t0 + (t1 - t0) * i / N
    local x, y = proj({ r * math.cos(t), r * math.sin(t), z })
    pts[#pts+1] = ("(%s,%s)"):format(fmt(x + ox), fmt(y))
  end
  return table.concat(pts, " -- ")
end

local function cylinder_render(r, h, proj, ox)
  local out = {}

  out[#out+1] = "\\draw[dashed] " .. ellipse_path(r, 0, proj, ox, 0, math.pi) .. ";"
  out[#out+1] = "\\draw " .. ellipse_path(r, 0, proj, ox, math.pi, 2*math.pi) .. ";"
  out[#out+1] = "\\draw " .. ellipse_path(r, h, proj, ox, 0, 2*math.pi) .. ";"

  local x2 = (proj({0,1,0}))
  local c = x2
  local ts = { math.atan(c), math.atan(c) + math.pi }
  for _, t in ipairs(ts) do
    local bx, by = proj({ r * math.cos(t), r * math.sin(t), 0 })
    local tx, ty = proj({ r * math.cos(t), r * math.sin(t), h })
    out[#out+1] = ("\\draw (%s,%s) -- (%s,%s);")
      :format(fmt(bx + ox), fmt(by), fmt(tx + ox), fmt(ty))
  end
  return table.concat(out, "\n")
end

local function cone_render(r, h, proj, ox)
  local out = {}
  out[#out+1] = "\\draw[dashed] " .. ellipse_path(r, 0, proj, ox, 0, math.pi) .. ";"
  out[#out+1] = "\\draw " .. ellipse_path(r, 0, proj, ox, math.pi, 2*math.pi) .. ";"
  local ax, ay = proj({0, 0, h})
  ax = ax + ox

  local function g(t)
    local px, py = proj({ r * math.cos(t), r * math.sin(t), 0 })
    px = px + ox
    local dx, dy = proj({ -r * math.sin(t), r * math.cos(t), 0 })
    return (px - ax) * dy - (py - ay) * dx
  end
  local roots = {}
  local steps = 720
  local prev = g(0)
  for i = 1, steps do
    local t = 2 * math.pi * i / steps
    local cur = g(t)
    if prev * cur <= 0 and #roots < 2 then
      local lo, hi = 2 * math.pi * (i - 1) / steps, t
      for _ = 1, 50 do
        local mid = (lo + hi) / 2
        if g(lo) * g(mid) <= 0 then hi = mid else lo = mid end
      end
      roots[#roots+1] = (lo + hi) / 2
    end
    prev = cur
  end
  for _, t in ipairs(roots) do
    local px, py = proj({ r * math.cos(t), r * math.sin(t), 0 })
    out[#out+1] = ("\\draw (%s,%s) -- (%s,%s);")
      :format(fmt(ax), fmt(ay), fmt(px + ox), fmt(py))
  end
  return table.concat(out, "\n")
end

local function sphere_render(r, proj, ox)
  local out = {}
  out[#out+1] = ("\\draw (%s,0) circle [radius=%s];"):format(fmt(ox), fmt(r))
  out[#out+1] = "\\draw[dashed] " .. ellipse_path(r, 0, proj, ox, 0, math.pi) .. ";"
  out[#out+1] = "\\draw " .. ellipse_path(r, 0, proj, ox, math.pi, 2*math.pi) .. ";"
  return table.concat(out, "\n")
end

local KINDS = { cube=true, cuboid=true, pyramid=true,
                cylinder=true, cone=true, sphere=true }

local function num_attr(attrs, key, what)
  local v = tonumber(attrs[key] or "")
  if not v or v <= 0 then
    error("texecole: <draw> solid " .. what .. " needs " .. key
        .. ":{a positive number}")
  end
  return v
end

function M.render(rest, state)
  local kind, tail = rest:match("^%s*(%S+)%s*(.*)$")
  if not KINDS[kind] then
    error("texecole: <draw> solid takes cube, cuboid, pyramid, cylinder, "
        .. "cone or sphere (got '" .. tostring(kind) .. "')")
  end

  local names, optstr = nil, {}
  for w in tail:gmatch("%S+") do
    if not names and w:match("^%a+$") and not w:find(":") then
      names = {}
      for ch in w:gmatch("%a") do names[#names+1] = ch end
    else
      optstr[#optstr+1] = w
    end
  end
  local attrs = U.parse_attrs(table.concat(optstr, " "), { tag = "solid" })
  local proj = projector(attrs)
  local ox = state.cursor

  local tex, xmax, _
  if kind == "cube" or kind == "cuboid" then
    local a, b, c
    if kind == "cube" then
      a = num_attr(attrs, "edge", "cube"); b, c = a, a
    else
      local sv = attrs.sides
      if not sv then
        error("texecole: <draw> solid cuboid needs sides:{a, b, c}")
      end
      a, b, c = sv:match("^%s*([%d%.]+)%s*,%s*([%d%.]+)%s*,%s*([%d%.]+)%s*$")
      a, b, c = tonumber(a), tonumber(b), tonumber(c)
      if not a then
        error("texecole: <draw> solid cuboid sides:{a, b, c} needs three numbers")
      end
    end
    if names and #names ~= 8 then
      error("texecole: <draw> solid " .. kind .. " names 8 vertices (ABCDEFGH), got "
          .. #names)
    end
    local verts, faces = box_geometry(a, b, c)
    tex, _, xmax = poly_render(verts, faces, names, proj, ox)
    state.cursor = xmax + 1.5
  elseif kind == "pyramid" then
    local b = num_attr(attrs, "base", "pyramid")
    local h = num_attr(attrs, "height", "pyramid")
    if names and #names ~= 5 then
      error("texecole: <draw> solid pyramid names 5 vertices (SABCD), got " .. #names)
    end
    local verts, faces = pyramid_geometry(b, h)
    tex, _, xmax = poly_render(verts, faces, names, proj, ox)
    state.cursor = xmax + 1.5
  elseif kind == "cylinder" then
    local r = num_attr(attrs, "radius", "cylinder")
    local h = num_attr(attrs, "height", "cylinder")
    tex = cylinder_render(r, h, proj, ox + r)
    state.cursor = ox + 2*r + 1.9
  elseif kind == "cone" then
    local r = num_attr(attrs, "radius", "cone")
    local h = num_attr(attrs, "height", "cone")
    tex = cone_render(r, h, proj, ox + r)
    state.cursor = ox + 2*r + 1.9
  else
    local r = num_attr(attrs, "radius", "sphere")
    tex = sphere_render(r, proj, ox + r)
    state.cursor = ox + 2*r + 1.9
  end
  return tex
end

return M
