-- Bundled by luabundle {"version":"1.6.0"}
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
	local loadingPlaceholder = {[{}] = true}

	local register
	local modules = {}

	local require
	local loaded = {}

	register = function(name, body)
		if not modules[name] then
			modules[name] = body
		end
	end

	require = function(name)
		local loadedModule = loaded[name]

		if loadedModule then
			if loadedModule == loadingPlaceholder then
				return nil
			end
		else
			if not modules[name] then
				if not superRequire then
					local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
					error('Tried to require ' .. identifier .. ', but no such module has been registered')
				else
					return superRequire(name)
				end
			end

			loaded[name] = loadingPlaceholder
			loadedModule = modules[name](require, loaded, register, modules)
			loaded[name] = loadedModule
		end

		return loadedModule
	end

	return require, loaded, register, modules
end)(nil)
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("arkhamdb/DeckImporterMain")
end)
__bundle_register("arkhamdb/DeckImporterMain", function(require, _LOADED, __bundle_register, __bundle_modules)
require("arkhamdb/DeckImporterUi")
require("playercards/PlayerCardSpawner")

local playAreaApi = require("core/PlayAreaApi")
local arkhamDb = require("arkhamdb/ArkhamDb")
local zones = require("playermat/Zones")

local DEBUG = false

local ALL_CARDS_GUID = "15bb07"

local customizationRowsWithFields = { }
-- inputMap maps from (our 1-indexes) customization row index to inputValue table index
-- The Raven Quill
customizationRowsWithFields["09042"] = {}
customizationRowsWithFields["09042"].inputCount = 2
customizationRowsWithFields["09042"].inputMap = {}
customizationRowsWithFields["09042"].inputMap[1] = 1
customizationRowsWithFields["09042"].inputMap[5] = 2
-- Friends in Low Places
customizationRowsWithFields["09060"] = {}
customizationRowsWithFields["09060"].inputCount = 2
customizationRowsWithFields["09060"].inputMap = {}
customizationRowsWithFields["09060"].inputMap[1] = 1
customizationRowsWithFields["09060"].inputMap[3] = 2
-- Living Ink
customizationRowsWithFields["09079"] = {}
customizationRowsWithFields["09079"].inputCount = 3
customizationRowsWithFields["09079"].inputMap = {}
customizationRowsWithFields["09079"].inputMap[1] = 1
customizationRowsWithFields["09079"].inputMap[5] = 2
customizationRowsWithFields["09079"].inputMap[6] = 3
-- Grizzled
customizationRowsWithFields["09101"] = {}
customizationRowsWithFields["09101"].inputCount = 3
customizationRowsWithFields["09101"].inputMap = {}
customizationRowsWithFields["09101"].inputMap[1] = 1
customizationRowsWithFields["09101"].inputMap[2] = 2
customizationRowsWithFields["09101"].inputMap[3] = 3

function onLoad(script_state)
  initializeUi(JSON.decode(script_state))
  math.randomseed(os.time())
  arkhamDb.initialize()
end

function arkhamdb_reinit()
   arkhamDb.initialize()
end

function onSave() return JSON.encode(getUiState()) end

-- Returns the zone name where the specified card should be placed, based on its metadata.
---@param cardMetadata Table of card metadata.
---@return Zone String Name of the zone such as "Deck", "SetAside1", etc.
--     See Zones object documentation for a list of valid zones.
function getDefaultCardZone(cardMetadata, bondedList)
  if (cardMetadata.id == "09080-m") then -- Have to check the Servitor before other minicards
    return "SetAside6"
  elseif (cardMetadata.id == "09006") then -- On The Mend is set aside
    return "SetAside2"
  elseif cardMetadata.type == "Investigator" then
    return "Investigator"
  elseif cardMetadata.type == "Minicard" then
    return "Minicard"
  elseif cardMetadata.type == "UpgradeSheet" then
    return "SetAside4"
  elseif cardMetadata.startsInPlay then
    return "BlankTop"
  elseif cardMetadata.permanent then
    return "SetAside1"
  elseif bondedList[cardMetadata.id] then
    return "SetAside2"
    -- SetAside3 is used for Ancestral Knowledge / Underworld Market
  else
    return "Deck"
  end
end

function buildDeck(playerColor, deckId)
  local uiState = getUiState()
  arkhamDb.getDecklist(
      playerColor,
      deckId,
      uiState.private,
      uiState.loadNewest,
      uiState.investigators,
      loadCards)
end

-- Process the slot list, which defines the card Ids and counts of cards to load. Spawn those cards
-- at the appropriate zones and report an error to the user if any could not be loaded.
-- This is a callback function which handles the results of ArkhamDb.getDecklist()
-- This method uses an encapsulated coroutine with yields to make the card spawning cleaner.
--
---@param slots Table Key-Value table of cardId:count. cardId is the ArkhamDB ID of the card to spawn,
--     and count is the number which should be spawned
---@param investigatorId String ArkhamDB ID (code) for this deck's investigator.
--     Investigator cards should already be added to the slots list if they
--     should be spawned, but this value is separate to check for special
--     handling for certain investigators
---@param bondedList Table A table of cardID keys to meaningless values.  Card IDs in this list were added
--     from a parent bonded card.
---@param customizations String ArkhamDB data for customizations on customizable cards
---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
function loadCards(slots, investigatorId, bondedList, customizations, playerColor)
  function coinside()
    local allCardsBag = getObjectFromGUID(ALL_CARDS_GUID)
    local yPos = {}
    local cardsToSpawn = {}
    for cardId, cardCount in pairs(slots) do
      local card = allCardsBag.call("getCardById", { id = cardId })
      if card ~= nil then
        local cardZone = getDefaultCardZone(card.metadata, bondedList)
        for i = 1, cardCount do
          table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })
        end

        slots[cardId] = 0
      end
    end

    handleAncestralKnowledge(cardsToSpawn)
    handleUnderworldMarket(cardsToSpawn, playerColor)
    handleHunchDeck(investigatorId, cardsToSpawn, playerColor)
    handleCustomizableUpgrades(cardsToSpawn, customizations)

    -- Split the card list into separate lists for each zone
    local zoneDecks = buildZoneLists(cardsToSpawn)
    -- Spawn the list for each zone
    for zone, zoneCards in pairs(zoneDecks) do
      local deckPos = zones.getZonePosition(playerColor, zone)
      deckPos.y = 3

      local callback = nil
      -- If cards are spread too close together TTS groups them weirdly, selecting multiples
      -- when hovering over a single card.  This distance is the minimum to avoid that
      local spreadDistance = 1.15
      if (zone == "SetAside4") then
        -- SetAside4 is reserved for customization cards, and we want them spread on the table
        -- so their checkboxes are visible
        if (playerColor == "White") then
          deckPos.z = deckPos.z + (#zoneCards - 1) * spreadDistance
        elseif (playerColor == "Green") then
          deckPos.x = deckPos.x + (#zoneCards - 1) * spreadDistance
        end
        callback = function(deck) deck.spread(spreadDistance) end
      elseif zone == "Deck" then
        callback = function(deck) deckSpawned(deck, playerColor) end
      end
      Spawner.spawnCards(
          zoneCards,
          deckPos,
          zones.getDefaultCardRotation(playerColor, zone),
          true, -- Sort deck
          callback)

      coroutine.yield(0)
    end

    -- Look for any cards which haven't been loaded
    local hadError = false
    for cardId, remainingCount in pairs(slots) do
      if remainingCount > 0 then
        hadError = true
        arkhamDb.logCardNotFound(cardId, playerColor)
      end
    end
    if (not hadError) then
      printToAll("Deck loaded successfully!", playerColor)
    end
    return 1
  end

  startLuaCoroutine(self, "coinside")
end

-- Callback handler for the main deck spawning.  Looks for cards which should start in hand, and
-- draws them for the appropriate player.
---@param deck Object Callback-provided spawned deck object
---@param playerColor String Color of the player to draw the cards to
function deckSpawned(deck, playerColor)
  local handPos = Player[playerColor].getHandTransform(1).position -- Only one hand zone per player
  local deckCards = deck.getData().ContainedObjects
  -- Process in reverse order so taking cards out doesn't upset the indexing
  for i = #deckCards, 1, -1 do
    local cardMetadata = JSON.decode(deckCards[i].GMNotes) or { }
    if cardMetadata.startsInHand then
      deck.takeObject({ index = i - 1, position = handPos, flip = true, smooth = true})
    end
  end
end

-- Conver the Raven Quill's selections from card IDs to card names.  This could be more elegant
-- but the inputs are very static so we're using some brute force.
---@param selectionString String provided by ArkhamDB, indicates the customization selections
--     Should be either a single card ID or two separated by a ^ (e.g. XXXXX^YYYYY)
function convertRavenQuillSelections(selectionString)
  if (string.len(selectionString) == 5) then
    return getCardName(selectionString)
  elseif (string.len(selectionString) == 11) then
    return getCardName(string.sub(selectionString, 1, 5)) .. ", " .. getCardName(string.sub(selectionString, 7))
  end
end

-- Returns the simple name of a card given its ID.  This will find the card and strip any trailing
-- SCED-specific suffixes such as (Taboo) or (Level)
function getCardName(cardId)
  local allCardsBag = getObjectFromGUID("15bb07")
  local card = allCardsBag.call("getCardById", { id = cardId })
  if (card ~= nil) then
    local name = card.data.Nickname
    if (string.find(name, " %(")) then
      return string.sub(name, 1, string.find(name, " %(") - 1)
    else
      return name
    end
  end
end

-- Split a single list of cards into a separate table of lists, keyed by the zone
---@param cards: Table of {cardData, cardMetadata, zone}
---@return: Table of {zoneName=card list}
function buildZoneLists(cards)
  local zoneList = {}
  for _, card in ipairs(cards) do
    if zoneList[card.zone] == nil then
      zoneList[card.zone] = {}
    end
    table.insert(zoneList[card.zone], card)
  end

  return zoneList
end

-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3
---@param cardList Table Deck list being created
function handleAncestralKnowledge(cardList)
  local hasAncestralKnowledge = false
  local skillList = {}
  -- Have to process the entire list to check for Ancestral Knowledge and get all possible skills, so do both in one pass
  for i, card in ipairs(cardList) do
    if card.metadata.id == "07303" then
      hasAncestralKnowledge = true
      card.zone = "SetAside3"
    elseif (card.metadata.type == "Skill"
        and card.zone == "Deck"
        and not card.metadata.weakness) then
      table.insert(skillList, i)
    end
  end
  if hasAncestralKnowledge then
    for i = 1, 5 do
      -- Move 5 random skills to SetAside3
      local skillListIndex = math.random(#skillList)
      cardList[skillList[skillListIndex]].zone = "UnderSetAside3"
      table.remove(skillList, skillListIndex)
    end
  end
end

-- Check for and handle Underworld Market by moving all Illicit cards to UnderSetAside3
---@param cardList Table Deck list being created
---@param playerColor String Color this deck is being loaded for
function handleUnderworldMarket(cardList, playerColor)
  local hasMarket = false
  local illicitList = {}
  -- Process the entire list to check for Underworld Market and get all possible skills, doing both in one pass
  for i, card in ipairs(cardList) do
    if card.metadata.id == "09077" then
      -- Underworld Market found
      hasMarket = true
      card.zone = "SetAside3"
    elseif (card.metadata.traits ~= nil and string.find(card.metadata.traits, "Illicit", 1, true)
        and card.metadata.bonded_to == nil
        and not card.metadata.weakness) then
      table.insert(illicitList, i)
    end
  end

  if hasMarket then
    if #illicitList < 10 then
      printToAll("Only " .. #illicitList ..
          " Illicit cards in your deck, you can't trigger Underworld Market's ability.",
          playerColor)
    else
      -- Process cards to move them to the market deck. This is done in reverse
      -- order because the sorting needs to be reversed (deck sorts for face down)
      -- Performance here may be an issue, as table.remove() is an O(n) operation
      -- which makes the full shift O(n^2). But keep it simple unless it becomes
      -- a problem
      for i = #illicitList, 1, -1 do
        local moving = cardList[illicitList[i]]
        moving.zone = "UnderSetAside3"
        table.remove(cardList, illicitList[i])
        table.insert(cardList, moving)
      end

      if #illicitList > 10 then
        printToAll("Moved all " .. #illicitList ..
            " Illicit cards to the Market deck, reduce it to 10",
            playerColor)
      else
        printToAll("Built the Market deck", playerColor)
      end
    end
  end
end

-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch
--     Deck.
---@param investigatorId String ID for the deck's investigator card. Passed separately because the
---    investigator may not be included in the cardList
---@param cardList Table Deck list being created
---@param playerColor String Color this deck is being loaded for
function handleHunchDeck(investigatorId, cardList, playerColor)
  if investigatorId == "05002" then -- Joe Diamond
    local insightList = {}
    for i, card in ipairs(cardList) do
      if (card.metadata.type == "Event"
          and card.metadata.traits ~= nil
          and string.match(card.metadata.traits, "Insight")
          and card.metadata.bonded_to == nil) then
        table.insert(insightList, i)
      end
    end
    -- Process insights to move them to the hunch deck. This is done in reverse
    -- order because the sorting needs to be reversed (deck sorts for face down)
    -- Performance here may be an issue, as table.remove() is an O(n) operation
    -- which makes the full shift O(n^2). But keep it simple unless it becomes
    -- a problem
    for i = #insightList, 1, -1 do
      local moving = cardList[insightList[i]]
      moving.zone = "SetAside5"
      table.remove(cardList, insightList[i])
      table.insert(cardList, moving)
    end
    if #insightList < 11 then
      printToAll("Joe's hunch deck must have 11 cards but the deck only has " .. #insightList ..
          " Insight events.", playerColor)
    elseif #insightList > 11 then
      printToAll("Moved all " .. #insightList ..
          " Insight events to the hunch deck, reduce it to 11.", playerColor)
    else
      printToAll("Built Joe's hunch deck", playerColor)
    end
  end
end

-- For any customization upgrade cards in the card list, process the metadata from the deck to
-- set the save state to show the correct checkboxes/text field values
---@param cardList Table Deck list being created
---@param customizations Table Deck's meta table, extracted from ArkhamDB's deck structure
function handleCustomizableUpgrades(cardList, customizations)
  for _, card in ipairs(cardList) do
    if card.metadata.type == "UpgradeSheet" then
      local baseId = string.sub(card.metadata.id, 1, 5)
      local upgrades = customizations["cus_" .. baseId]
      log(upgrades)
      log(baseId)

      if upgrades ~= nil then
        -- initialize tables
        -- markedBoxes: contains the amount of markedBoxes (left to right) per row (starting at row 1)
        -- inputValues: contains the amount of inputValues per row (starting at row 0)
        local markedBoxes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
        local inputValues = {}
        local index_xp    = {}

        -- get the index and xp values (looks like this: X|X,X|X, ..)
        for str in string.gmatch(customizations["cus_" .. baseId], "([^,]+)") do
          table.insert(index_xp, str)
        end

        -- split each pair and assign it to the proper position in markedBoxes
        if (customizationRowsWithFields[baseId] ~= nil) then
          for i = 1, customizationRowsWithFields[baseId].inputCount do
            table.insert(inputValues, "")
          end
        end
        local inputCount = 0
        for _, entry in ipairs(index_xp) do
          local counter = 0
          local index   = 0

          -- if found number is 0, then only get inputvalue
          for str in string.gmatch(entry, "([^|]+)") do
            counter = counter + 1
            if counter == 1 then
              index = tonumber(str) + 1
            elseif counter == 2 then
              markedBoxes[index] = tonumber(str)
            elseif counter == 3 and str ~= "" then
              if (baseId == "09042") then
                inputValues[customizationRowsWithFields[baseId].inputMap[index]] = convertRavenQuillSelections(str)
              elseif customizationRowsWithFields[baseId] ~= nil then
                inputValues[customizationRowsWithFields[baseId].inputMap[index]] = str
              end
            end
          end
        end

        -- remove first entry in markedBoxes if row 0 has textbox
        if customizationRowsWithFields[baseId] ~= nil
            and customizationRowsWithFields[baseId].inputCount > 0 then
          table.remove(markedBoxes, 1)
        end

        -- write the loaded values to the save_data of the sheets
        card.data["LuaScriptState"] = JSON.encode({ markedBoxes, inputValues })
      end
    end
  end
end

function log(message)
  if DEBUG then print(message) end
end
end)
__bundle_register("arkhamdb/ArkhamDb", function(require, _LOADED, __bundle_register, __bundle_modules)
do
  local playAreaApi = require("core/PlayAreaApi")
  local ArkhamDb = { }
  local internal = { }

  local RANDOM_WEAKNESS_ID = "01000"

  local tabooList = { }
  --Forward declaration
  ---@type Request
  local Request = {}
  local configuration

  -- Sets up the ArkhamDb interface.  Should be called from the parent object on load.
  ArkhamDb.initialize = function()
    configuration = internal.getConfiguration()
    Request.start({ configuration.api_uri, configuration.taboo }, function(status)
      local json = JSON.decode(internal.fixUtf16String(status.text))
      for _, taboo in pairs(json) do
        ---@type <string, boolean>
        local cards = {}

        for _, card in pairs(JSON.decode(taboo.cards)) do
          cards[card.code] = true
        end

        tabooList[taboo.id] = {
          date = taboo.date_start,
          cards = cards
        }
      end
      return true, nil
    end)
  end

  -- Start the deck build process for the given player color and deck ID.  This
  -- will retrieve the deck from ArkhamDB, and pass to a callback for processing.
  ---@param playerColor String.  Color name of the player mat to place this deck on (e.g. "Red").
  ---@param deckId String.  ArkhamDB deck id to be loaded
  ---@param isPrivate Boolean.  Whether this deck is published or private on ArkhamDB
  ---@param loadNewest Boolean.  Whether the newest version of this deck should be loaded
  ---@param loadInvestigators Boolean.  Whether investigator cards should be loaded as part of this
  ---    deck
  ---@param callback Function.  Callback which will be sent the results of this load.  Parameters
  ---    to the callback will be:
  ---        slots Table.  A map of card ID to count in the deck
  ---        investigatorCode String.  ID of the investigator in this deck
  ---        customizations Table.  The decoded table of customization upgrades in this deck
  ---        playerColor String.  Color this deck is being loaded for
  ArkhamDb.getDecklist = function(
      playerColor,
      deckId,
      isPrivate,
      loadNewest,
      loadInvestigators,
      callback)
    -- Get a simple card to see if the bag indexes are complete. If not, abort
    -- the deck load. The called method will handle player notification.
    local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
    local checkCard = allCardsBag.call("getCardById", { id = "01001" })
    if (checkCard ~= nil and checkCard.data == nil) then
      return
    end

    local deckUri = { configuration.api_uri,
      isPrivate and configuration.private_deck or configuration.public_deck, deckId }

    local deck = Request.start(deckUri, function(status)
      if string.find(status.text, "<!DOCTYPE html>") then
        internal.maybePrint("Private deck ID " .. deckId .. " is not shared", playerColor)
        return false, table.concat({ "Private deck ", deckId, " is not shared" })
      end
      local json = JSON.decode(status.text)

      if not json then
        internal.maybePrint("Deck ID " .. deckId .. " not found", playerColor)
        return false, "Deck not found!"
      end

      return true, JSON.decode(status.text)
    end)

    deck:with(internal.onDeckResult, playerColor, loadNewest, loadInvestigators, callback)
  end

  -- Logs that a card could not be loaded in the mod by printing it to the console in the given
  -- color of the player owning the deck.  Attempts to look up the name on ArkhamDB for clarity,
  -- but prints the card ID if the name cannot be retrieved.
  ---@param cardId String.  ArkhamDB ID of the card that could not be found
  ---@param playerColor String.  Color of the player's deck that had the problem
  ArkhamDb.logCardNotFound = function(cardId, playerColor)
    local request = Request.start({
      configuration.api_uri,
      configuration.cards,
      cardId
    },
      function(result)
        local adbCardInfo = JSON.decode(internal.fixUtf16String(result.text))
        local cardName = adbCardInfo.real_name
        if (cardName ~= nil) then
          if (adbCardInfo.xp ~= nil and adbCardInfo.xp > 0) then
            cardName = cardName .. " (" .. adbCardInfo.xp .. ")"
          end
          internal.maybePrint("Card not found: " .. cardName .. ", ArkhamDB ID " .. cardId, playerColor)
        else
          internal.maybePrint("Card not found in ArkhamDB, ID " .. cardId, playerColor)
        end
      end)
  end

  -- Callback when the deck information is received from ArkhamDB.  Parses the
  -- response then applies standard transformations to the deck such as adding
  -- random weaknesses and checking for taboos.  Once the deck is processed,
  -- passes to loadCards to actually spawn the defined deck.
  ---@param deck ArkhamImportDeck
  ---@param playerColor String Color name of the player mat to place this deck on (e.g. "Red")
  ---@param loadNewest Boolean Whether the newest version of this deck should be loaded
  ---@param loadInvestigators Boolean Whether investigator cards should be loaded as part of this
  ---    deck
  ---@param callback Function Callback which will be sent the results of this load.  Parameters
  ---    to the callback will be:
  ---        slots Table.  A map of card ID to count in the deck
  ---        investigatorCode String.  ID of the investigator in this deck
  ---        bondedList A table of cardID keys to meaningless values.  Card IDs in this list were
  ---        added from a parent bonded card.
  ---        customizations Table.  The decoded table of customization upgrades in this deck
  ---        playerColor String.  Color this deck is being loaded for
  internal.onDeckResult = function(deck, playerColor, loadNewest, loadInvestigators, callback)
    -- Load the next deck in the upgrade path if the option is enabled
    if (loadNewest and deck.next_deck ~= nil and deck.next_deck ~= "") then
      buildDeck(playerColor, deck.next_deck)
      return
    end

    internal.maybePrint(table.concat({ "Found decklist: ", deck.name }), playerColor)

    -- Initialize deck slot table and perform common transformations.  The order of these should not
    -- be changed, as later steps may act on cards added in each.  For example, a random weakness or
    -- investigator may have bonded cards or taboo entries, and should be present
    local slots = deck.slots
    internal.maybeDrawRandomWeakness(slots, playerColor)
    if loadInvestigators then
      internal.addInvestigatorCards(deck, slots)
    end
    internal.maybeAddCustomizeUpgradeSheets(slots)
    internal.maybeAddSummonedServitor(slots)
    internal.maybeAddOnTheMend(slots, playerColor)
    local bondList = internal.extractBondedCards(slots)
    internal.checkTaboos(deck.taboo_id, slots, playerColor)

    -- get upgrades for customizable cards
    local meta = deck.meta
    local customizations = {}
    if meta then
      customizations = JSON.decode(meta)
    end

    callback(slots, deck.investigator_code, bondList, customizations, playerColor)
  end

  -- Checks to see if the slot list includes the random weakness ID. If it does,
  -- removes it from the deck and replaces it with the ID of a random basic weakness provided by the
  -- all cards bag
  ---@param slots Table The slot list for cards in this deck. Table key is the cardId, value is the number
  ---    of those cards which will be spawned
  ---@param playerColor String Color of the player this deck is being loaded for. Used for broadcast
  ---    if a weakness is added.
  internal.maybeDrawRandomWeakness = function(slots, playerColor)
    local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
    local hasRandomWeakness = false
    for cardId, cardCount in pairs(slots) do
      if cardId == RANDOM_WEAKNESS_ID then
        hasRandomWeakness = true
        break
      end
    end
    if hasRandomWeakness then
      local weaknessId = allCardsBag.call("getRandomWeaknessId")
      slots[weaknessId] = 1
      slots[RANDOM_WEAKNESS_ID] = nil
      internal.maybePrint("Random basic weakness added to deck", playerColor)
    end
  end

  -- Adds both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each
  ---@param deck Table The processed ArkhamDB deck response
  ---@param slots Table The slot list for cards in this deck.  Table key is the cardId, value is the
  ---    number of those cards which will be spawned
  internal.addInvestigatorCards = function(deck, slots)
    local investigatorId = deck.investigator_code
    slots[investigatorId .. "-m"] = 1
    local deckMeta = JSON.decode(deck.meta)
    local parallelFront = deckMeta ~= nil and deckMeta.alternate_front ~= nil and deckMeta.alternate_front ~= ""
    local parallelBack = deckMeta ~= nil and deckMeta.alternate_back ~= nil and deckMeta.alternate_back ~= ""
    if parallelFront and parallelBack then
      investigatorId = investigatorId .. "-p"
    elseif parallelFront then
      local alternateNum = tonumber(deckMeta.alternate_front)
      if alternateNum >= 01501 and alternateNum <= 01506 then
        investigatorId = investigatorId .. "-r"
      else
        investigatorId = investigatorId .. "-pf"
      end
    elseif parallelBack then
      investigatorId = investigatorId .. "-pb"
    end
    slots[investigatorId] = 1
  end

  -- Process the card list looking for the customizable cards, and add their upgrade sheets if needed
  ---@param slots Table The slot list for cards in this deck. Table key is the cardId, value is the number
  --     of those cards which will be spawned
  internal.maybeAddCustomizeUpgradeSheets = function(slots)
    local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
    for cardId, _ in pairs(slots) do
      -- upgrade sheets for customizable cards
      local upgradesheet = allCardsBag.call("getCardById", { id = cardId .. "-c" })
      if upgradesheet ~= nil then
        slots[cardId .. "-c"] = 1
      end
    end
  end

  -- Process the card list looking for the Summoned Servitor, and add its minicard to the list if
  -- needed
  ---@param slots Table The slot list for cards in this deck. Table key is the cardId, value is the number
  --     of those cards which will be spawned
  internal.maybeAddSummonedServitor = function(slots)
    if slots["09080"] ~= nil then
      slots["09080-m"] = 1
    end
  end

  -- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1.  Update
  -- the count based on the investigator count
  ---@param slots Table The slot list for cards in this deck. Table key is the cardId, value is the number
  --     of those cards which will be spawned
  ---@param playerColor String Color of the player this deck is being loaded for. Used for broadcast if an error occurs
  internal.maybeAddOnTheMend = function(slots, playerColor)
    if slots["09006"] ~= nil then
      local investigatorCount = playAreaApi.getInvestigatorCount()
      if investigatorCount ~= nil then
        slots["09006"] = investigatorCount
      else
        internal.maybePrint("Something went wrong with the load, adding 4 copies of On the Mend", playerColor)
        slots["09006"] = 4
      end
    end
  end

  -- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.
  ---@param slots Table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
  internal.extractBondedCards = function(slots)
    local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
    -- Create a list of bonded cards first so we don't modify slots while iterating
    local bondedCards = { }
    local bondedList = { }
    for cardId, cardCount in pairs(slots) do
      local card = allCardsBag.call("getCardById", { id = cardId })
      if (card ~= nil and card.metadata.bonded ~= nil) then
        for _, bond in ipairs(card.metadata.bonded) do
          bondedCards[bond.id] = bond.count
          -- We need to know which cards are bonded to determine their position, remember them
          bondedList[bond.id] = true
          -- Also adding taboo versions of bonded cards to the list
          bondedList[bond.id .. "-t"] = true
        end
      end
    end
    -- Add any bonded cards to the main slots list
    for bondedId, bondedCount in pairs(bondedCards) do
      slots[bondedId] = bondedCount
    end

    return bondedList
  end

  -- Check the deck for cards on its taboo list. If they're found, replace the entry in the slot with the Taboo id (i.e. "XXXX" becomes "XXXX-t")
  ---@param tabooId String The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used
  ---@param slots Table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned
  internal.checkTaboos = function(tabooId, slots, playerColor)
    if tabooId then
      local allCardsBag = getObjectFromGUID(configuration.card_bag_guid)
      for cardId, _ in pairs(tabooList[tabooId].cards) do
        if slots[cardId] ~= nil then
          -- Make sure there's a taboo version of the card before we replace it
          -- SCED only maintains the most recent taboo cards. If a deck is using
          -- an older taboo list it's possible the card isn't a taboo any more
          local tabooCard = allCardsBag.call("getCardById", { id = cardId .. "-t" })
          if tabooCard == nil then
            local basicCard = allCardsBag.call("getCardById", { id = cardId })
            internal.maybePrint("Taboo version for " .. basicCard.data.Nickname .. " is not available. Using standard version", playerColor)
          else
            slots[cardId .. "-t"] = slots[cardId]
            slots[cardId] = nil
          end
        end
      end
    end
  end

  internal.maybePrint = function(message, playerColor)
    if playerColor ~= "None" then
      printToAll(message, playerColor)
    end
  end

  -- Gets the ArkhamDB config info from the configuration object.
  ---@return Table.  Configuration data
  internal.getConfiguration = function()
    local configuration = getObjectsWithTag("import_configuration_provider")[1]:getTable("configuration")
    printPriority = configuration.priority
    return configuration
  end

  internal.fixUtf16String = function(str)
    return str:gsub("\\u(%w%w%w%w)", function(match)
      return string.char(tonumber(match, 16))
    end)
  end

  ---@type Request
  Request = {
    is_done = false,
    is_successful = false
  }

  -- Creates a new instance of a Request. Should not be directly called. Instead use Request.start and Request.deferred.
  ---@param uri string
  ---@param configure fun(request: Request, status: WebRequestStatus)
  ---@return Request
  function Request:new(uri, configure)
    local this = {}

    setmetatable(this, self)
    self.__index = self

    if type(uri) == "table" then
      uri = table.concat(uri, "/")
    end

    this.uri = uri

    WebRequest.get(uri, function(status)
      configure(this, status)
    end)

    return this
  end

  -- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.
  -- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)
  ---@param uri string
  ---@param on_success fun(request: Request, status: WebRequestStatus, vararg any)
  ---@param on_error fun(status: WebRequestStatus)|nil
  ---@vararg any[]
  ---@return Request
  function Request.deferred(uri, on_success, on_error, ...)
    local parameters = table.pack(...)
    return Request:new(uri, function(request, status)
      if (status.is_done) then
        if (status.is_error) then
          request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error
          request.is_successful = false
          request.is_done = true
        else
          on_success(request, status)
        end
      end
    end)
  end

  -- Creates a new request. on_success should return weather the resultant data is as expected, and the processed content of the request.
  ---@param uri string
  ---@param on_success fun(status: WebRequestStatus, vararg any): boolean, any
  ---@param on_error nil|fun(status: WebRequestStatus, vararg any): string
  ---@vararg any[]
  ---@return Request
  function Request.start(uri, on_success, on_error, ...)
    local parameters = table.pack(...)
    return Request.deferred(uri, function(request, status)
      local result, message = on_success(status, table.unpack(parameters))
      if not result then request.error_message = message else request.content = message end
      request.is_successful = result
      request.is_done = true
    end, on_error, table.unpack(parameters))
  end

  ---@param requests Request[]
  ---@param on_success fun(content: any[], vararg any[])
  ---@param on_error fun(requests: Request[], vararg any[])|nil
  ---@vararg any
  function Request.with_all(requests, on_success, on_error, ...)
    local parameters = table.pack(...)

    Wait.condition(function()
      ---@type any[]
      local results = {}

      ---@type Request[]
      local errors = {}

      for _, request in ipairs(requests) do
        if request.is_successful then
          table.insert(results, request.content)
        else
          table.insert(errors, request)
        end
      end

      if (#errors <= 0) then
        on_success(results, table.unpack(parameters))
      elseif on_error == nil then
        for _, request in ipairs(errors) do
          internal.maybePrint(table.concat({ "[ERROR]", request.uri, ":", request.error_message }))
        end
      else
        on_error(requests, table.unpack(parameters))
      end
    end, function()
      for _, request in ipairs(requests) do
        if not request.is_done then return false end
      end
      return true
    end)
  end

  ---@param callback fun(content: any, vararg any)
  function Request:with(callback, ...)
    local arguments = table.pack(...)
    Wait.condition(function()
      if self.is_successful then
        callback(self.content, table.unpack(arguments))
      end
    end, function() return self.is_done
    end)
  end

  return ArkhamDb
end
end)
__bundle_register("arkhamdb/DeckImporterUi", function(require, _LOADED, __bundle_register, __bundle_modules)
local INPUT_FIELD_HEIGHT = 340
local INPUT_FIELD_WIDTH = 1500
local FIELD_COLOR = { 0.9, 0.7, 0.5 }

local PRIVATE_TOGGLE_LABELS = {}
PRIVATE_TOGGLE_LABELS[true] = "Private"
PRIVATE_TOGGLE_LABELS[false] = "Published"

local UPGRADED_TOGGLE_LABELS = {}
UPGRADED_TOGGLE_LABELS[true] = "Upgraded"
UPGRADED_TOGGLE_LABELS[false] = "Specific"

local LOAD_INVESTIGATOR_TOGGLE_LABELS = {}
LOAD_INVESTIGATOR_TOGGLE_LABELS[true] = "Yes"
LOAD_INVESTIGATOR_TOGGLE_LABELS[false] = "No"

local redDeckId = ""
local orangeDeckId = ""
local whiteDeckId = ""
local greenDeckId = ""

local privateDeck = true
local loadNewestDeck = true
local loadInvestigators = false

-- Returns a table with the full state of the UI, including options and deck IDs.
-- This can be used to persist via onSave(), or provide values for a load operation
-- Table values:
--   redDeck: Deck ID to load for the red player
--   orangeDeck: Deck ID to load for the orange player
--   whiteDeck: Deck ID to load for the white player
--   greenDeck: Deck ID to load for the green player
--   private: True to load a private deck, false to load a public deck
--   loadNewest: True if the most upgraded version of the deck should be loaded
--   investigators: True if investigator cards should be spawned
function getUiState()
  return {
    redDeck = redDeckId,
    orangeDeck = orangeDeckId,
    whiteDeck = whiteDeckId,
    greenDeck = greenDeckId,
    private = privateDeck,
    loadNewest = loadNewestDeck,
    investigators = loadInvestigators
  }
end

-- Sets up the UI for the deck loader, populating fields from the given save state table decoded from onLoad()
function initializeUi(savedUiState)
  if savedUiState ~= nil then
    redDeckId = savedUiState.redDeck
    orangeDeckId = savedUiState.orangeDeck
    whiteDeckId = savedUiState.whiteDeck
    greenDeckId = savedUiState.greenDeck
    privateDeck = savedUiState.private
    loadNewestDeck = savedUiState.loadNewest
    loadInvestigators = savedUiState.investigators
  end

  makeOptionToggles()
  makeDeckIdFields()
  makeBuildButton()
end

function makeOptionToggles()
  -- common parameters
  local checkbox_parameters = {}
  checkbox_parameters.function_owner = self
  checkbox_parameters.width = INPUT_FIELD_WIDTH
  checkbox_parameters.height = INPUT_FIELD_HEIGHT
  checkbox_parameters.scale = { 0.1, 0.1, 0.1 }
  checkbox_parameters.font_size = 240
  checkbox_parameters.hover_color = { 0.4, 0.6, 0.8 }
  checkbox_parameters.color = FIELD_COLOR

  -- public / private deck
  checkbox_parameters.click_function = "publicPrivateChanged"
  checkbox_parameters.position = { 0.25, 0.1, -0.102 }
  checkbox_parameters.tooltip = "Published or private deck?\n\nPLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!"
  checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck]
  self.createButton(checkbox_parameters)

  -- load upgraded?
  checkbox_parameters.click_function = "loadUpgradedChanged"
  checkbox_parameters.position = { 0.25, 0.1, -0.01 }
  checkbox_parameters.tooltip = "Load newest upgrade or exact deck?"
  checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck]
  self.createButton(checkbox_parameters)

  -- load investigators?
  checkbox_parameters.click_function = "loadInvestigatorsChanged"
  checkbox_parameters.position = { 0.25, 0.1, 0.081 }
  checkbox_parameters.tooltip = "Spawn investigator cards?"
  checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators]
  self.createButton(checkbox_parameters)
end

-- Create the four deck ID entry fields
function makeDeckIdFields()
  local input_parameters = {}
  -- Parameters common to all entry fields
  input_parameters.function_owner = self
  input_parameters.scale = { 0.1, 0.1, 0.1 }
  input_parameters.width = INPUT_FIELD_WIDTH
  input_parameters.height = INPUT_FIELD_HEIGHT
  input_parameters.font_size = 320
  input_parameters.tooltip = "Deck ID from ArkhamDB URL of the deck\nPublic URL: 'https://arkhamdb.com/decklist/view/101/knowledge-overwhelming-solo-deck-1.0' = '101'\nPrivate URL: 'https://arkhamdb.com/deck/view/102' = '102'"
  input_parameters.alignment = 3 -- Center
  input_parameters.color = FIELD_COLOR
  input_parameters.font_color = { 0, 0, 0 }
  input_parameters.validation = 2 -- Integer

  -- Green
  input_parameters.input_function = "greenDeckChanged"
  input_parameters.position = { -0.166, 0.1, 0.385 }
  input_parameters.value = greenDeckId
  self.createInput(input_parameters)
  -- Red
  input_parameters.input_function = "redDeckChanged"
  input_parameters.position = { 0.171, 0.1, 0.385 }
  input_parameters.value = redDeckId
  self.createInput(input_parameters)
  -- White
  input_parameters.input_function = "whiteDeckChanged"
  input_parameters.position = { -0.166, 0.1, 0.474 }
  input_parameters.value = whiteDeckId
  self.createInput(input_parameters)
  -- Orange
  input_parameters.input_function = "orangeDeckChanged"
  input_parameters.position = { 0.171, 0.1, 0.474 }
  input_parameters.value = orangeDeckId
  self.createInput(input_parameters)
end

-- Create the Build All button. This is a transparent button which covers the Build All portion of the background graphic
function makeBuildButton()
  local button_parameters = {}
  button_parameters.click_function = "loadDecks"
  button_parameters.function_owner = self
  button_parameters.position = { 0, 0.1, 0.71 }
  button_parameters.width = 320
  button_parameters.height = 30
  button_parameters.color = { 0, 0, 0, 0 }
  button_parameters.tooltip = "Click to build all four decks!"
  self.createButton(button_parameters)
end

-- Event handlers for deck ID change
function redDeckChanged(_, _, inputValue) redDeckId = inputValue end

function orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end

function whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end

function greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end

-- Event handlers for toggle buttons
function publicPrivateChanged()
  privateDeck = not privateDeck
  self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck] }
end

function loadUpgradedChanged()
  loadNewestDeck = not loadNewestDeck
  self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] }
end

function loadInvestigatorsChanged()
  loadInvestigators = not loadInvestigators
  self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] }
end

function loadDecks()
  -- testLoadLotsOfDecks()
  -- Method in DeckImporterMain, visible due to inclusion

  -- TODO: Make this use the configuration ID for the all cards bag
  local allCardsBag = getObjectFromGUID("15bb07")
  local indexReady = allCardsBag.call("isIndexReady")
  if (not indexReady) then
    broadcastToAll("Still loading player cards, please try again in a few seconds", {0.9, 0.2, 0.2})
    return
  end
  if (redDeckId ~= nil and redDeckId ~= "") then
    buildDeck("Red", redDeckId)
  end
  if (orangeDeckId ~= nil and orangeDeckId ~= "") then
    buildDeck("Orange", orangeDeckId)
  end
  if (whiteDeckId ~= nil and whiteDeckId ~= "") then
    buildDeck("White", whiteDeckId)
  end
  if (greenDeckId ~= nil and greenDeckId ~= "") then
    buildDeck("Green", greenDeckId)
  end
end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
  local PlayAreaApi = { }

  local PLAY_AREA_GUID = "721ba2"

  -- Returns the current value of the investigator counter from the playmat
  ---@return Integer.  Number of investigators currently set on the counter
  PlayAreaApi.getInvestigatorCount = function()
    return getObjectFromGUID(PLAY_AREA_GUID).call("getInvestigatorCount")
  end

  -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction.  Certain
  -- fixed objects will be ignored, as will anything the player has tagged with
  -- 'displacement_excluded'
  ---@param playerColor Color of the player requesting the shift.  Used solely to send an error
  ---    message in the unlikely case that the scripting zone has been deleted
  PlayAreaApi.shiftContentsUp = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsUp", playerColor)
  end

  PlayAreaApi.shiftContentsDown = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsDown", playerColor)
  end

  PlayAreaApi.shiftContentsLeft = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsLeft", playerColor)
  end

  PlayAreaApi.shiftContentsRight = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsRight", playerColor)
  end

  -- Reset the play area's tracking of which cards have had tokens spawned.
  PlayAreaApi.resetSpawnedCards = function()
    return getObjectFromGUID(PLAY_AREA_GUID).call("resetSpawnedCards")
  end

  -- Event to be called when the current scenario has changed.
  ---@param scenarioName Name of the new scenario
  PlayAreaApi.onScenarioChanged = function(scenarioName)
    getObjectFromGUID(PLAY_AREA_GUID).call("onScenarioChanged", scenarioName)
  end

  -- Sets this playmat's snap points to limit snapping to locations or not.
  -- If matchTypes is false, snap points will be reset to snap all cards.
  ---@param matchTypes Boolean  Whether snap points should only snap for the matching card types.
  PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)
    getObjectFromGUID(PLAY_AREA_GUID).call("setLimitSnapsByType", matchCardTypes)
  end

  -- Receiver for the Global tryObjectEnterContainer event.  Used to clear vector lines from dragged
  -- cards before they're destroyed by entering the container
  PlayAreaApi.tryObjectEnterContainer = function(container, object)
    getObjectFromGUID(PLAY_AREA_GUID).call("tryObjectEnterContainer",
        { container = container, object = object })
  end

  return PlayAreaApi
end
end)
__bundle_register("playercards/PlayerCardSpawner", function(require, _LOADED, __bundle_register, __bundle_modules)

-- Amount to shift for the next card (zShift) or next row of cards (xShift)
-- Note that the table rotation is weird, and the X axis is vertical while the
-- Z axis is horizontal
local SPREAD_Z_SHIFT = -2.3
local SPREAD_X_SHIFT = -3.66

Spawner = { }

-- Spawns a list of cards at the given position/rotation.  This will separate cards by size -
-- investigator, standard, and mini, spawning them in that order with larger cards on bottom.  If
-- there are different types, the provided callback will be called once for each type as it spawns
-- either a card or deck.
-- @param cardList: A list of Player Card data structures (data/metadata)
-- @param pos Position table where the cards should be spawned (global)
-- @param rot Rotation table for the orientation of the spawned cards (global)
-- @param sort Boolean, true if this list of cards should be sorted before spawning
-- @param callback Function, callback to be called after the card/deck spawns.
Spawner.spawnCards = function(cardList, pos, rot, sort, callback)
  if (sort) then
    table.sort(cardList, Spawner.cardComparator)
  end

  local miniCards = { }
  local standardCards = { }
  local investigatorCards = { }

  for _, card in ipairs(cardList) do
    if (card.metadata.type == "Investigator") then
      table.insert(investigatorCards, card)
    elseif (card.metadata.type == "Minicard") then
      table.insert(miniCards, card)
    else
      table.insert(standardCards, card)
    end
  end
  -- Spawn each of the three types individually.  Each Y position shift accounts for the thickness
  -- of the spawned deck
  local position = { x = pos.x, y = pos.y, z = pos.z }
  Spawner.spawn(investigatorCards, position, { rot.x, rot.y - 90, rot.z}, callback)

  position.y = position.y + (#investigatorCards + #standardCards) * 0.07
  Spawner.spawn(standardCards, position, rot, callback)

  position.y = position.y + (#standardCards + #miniCards) * 0.07
  Spawner.spawn(miniCards, position, rot, callback)
end

Spawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)
  if (sort) then
    table.sort(cardList, Spawner.cardComparator)
  end

  local position = { x = startPos.x, y = startPos.y, z = startPos.z }
  -- Special handle the first row if we have less than a full single row, but only if there's a
  -- reasonable max column count.  Single-row spreads will send a large value for maxCols
  if maxCols < 100 and #cardList < maxCols then
    position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)
  end
  local cardsInRow = 0
  local rows = 0
  for _, card in ipairs(cardList) do
    Spawner.spawn({ card }, position, rot, callback)
    position.z = position.z + SPREAD_Z_SHIFT
    cardsInRow = cardsInRow + 1
    if cardsInRow >= maxCols then
      rows = rows + 1
      local cardsForRow = #cardList - rows * maxCols
      if cardsForRow > maxCols then
        cardsForRow = maxCols
      end
      position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)
      position.x = position.x + SPREAD_X_SHIFT
      cardsInRow = 0
    end
  end
end

-- Spawn a specific list of cards.  This method is for internal use and should not be called
-- directly, use spawnCards instead.
-- @param cardList: A list of Player Card data structures (data/metadata)
-- @param pos Position table where the cards should be spawned (global)
-- @param rot Rotation table for the orientation of the spawned cards (global)
-- @param callback Function, callback to be called after the card/deck spawns.
Spawner.spawn = function(cardList, pos, rot, callback)
  if (#cardList == 0) then
    return
  end
  -- Spawn a single card directly
  if (#cardList == 1) then
    spawnObjectData({
      data = cardList[1].data,
      position = pos,
      rotation = rot,
      callback_function = callback,
    })
    return
  end
  -- For multiple cards, construct a deck and spawn that
  local deck = Spawner.buildDeckDataTemplate()
  -- Decks won't inherently scale to the cards in them.  The card list being spawned should be all
  -- the same type/size by this point, so use the first card to set the size
  deck.Transform = {
    scaleX = cardList[1].data.Transform.scaleX,
    scaleY = 1,
    scaleZ = cardList[1].data.Transform.scaleZ,
  }
  for _, spawnCard in ipairs(cardList) do
    Spawner.addCardToDeck(deck, spawnCard.data)
  end
  spawnObjectData({
    data = deck,
    position = pos,
    rotation = rot,
    callback_function = callback,
  })
end

-- Inserts a card into the given deck.  This does three things:
--   1. Add the card's data to ContainedObjects
--   2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's
--      ID list.  Note that the deck's ID list is "DeckIDs" even though it
--      contains a list of card Ids
--   3. Extract the card's CustomDeck table and add it to the deck.  The deck's
--      "CustomDeck" field is a list of all CustomDecks used by cards within the
--      deck, keyed by the DeckID and referencing the custom deck table
---@param deck: TTS deck data structure to add to
---@param card: Data for the card to be inserted
Spawner.addCardToDeck = function(deck, cardData)
  for customDeckId, customDeckData in pairs(cardData.CustomDeck) do
    if (deck.CustomDeck[customDeckId] == nil) then
      -- CustomDeck not added to deck yet, add it
      deck.CustomDeck[customDeckId] = customDeckData
    elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then
      -- CustomDeck for this card matches the current one for the deck, do nothing
    else
      -- CustomDeck data conflict
      local newDeckId = nil
      for deckId, customDeck in pairs(deck.CustomDeck) do
        if (customDeckData.FaceURL == customDeck.FaceURL) then
          newDeckId = deckId
        end
      end
      if (newDeckId == nil) then
        -- No non-conflicting custom deck for this card, add a new one
        newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, "1000")
        deck.CustomDeck[newDeckId] = customDeckData
      end
      -- Update the card with the new CustomDeck info
      cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)
      cardData.CustomDeck[customDeckId] = nil
      cardData.CustomDeck[newDeckId] = customDeckData
      break
    end
  end
  table.insert(deck.ContainedObjects, cardData)
  table.insert(deck.DeckIDs, cardData.CardID)
end

-- Create an empty deck data table which can have cards added to it. This
-- creates a new table on each call without using metatables or previous
-- definitions because we can't be sure that TTS doesn't modify the structure
---@return: Table containing the minimal TTS deck data structure
Spawner.buildDeckDataTemplate = function()
  local deck = {}
  deck.Name = "Deck"

  -- Card data.  DeckIDs and CustomDeck entries will be built from the cards
  deck.ContainedObjects = {}
  deck.DeckIDs = {}
  deck.CustomDeck = {}

  -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here
  deck.Transform = {
    scaleX = 1,
    scaleY = 1,
    scaleZ = 1,
  }

  return deck
end

-- Returns the first ID which does not exist in the given table, starting at startId and increasing
-- @param objectTable Table keyed by strings which are numbers
-- @param startId First possible ID.
-- @return String ID >= startId
Spawner.findNextAvailableId = function(objectTable, startId)
  local id = startId
  while (objectTable[id] ~= nil) do
    id = tostring(tonumber(id) + 1)
  end

  return id
end

-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.
---@return: 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are
--      irrelevant as they provide only grouping and the order between them doesn't matter.
Spawner.getpbcn = function(metadata)
  if metadata.permanent then
    return 1
  elseif metadata.bonded_to ~= nil then
    return 2
  else -- Normal card
    return 3
  end
end

-- Comparison function used to sort the cards in a deck.  Groups bonded or
-- permanent cards first, then sorts within theose types by name/subname.
-- Normal cards will sort in standard alphabetical order, while
-- permanent/bonded/customizable will be in reverse alphabetical order.
--
-- Since cards spawn in the order provided by this comparator, with the first
-- cards ending up at the bottom of a pile, this ordering will spawn in reverse
-- alphabetical order.  This presents the cards in order for non-face-down
-- areas, and presents them in order when Searching the face-down deck.
Spawner.cardComparator = function(card1, card2)
  local pbcn1 = Spawner.getpbcn(card1.metadata)
  local pbcn2 = Spawner.getpbcn(card2.metadata)
  if pbcn1 ~= pbcn2 then
    return pbcn1 > pbcn2
  end
  if pbcn1 == 3 then
    if card1.data.Nickname ~= card2.data.Nickname then
      return card1.data.Nickname < card2.data.Nickname
    end
    return card1.data.Description < card2.data.Description
  else
    if card1.data.Nickname ~= card2.data.Nickname then
      return card1.data.Nickname > card2.data.Nickname
    end
    return card1.data.Description > card2.data.Description
  end
end
end)
__bundle_register("playermat/Zones", function(require, _LOADED, __bundle_register, __bundle_modules)
-- Sets up and returns coordinates for all possible spawn zones. Because Lua assigns tables by reference
-- and there is no built-in function to copy a table this is relatively brute force.
--
-- Positions are all relative to the player mat, and most are consistent. The
-- exception are the SetAside# zones, which are placed to the left of the mat
-- for White/Green, and the right of the mat for Orange/Red.
--
-- Investigator: Investigator card area.
-- Minicard: Placement for the investigator's minicard, just above the player mat
-- Deck, Discard: Standard locations for the deck and discard piles.
-- BlankTop: used for assets that start in play (e.g. Duke)
-- Tarot, Hand1, Hand2, Ally, BlankBottom, Accessory, Arcane1, Arcane2, Body: Asset slot positions
-- Threat[1-4]: Threat area slots. Threat[1-3] correspond to the named threat area slots, and Threat4 is the blank threat area slot.
-- SetAside[1-3]: Column closest to the player mat, with 1 at the top and 3 at the bottom.
-- SetAside[4-6]: Column farther away from the mat, with 4 at the top and 6 at the bottom.
-- SetAside1: Permanent cards
-- SetAside2: Bonded cards
-- SetAside3: Ancestral Knowledge / Underworld Market
-- SetAside4: Upgrade sheets for customizable cards
-- SetAside5: Hunch Deck for Joe Diamond
-- SetAside6: currently unused
do
  local Zones = { }

  local playerMatGuids     = {}
  playerMatGuids["Red"]    = "0840d5"
  playerMatGuids["Orange"] = "bd0ff4"
  playerMatGuids["White"]  = "8b081b"
  playerMatGuids["Green"]  = "383d8b"

  local commonZones                 = {}
  commonZones["Investigator"] = { -1.17702, 0, 0.00209 }
  commonZones["Minicard"]     = { -0.16, 0, -1.222326 }
  commonZones["Deck"]         = { -1.822724, 0, -0.02940192 }
  commonZones["Discard"]      = { -1.822451, 0, 0.6092291 }
  commonZones["Ally"]         = { -0.6157398, 0, 0.02435675 }
  commonZones["Body"]         = { -0.6306521, 0, 0.553170 }
  commonZones["Hand1"]        = { 0.2155387, 0, 0.04257287 }
  commonZones["Hand2"]        = { -0.1803701, 0, 0.03745948 }
  commonZones["Arcane1"]      = { 0.2124223, 0, 0.5596902 }
  commonZones["Arcane2"]      = { -0.1711275, 0, 0.5567944 }
  commonZones["Tarot"]        = { 0.6016169, 0, 0.03273106 }
  commonZones["Accessory"]    = { 0.6049907, 0, 0.5546234 }
  commonZones["BlankTop"]     = { 1.758446, 0, 0.03965336 }
  commonZones["BlankBottom"]  = { 1.754469, 0, 0.5634764 }
  commonZones["Threat1"]      = { -0.9116555, 0, -0.6446251 }
  commonZones["Threat2"]      = { -0.4544126, 0, -0.6428719 }
  commonZones["Threat3"]      = { 0.002246313, 0, -0.6430681 }
  commonZones["Threat4"]      = { 0.4590618, 0, -0.6432732 }

  local zoneData                            = {}
  zoneData["White"]                   = {}
  zoneData["White"]["Investigator"]   = commonZones["Investigator"]
  zoneData["White"]["Minicard"]       = commonZones["Minicard"]
  zoneData["White"]["Deck"]           = commonZones["Deck"]
  zoneData["White"]["Discard"]        = commonZones["Discard"]
  zoneData["White"]["Ally"]           = commonZones["Ally"]
  zoneData["White"]["Body"]           = commonZones["Body"]
  zoneData["White"]["Hand1"]          = commonZones["Hand1"]
  zoneData["White"]["Hand2"]          = commonZones["Hand2"]
  zoneData["White"]["Arcane1"]        = commonZones["Arcane1"]
  zoneData["White"]["Arcane2"]        = commonZones["Arcane2"]
  zoneData["White"]["Tarot"]          = commonZones["Tarot"]
  zoneData["White"]["Accessory"]      = commonZones["Accessory"]
  zoneData["White"]["BlankTop"]       = commonZones["BlankTop"]
  zoneData["White"]["BlankBottom"]    = commonZones["BlankBottom"]
  zoneData["White"]["Threat1"]        = commonZones["Threat1"]
  zoneData["White"]["Threat2"]        = commonZones["Threat2"]
  zoneData["White"]["Threat3"]        = commonZones["Threat3"]
  zoneData["White"]["Threat4"]        = commonZones["Threat4"]
  zoneData["White"]["SetAside1"]      = { 2.345893, 0, -0.520315 }
  zoneData["White"]["SetAside2"]      = { 2.345893, 0, 0.042552 }
  zoneData["White"]["SetAside3"]      = { 2.345893, 0, 0.605419 }
  zoneData["White"]["UnderSetAside3"] = { 2.495893, 0, 0.805419 }
  zoneData["White"]["SetAside4"]      = { 2.775893, 0, -0.520315 }
  zoneData["White"]["SetAside5"]      = { 2.775893, 0, 0.042552 }
  zoneData["White"]["SetAside6"]      = { 2.775893, 0, 0.605419 }
  zoneData["White"]["UnderSetAside6"] = { 2.925893, 0, 0.805419 }

  zoneData["Orange"]                   = {}
  zoneData["Orange"]["Investigator"]   = commonZones["Investigator"]
  zoneData["Orange"]["Minicard"]       = commonZones["Minicard"]
  zoneData["Orange"]["Deck"]           = commonZones["Deck"]
  zoneData["Orange"]["Discard"]        = commonZones["Discard"]
  zoneData["Orange"]["Ally"]           = commonZones["Ally"]
  zoneData["Orange"]["Body"]           = commonZones["Body"]
  zoneData["Orange"]["Hand1"]          = commonZones["Hand1"]
  zoneData["Orange"]["Hand2"]          = commonZones["Hand2"]
  zoneData["Orange"]["Arcane1"]        = commonZones["Arcane1"]
  zoneData["Orange"]["Arcane2"]        = commonZones["Arcane2"]
  zoneData["Orange"]["Tarot"]          = commonZones["Tarot"]
  zoneData["Orange"]["Accessory"]      = commonZones["Accessory"]
  zoneData["Orange"]["BlankTop"]       = commonZones["BlankTop"]
  zoneData["Orange"]["BlankBottom"]    = commonZones["BlankBottom"]
  zoneData["Orange"]["Threat1"]        = commonZones["Threat1"]
  zoneData["Orange"]["Threat2"]        = commonZones["Threat2"]
  zoneData["Orange"]["Threat3"]        = commonZones["Threat3"]
  zoneData["Orange"]["Threat4"]        = commonZones["Threat4"]
  zoneData["Orange"]["SetAside1"]      = { -2.350362, 0, -0.520315 }
  zoneData["Orange"]["SetAside2"]      = { -2.350362, 0, 0.042552 }
  zoneData["Orange"]["SetAside3"]      = { -2.350362, 0, 0.605419 }
  zoneData["Orange"]["UnderSetAside3"] = { -2.500362, 0, 0.80419 }
  zoneData["Orange"]["SetAside4"]      = { -2.7803627, 0, -0.520315 }
  zoneData["Orange"]["SetAside5"]      = { -2.7803627, 0, 0.042552 }
  zoneData["Orange"]["SetAside6"]      = { -2.7803627, 0, 0.605419 }
  zoneData["Orange"]["UnderSetAside6"] = { -2.9303627, 0, 0.80419 }

  -- Green positions are the same as White and Red the same as Orange
  zoneData["Red"] = zoneData["Orange"]
  zoneData["Green"] = zoneData["White"]

  -- Gets the global position for the given zone on the specified player mat.
  ---@param playerColor: Color name of the player mat to get the zone position for (e.g. "Red")
  ---@param zoneName: Name of the zone to get the position for.  See Zones object documentation for a list of valid zones.
  ---@return: Global position table, or nil if an invalid player color or zone is specified
  Zones.getZonePosition = function(playerColor, zoneName)
    if (playerColor ~= "Red"
        and playerColor ~= "Orange"
        and playerColor ~= "White"
        and playerColor ~= "Green") then
      return nil
    end
    return getObjectFromGUID(playerMatGuids[playerColor]).positionToWorld(zoneData[playerColor][zoneName])
  end

  -- Return the global rotation for a card on the given player mat, based on its metadata.
  ---@param playerColor: Color name of the player mat to get the rotation for (e.g. "Red")
  ---@param cardMetadata: Table of card metadata. Metadata fields type and permanent are required; all others are optional.
  ---@return: Global rotation vector for the given card. This will include the
  --     Y rotation to orient the card on the given player mat as well as a
  --     Z rotation to place the card face up or face down.
  Zones.getDefaultCardRotation = function(playerColor, zone)
    local deckRotation = getObjectFromGUID(playerMatGuids[playerColor]).getRotation()

    if zone == "Deck" then
      deckRotation = deckRotation + Vector(0, 0, 180)
    end

    return deckRotation
  end

  return Zones
end
end)
return __bundle_require("__root")
