-- Go2Cue for grandMA3 (2026-10-18): tells Go2Cue the cue running in your main sequence, the
-- next cue and the cues after it, with their names and fade times, so the stage manager and
-- the crew see what's NEXT. grandMA3 on its own only sends the cue that just ran.
--
-- Import it into the Plugins pool and run it once: it keeps going in the background until
-- you run it again. It sends over an OSC line (Menu > In & Out > OSC) whose Destination IP is
-- the stage manager's Go2Cue computer, port 53100, Send on and Send Command on (SendOSC sends
-- only on a line with Send Command on: "Illegal property" otherwise). Set the two numbers below (or the
-- user variables Go2CueSequence and Go2CueOSCLine) to your main sequence and that line.
-- Go2Cue reads /go2cue/<sequence>/cue|next|upcoming|list|clear (app/extra_feeds.h).

local SEQUENCE = 1   -- your main cue list's sequence number (0: whichever sequence is selected)
local OSC_LINE = 1   -- the In & Out > OSC line that sends to Go2Cue
local EVERY = 0.1    -- seconds between looks at the sequence
local AGAIN = 2      -- seconds: everything sent again, so Go2Cue catches up after a restart
local UPCOMING = 8   -- cues after NEXT, for the lists on Go2Cue's screens
local CHUNK = 10     -- cues to a /list message: the whole sequence, for the fold-down list
local LIST_AGAIN = 10  -- seconds: the whole list sent again, changed or not

local function setting(name, default)
  local ok, v = pcall(function() return GetVar(UserVars(), name) end)
  return ok and tonumber(v) or default
end

-- SendOSC takes commas and quotes as its own: keep them out of names.
local function clean(s)
  s = tostring(s or ''):gsub('[,"]', ' '):gsub('%s+', ' '):gsub('^ ', ''):gsub(' $', '')
  return s
end

local function cueNumber(cue)
  local no = cue and tonumber(cue.no)
  if not no then return nil end
  no = no / 1000  -- grandMA3 keeps cue 7.5 as 7500
  if no == math.floor(no) then return string.format('%d', no) end
  return (string.format('%.3f', no):gsub('0+$', ''))
end

local function fadeOf(cue)
  local part = cue and cue[1]
  if not part then return 0 end
  local ok, v = pcall(function() return part:Get('CueInFade') end)
  if not ok then return 0 end
  return tonumber(v) or tonumber(tostring(v or ''):match('[%d%.]+')) or 0
end

local function nameOf(cue)
  local n = clean(cue and cue.name)
  if n == '' then n = 'Cue ' .. (cueNumber(cue) or '') end
  return n
end

-- The cues in order (the Off cue and cue 0 left out).
local function cuesOf(seq)
  local list = {}
  for _, c in ipairs(seq:Children() or {}) do
    local no = tonumber(c.no)
    if no and no > 0 and c.name ~= 'OffCue' then list[#list + 1] = c end
  end
  return list
end

local function send(line, addr, types, ...)
  local args = { ... }
  local text = types == '' and addr or addr .. ',' .. types
  for _, a in ipairs(args) do text = text .. ',' .. a end
  Cmd(string.format('SendOSC %d "%s"', line, text))
end

-- The whole sequence, CHUNK cues at a time: /list <first index> <total> then number, name, fade.
local function sendList(line, base, cues)
  if #cues == 0 then send(line, base .. '/list', 'ii', 0, 0) return end
  for first = 1, #cues, CHUNK do
    local args, types = { first - 1, #cues }, 'ii'
    for i = first, math.min(#cues, first + CHUNK - 1) do
      local c = cues[i]
      args[#args + 1] = cueNumber(c)
      args[#args + 1] = nameOf(c)
      args[#args + 1] = string.format('%.1f', fadeOf(c))
      types = types .. 'ssf'
    end
    send(line, base .. '/list', types, table.unpack(args))
  end
end

local function look(state)
  local seqNo = setting('Go2CueSequence', SEQUENCE)
  local line = setting('Go2CueOSCLine', OSC_LINE)
  local seq = seqNo == 0 and SelectedSequence() or DataPool().Sequences[seqNo]
  if not seq then return end
  local no = tonumber(seq.no) or seqNo
  local cues = cuesOf(seq)
  local running = seq:HasActivePlayback()
  local current = running and seq:CurrentChild() or nil
  local now = current and cueNumber(current) or ''
  local at = 0
  for i, c in ipairs(cues) do
    if current and cueNumber(c) == now then at = i end
  end
  -- A cue loaded on the sequence (Load, or Go2Cue's Make it NEXT) is what the next GO runs.
  local nextAt = at + 1
  local okL, loaded = pcall(function() return seq.LoadedCue end)
  local loadedNo = okL and loaded and cueNumber(loaded)
  if loadedNo and loadedNo ~= now then
    for i, c in ipairs(cues) do
      if cueNumber(c) == loadedNo then nextAt = i end
    end
  end
  local nextCue = cues[nextAt]
  local sign = no .. '|' .. now .. '|' .. (nextCue and cueNumber(nextCue) or '') .. '|' .. #cues
  local due = sign ~= state.sign or os.time() - state.at >= AGAIN
  if not due then return end
  state.sign, state.at = sign, os.time()
  local base = '/go2cue/' .. no
  if current then
    send(line, base .. '/cue', 'ssf', now, nameOf(current), string.format('%.1f', fadeOf(current)))
  else
    send(line, base .. '/clear', 'i', 0)  -- SendOSC refuses a message with no values (Illegal property)
  end
  if nextCue then
    send(line, base .. '/next', 'ss', cueNumber(nextCue), nameOf(nextCue))
    local more, types = {}, ''
    for i = nextAt + 1, math.min(#cues, nextAt + UPCOMING) do
      more[#more + 1] = cueNumber(cues[i])
      more[#more + 1] = nameOf(cues[i])
      types = types .. 'ss'
    end
    if #more > 0 then send(line, base .. '/upcoming', types, table.unpack(more)) end
  end
  local parts = {}
  for _, c in ipairs(cues) do parts[#parts + 1] = cueNumber(c) .. '|' .. nameOf(c) .. '|' .. fadeOf(c) end
  local listSign = no .. '#' .. table.concat(parts, '#')
  if listSign ~= state.listSign or os.time() - (state.listAt or 0) >= LIST_AGAIN then
    state.listSign, state.listAt = listSign, os.time()
    sendList(line, base, cues)
  end
end

local function Main()
  if _G.Go2CueRunning then  -- running already: this run stops it
    _G.Go2CueRunning = false
    Printf('Go2Cue: stopped')
    return
  end
  _G.Go2CueRunning = true
  Printf('Go2Cue: following sequence %d, sending on OSC line %d. Run the plugin again to stop.',
    setting('Go2CueSequence', SEQUENCE), setting('Go2CueOSCLine', OSC_LINE))
  local state = { sign = '', at = 0 }
  while _G.Go2CueRunning do
    local ok, err = pcall(look, state)
    if not ok then Printf('Go2Cue: %s', tostring(err)) end
    coroutine.yield(EVERY)
  end
end

return Main
