--[[
  Package: seatingchart
  Date:
  2026-07-22
  Version:
  1.0.0beta
]]
Seat = {
   id = nil,
   row = 0,
   col = 0,
   x = 0,
   y = 0,
   rrow = 0,
   rcol = 0,
   rotate = 0,
   kind = nil,
   label="",
   textlabel=""
}
function Seat:new(o)
   o = o or {}
   setmetatable(o, self)
   self.__index = self
   return o
end

AllSeats={}
SEATREMOVED  = 1
SEATEMPTY    = 2
SEATASSIGNED = 3
AISLE        = 4
SPECIAL      = 5

function initSeating(rows, cols, shape)
   local cid = 0
   -- A document may contain several charts. Replacing only the leading
   -- entries would leave seats from a larger preceding chart behind, because
   -- drawSeats() deliberately iterates over the array with ipairs().
   for key in pairs(AllSeats) do
      AllSeats[key] = nil
   end
   AllSeats.size=0
   AllSeats.rows=rows
   AllSeats.cols=cols
   --tex.sprint("\\typeout{*** initSeating CALLED, ndx: }")
   if shape == "rect" then
      for r = 1, rows do
 for c = 1, cols do
    cid = cid+1
    AllSeats[cid] = Seat.new({id=cid, row=r, col=c, x=c-(cols+1)/2, y=r-1.0, rrow=0, rcol=0, rotate=0, kind=SEATEMPTY, label=tostring(cid), textlabel=""})
    AllSeats.size=AllSeats.size+1
 end
      end
   elseif shape ==  "arc" then
      local max_arc=120 * math.pi/180
      local radius
      local step
      local rot
      local x
      local y
      local phi
      -- Choose the innermost radius so that a complete row spans at most
      -- max_arc while adjacent seat centres remain one unit apart. Increasing
      -- the radius by one for each row gives the same radial distance.
      local inner_radius = 1
      if cols > 1 then
         inner_radius = 0.5 / math.sin(max_arc/(2*(cols-1)))
      end
      for r = 1, rows do
 radius = inner_radius + r - 1
 if cols > 1 then
    step = 2 * math.asin(0.5/radius)
 else
    step = 0
 end
 for c = 1, cols do
    cid = cid+1
    phi = (c-(cols+1)/2) * step
    x = radius * math.sin(phi)
    y = radius * math.cos(phi) - inner_radius
    rot=-phi/math.pi*180
    AllSeats[cid] = Seat.new({id=cid, row=r, col=c,
      rotate=rot,
      x=x,
      y=y,
      rrow=0, rcol=0, kind=SEATEMPTY, label=tostring(cid)})
    AllSeats.size=AllSeats.size+1
 end
      end
   end

end

function initArc(rows, cols)
end

function seatDim(w,h)
   AllSeats.seatwidth = w
   AllSeats.seatheight = h
end

function findSeatAt(y,x)
   if y<0 then
      y = AllSeats.rows + y + 1
   end
   if x<0 then
      x = AllSeats.cols + x + 1
   end
   if x>AllSeats.cols or y > AllSeats.rows then
      tex.sprint("\\PackageError{\\packagename}{Index out of bound}{Check the used coordinates.}")
   end
   local ndx = (y-1)* AllSeats.cols + x
   return ndx
end

function assignSeat(ndx,d)
   local kind = d.kind or SEATASSIGNED
   local runningrow = d.rr or 0
   local runnigseat = d.rc or 0
   -- tex.sprint("\\typeout{*** AllSeats[ndx] ",AllSeats,"[",ndx,"]}")
   -- tex.sprint("\\typeout{ kind=",kind,", text='",d.label,"', rr=",runningrow,", rs=",runnigseat,"}")
   if ndx ~= nil and AllSeats[ndx].kind == SEATEMPTY then
      AllSeats[ndx].kind = kind
      AllSeats[ndx].label=d.label or AllSeats[ndx].label
      AllSeats[ndx].rrow=runningrow
      AllSeats[ndx].rcol=runnigseat
      return true
   else
      return false
   end
end
function assignSeatAt(y,x,d)
   assignSeat(findSeatAt(y,x),d)
end
function removeSeatAt(y,x)
   assignSeat(findSeatAt(y,x),{kind=SEATREMOVED})
end
function removeAisle(c,from,to)
   for r = from, to do
      assignSeatAt(r,c,{kind=AISLE})
   end
end

function seatingSchemeInRows(pat,policy)
   local numseats =  AllSeats.cols
   local numrows =  AllSeats.rows
   local frow = policy["first row"]
   local lrow = policy["last row"]
   local rstep = policy["row sep"]
   local rres  = policy["row restart"]
   local ltext = policy["label format"]
   local runningrow = 1
   rs={}
   for r = frow, lrow do
      if (r-frow) % rstep == 0 then
 table.move({{[1]=r,[2]=runningrow}}, 1, 1, #rs + 1, rs)
 runningrow=runningrow+1
      end
      if (r-frow) == rres-1 then
 frow = frow + rres
      end
   end
   -- tex.sprint("\\typeout{**** pattern=",pat,", numseats=",numseats,"}")
   if string.len(pat) < numseats then
      pat = string.rep(pat,math.ceil(numseats/string.len(pat)))
   end
   for _,row in pairs(rs) do
      local r=row[1]
      local rr=row[2]
      -- tex.sprint("\\typeout{**** seatingSchemeInRow called r=",r,"}")
      local step = 1
      local pndx = 1
      local seat = 1
      if policy["rtol"] then
 step = -1
 pndx = -1
 seat = numseats
      end
      local runningseat=1
      -- tex.sprint("\\typeout{ Seatscheme is ",pat,"}")
      while ((seat >= 1) and (seat<= numseats)) do
 local curkind = AllSeats[findSeatAt(r,seat)].kind
 -- tex.sprint("\\typeout{ Try seat ",seat,"(kind=",curkind,") with 'pat[",pndx,"]=",string.upper(string.sub(pat,pndx,pndx)),"'}")
 if string.upper(string.sub(pat,pndx,pndx)) == 'X' and ((curkind == SEATEMPTY) or (curkind == SEATASSIGNED))then
    -- tex.sprint("\\typeout{*** Ask for assignment, rr=",rr,", rc=",runningseat,", label =",ltext,"}")
    assignSeatAt(r,seat,{ kind=SEATASSIGNED, rr=rr, rc=runningseat, label=ltext } )
    runningseat=runningseat+1
    pndx = pndx + step
 elseif curkind == SEATEMPTY then
    pndx = pndx + step
 elseif string.upper(string.sub(pat,pndx,pndx)) == 'X' and curkind == SEATREMOVED and  policy["ignore removed seats"]==false then
    pndx = pndx + step
    runningseat=runningseat+1
 elseif curkind == SEATREMOVED and  policy["ignore removed seats"]==false then
    pndx = pndx + step
 elseif curkind == AISLE then
    if policy["aisle restarts"] then
       pndx = step
    else
       pndx = pndx + policy["aisle counts"]*step
    end
 end
 seat = seat + step
      end
   end
end

-- Drawing
function seatempty(s)
   local width = AllSeats.seatwidth
   local height = AllSeats.seatheight
   tex.sprint("\\node[rectangle,empty-seat, minimum\\space width=",width,", minimum\\space height=",height,",rotate=",s.rotate,"] at (",s.x,",",s.y,") {};")
   tex.sprint("\\node[empty-label, minimum\\space width=",width,",rotate=",s.rotate,"] at (",s.x,",",s.y,")"..
      "{\\tucemptylabelformat{",s.row,"}{",s.col,"}{",s.rrow,"}{",s.rcol,"}{",s.label,"}};")
end

function seatassigned(s)
   local width = AllSeats.seatwidth
   local height = AllSeats.seatheight
   tex.sprint("\\node[rectangle,assigned-seat, minimum\\space width=",width,", minimum\\space height=",height,",rotate=",s.rotate,"] at (",s.x,",",s.y,") {Xy};")
   tex.sprint("\\node[assigned-label,minimum\\space width=",width,",rotate=",s.rotate,"] at (",s.x,",",s.y,")"..
          "{\\scassignedlabelformat{",s.row,"}{",s.col,"}{",s.rrow,"}{",s.rcol,"}{",s.label,"}};")
end

function seatremoved(s)
end

function drawSeats()
 local  case_tbl =
    {
       [SEATREMOVED]  = seatremoved,
       [AISLE]        = seatremoved,
       [SEATEMPTY]    = seatempty,
       [SEATASSIGNED] = seatassigned,
    }
   for _ ,s in  ipairs(AllSeats) do
      if s.kind ~= nil then
 -- tex.sprint("\\typeout{ *** kind=",s.kind,", x=",s.col,", y=",s.row,"}")
 func = case_tbl[s.kind]
 if func ~= nil then
    func(s)
 end
      end
   end
end

function getNumberOfSeats()
   tex.count["scnumofseats"]=AllSeats.size
end

function setupNextlabel(ndx)
   -- print("ndx=",ndx, "type:",type(ndx))
   local s = AllSeats[ndx]
   if s and (s.kind ~=nil) and (s.kind == SEATASSIGNED) then
      -- print("S={",s.row,",",s.col,",",s.rrow,",",s.rcol,", '",s.label,"' }")
      tex.sprint("\\toggletrue{scfoundassigned}")
      tex.sprint("\\def\\scnextlabel{\\scassignedlabelformat{"..tostring(s.row).."}{"..tostring(s.col).."}{"..tostring(s.rrow).."}{"..tostring(s.rcol).."}{"..tostring(s.label).."}}")
   end
end

function saveTextLabel(ndx)
   -- print("*** Try to assign textlabel to "..tostring(ndx), tex.box[0],AllSeats[ndx],AllSeats[ndx].textlabel)
   if tex.box[0] and AllSeats[ndx] then
      -- print("*** Do assign textlabel to "..tostring(ndx))
      AllSeats[ndx].textlabel=""
      -- print("\n")
      for n in node.traverse(tex.box[0].head) do
 -- 12: glue, 13: kern
 if n.id==12 or n.id==13 then AllSeats[ndx].textlabel=AllSeats[ndx].textlabel.." " end
 -- 29: Glyph
 if n.id==29 then  AllSeats[ndx].textlabel=AllSeats[ndx].textlabel..string.char(n.char) end
 -- 7/2: disc, automatic
 if n.id==7 and n.subtype==2 then
    local rep = n.replace
    AllSeats[ndx].textlabel=AllSeats[ndx].textlabel..string.char(rep.char)
 end
      end
   end
end

local function parseCSVLine(line)
   local fields={}
   local field={}
   local quoted=false
   local index=1
   while index <= #line do
      local char=string.sub(line,index,index)
      if char == '"' then
         if quoted and string.sub(line,index+1,index+1) == '"' then
            table.insert(field,'"')
            index=index+1
         else
            quoted=not quoted
         end
      elseif char == ',' and not quoted then
         table.insert(fields,table.concat(field))
         field={}
      else
         table.insert(field,char)
      end
      index=index+1
   end
   table.insert(fields,table.concat(field))
   return fields
end

local function trim(value)
   return (string.gsub(value,"^%s*(.-)%s*$","%1"))
end

local function seatingInputLabel(fields,first)
   local labels={}
   for index=first,#fields do
      table.insert(labels,trim(fields[index]))
   end
   return table.concat(labels,", ")
end

local function setImportedLabel(seat,label)
   if seat == nil or seat.kind == SEATREMOVED or seat.kind == AISLE then
      return false
   end
   seat.kind=SEATASSIGNED
   seat.label=label
   return true
end

function loadSeatingList(filename,mode,header)
   local input=io.open(filename,"r")
   if not input then
      tex.sprint("\\PackageError{\\packagename}{Can't read seating input file}{Check the file name and path.}")
      return
   end

   local records={}
   for line in input:lines() do
      line=string.gsub(line,"\r$","")
      if line ~= "" then
         table.insert(records,parseCSVLine(line))
      end
   end
   input:close()
   if header and #records > 0 then
      table.remove(records,1)
   end

   if mode == "sequential" then
      local record=1
      for _,seat in ipairs(AllSeats) do
         if seat.kind == SEATASSIGNED and records[record] ~= nil then
            seat.label=seatingInputLabel(records[record],1)
            record=record+1
         end
      end
      if record <= #records then
         texio.write_nl("term and log","Package seatingchart Warning: "..
            tostring(#records-record+1).." seating input record(s) could not be assigned")
      end
      return
   end

   for record,fields in ipairs(records) do
      local seat
      local label
      if mode == "seat" then
         seat=AllSeats[tonumber(trim(fields[1] or ""))]
         label=seatingInputLabel(fields,2)
      elseif mode == "coordinates" then
         local row=tonumber(trim(fields[1] or ""))
         local col=tonumber(trim(fields[2] or ""))
         if row and col and row >= 1 and row <= AllSeats.rows and
            col >= 1 and col <= AllSeats.cols then
            seat=AllSeats[(row-1)*AllSeats.cols+col]
         end
         label=seatingInputLabel(fields,3)
      end
      if not setImportedLabel(seat,label) then
         texio.write_nl("term and log","Package seatingchart Warning: seating input record "..
            tostring(record).." refers to an unavailable seat")
      end
   end
end

function generateSeatList(outstream,instream, coor)
   local names={}
   local emtpydefault
   if instream ~= nil then
      f = io.open(instream,"r")
      if not f then
 tex.print("\\PackageError{\\packagename}{Can't read file '",instream,"'}{}")
 return
      else
 for line in io.lines(instream) do
    -- print("*** TAKE "..line)
    table.insert(names, line)
 end
 -- print("names[1]="..names[1])
 emptydefault=string.gsub(names[1],"[^,]","")
 -- print("empty='"..emptydefault.."'")
 f:close()
      end
   end

   seatfile = io.open(outstream, "w")
   if not seatfile then
 tex.print("\\PackageError{\\packagename}{Can't read file '",instream,"'}{}")
 return
   else
      local index=1
      for j ,s in  ipairs(AllSeats) do
 -- print("j="..tostring(j))
 if s.kind == SEATASSIGNED then
    if names[index] ~= nil then
       seatfile:write(names[index]..",")
    elseif instream ~= nil then
       -- print("** empty='"..emptydefault.."'")
       seatfile:write(emptydefault..",")
    end
    if coor then
       seatfile:write(tostring(s.row)..","..tostring(s.col)..",")
    end
    -- print("Label is",s.textlabel)
    seatfile:write(s.textlabel)
    seatfile:write(string.char(10))
    index=index+1
 end
      end
      seatfile:close()
      if index <= #names then
 luatexbase.add_to_callback("stop_run", screportmissing(names,index), "Report" )
      end
   end
end

function screportmissing(names, index)
   local report=string.char(10).."\27[34mPackage 'seatingchart':"..string.char(10)..[[*******************************************
The following person(s) couldn't be seated:]]..string.char(10)

   for i=index, #names do
      report=report.." - \27[1m"..names[i].."\27[22m"..string.char(10)
   end
   report=report.."*******************************************"..string.char(10)
   return function()
      texio.setescape(false)
      texio.write_nl(report)
   end
end
