-- 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("playercards/spawnbag/UpgradeSheetBag")
end)
__bundle_register("playercards/spawnbag/UpgradeSheetBag", function(require, _LOADED, __bundle_register, __bundle_modules)
require("playercards/spawnbag/SpawnBag")

local UPGRADES_SPEC = {
  name = "UpgradeSheets",
  cards = {
    "09040-c", -- Alchemical Distillation
    "09023-c", -- Custom Modifications
    "09059-c", -- Damning Testimony
    "09041-c", -- Emperical Hypothesis
    "09060-c", -- Friends in Low Places
    "09101-c", -- Grizzled
    "09061-c", -- Honed Instinct
    "09021-c", -- Hunter's Armor
    "09119-c", -- Hyperphysical Shotcaster
    "09079-c", -- Living Ink
    "09100-c", -- Makeshift Trap
    "09099-c", -- Pocket Multi Tool
    "09081-c", -- Power Word
    "09022-c", -- Runic Axe
    "09080-c", -- Summoned Servitor
    "09042-c", -- Raven's Quill
  },
  globalPos = { x = -42.71, y = 1.5, z = 85.61 },
  rotation = { x = 0, y = 270, z = 0 },
  spread = true,
}
local SERVITOR_SPEC = {
  name = "SummonedServitorMini",
  cards = {
    "09080-m",
  },
  globalPos = { x = -45.84, y = 1.5, z = 53.41 },
  rotation = { x = 0, y = 270, z = 0 },
}

function onLoad(savedData)
  if (savedData ~= nil) then
    local saveState = JSON.decode(savedData)
    if (saveState.spawnBagState ~= nil) then
      SpawnBag.loadFromSave(saveState.spawnBagState)
    end
  end
  createActionButtons()
end

function onSave()
  local saveState = {
    spawnBagState = SpawnBag.getStateForSave(),
  }
  return JSON.encode(saveState)
end

function createActionButtons()
    self.createButton({
        label="Place",
        click_function="buttonClick_place",
        function_owner=self,
        position={1,0.1,2.1},
        rotation={0,0,0},
        height=350,
        width=800,
        font_size=250,
        color={0,0,0},
        font_color={1,1,1}
    })
    self.createButton({
        label="Recall",
        click_function="buttonClick_recall",
        function_owner=self,
        position={-1,0.1,2.1},
        rotation={0,0,0},
        height=350,
        width=800,
        font_size=250,
        color={0,0,0},
        font_color={1,1,1}
    })
end

function buttonClick_place()
  SpawnBag.spawn(UPGRADES_SPEC)
  SpawnBag.spawn(SERVITOR_SPEC)
end

-- Recalls objects to bag from table
function buttonClick_recall()
  SpawnBag.recall()
end
end)
__bundle_register("playercards/spawnbag/SpawnBag", function(require, _LOADED, __bundle_register, __bundle_modules)
require("playercards/PlayerCardSpawner")

-- Allows spawning of defined lists of cards which will be created from the template in the All
-- Player Cards bag.  SpawnBag.spawn will create objects based on a table definition, while
-- SpawnBag.recall will clean them all up.  Recall will be limited to a small area around the
-- spawned objects.  Objects moved out of this area will not be cleaned up.
--
-- SpawnSpec: Spawning requires a spawn specification with the following structure:
-- {
--   name: Name of this spawn content, used for internal tracking.  Multiple specs can be spawned,
--       but each requires a separate name
--   cards: A list of card IDs to be spawned
--   globalPos: Where the spawned objects should be placed, in global coordinates.  This should be
--       a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 }
--   rotation: Rotation for the spawned objects.  X=180 should be used for face down items.  As with
--       globalPos, this should be a valid Vector with x, y, and z defined
--   spread: Optional.  If present and true, cards will be spawned next to each other in a spread
--       moving to the right.  globalPos will define the location of the first card, each after that
--       will be moved a predefined distance
-- }
-- See BondedBag.ttslua for an example

SpawnBag = { }

-- To assist debugging, will draw a box around the recall zone when it's set up
local SHOW_RECALL_ZONE = false

local ALL_CARDS_GUID = "15bb07"

-- Distance to expand the recall zone around any added object.
local RECALL_BUFFER_X = 0.9
local RECALL_BUFFER_Z = 0.5

-- In order to mimic the behavior of the previous memory buttons we use a temporary bag when
-- recalling objects.  This bag is tiny and transparent, and will be placed at the same location as
-- this object.  Once all placed cards are recalled bag to this bag, it will be destroyed
local RECALL_BAG = {
  Name = "Bag",
  Transform = {
    scaleX = 0.01,
    scaleY = 0.01,
    scaleZ = 0.01,
  },
  ColorDiffuse = {
    r = 0,
    g = 0,
    b = 0,
    a = 0,
  },
  Locked = true,
  Grid = true,
  Snap = false,
  Tooltip = false,
}

-- Tracks what has been placed by this "bag" so they can be recalled
local placedSpecs = { }
local placedObjectGuids = { }
local recallZone = nil

-- Loads a table of saved state, extracted during the parent object's onLoad
SpawnBag.loadFromSave = function(saveTable)
  placedSpecs = saveTable.placed
  placedObjectGuids = saveTable.placedObjects
  recallZone = saveTable.recall
end

-- Generates a table of save state that can be included in the parent object's onSave
SpawnBag.getStateForSave = function()
  return {
    placed = placedSpecs,
    placedObjects = placedObjectGuids,
    recall = recallZone,
  }
end

-- Places the given spawnSpec on the table.  See SpawnBag.ttslua header for spawnSpec table data and
-- examples
SpawnBag.spawn = function(spawnSpec)
  -- Limit to one placement at a time
  if (placedSpecs[spawnSpec.name]) then
    return
  end
  if (spawnSpec == nil) then
    -- TODO: error here
    return
  end
  local cardsToSpawn = { }
  local allCardsBag = getObjectFromGUID(ALL_CARDS_GUID)
  for _, cardId in ipairs(spawnSpec.cards) do
    local cardData = allCardsBag.call("getCardById", { id = cardId })
    if (cardData ~= nil) then
      table.insert(cardsToSpawn, cardData)
    else
      -- TODO: error here
    end
  end
  if (spawnSpec.spread) then
    Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, recordPlacedObject)
  else
    Spawner.spawnCards(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, recordPlacedObject)
  end
  placedSpecs[spawnSpec.name] = true
end

-- Recalls all spawned objects to the bag, and clears the placedObjectGuids list
SpawnBag.recall = function()
  local trash = spawnObjectData({data = RECALL_BAG, position = self.getPosition()})
  for guid, _ in pairs(placedObjectGuids) do
    local obj = getObjectFromGUID(guid)
    if (obj ~= nil) then
      if (isInRecallZone(obj)) then
        trash.putObject(obj)
      end
      placedObjectGuids[guid] = nil
    end
  end

  trash.destruct()
  -- We've recalled everything we can, some cards may have been moved out of the
  -- card area.  Just reset at this point.
  placedSpecs = { }
  placedObjectGuids = { }
  recallZone = nil
end

-- Callback for when an object has been spawned.  Tracks the object for later recall and updates the
-- recall zone.
function recordPlacedObject(spawned)
  placedObjectGuids[spawned.getGUID()] = true
  expandRecallZone(spawned)
end

-- Expands the current recall zone based on the position of the given object.  The recall zone will
-- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer
function expandRecallZone(spawnedCard)
  local pos = spawnedCard.getPosition()
  if (recallZone == nil) then
    -- First card out of the bag, initialize surrounding that
    recallZone = { }
    recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z }
    recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z }
    return
  else
    if (pos.x > recallZone.upperLeft.x) then
      recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X
    end
    if (pos.x < recallZone.lowerRight.x) then
      recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X
    end
    if (pos.z > recallZone.upperLeft.z) then
      recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z
    end
    if (pos.z < recallZone.lowerRight.z) then
      recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z
    end
  end
  if (SHOW_RECALL_ZONE) then
    local y = 1.5
    local thick = 0.05
    Global.setVectorLines({
      {
          points    = { {recallZone.upperLeft.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.lowerRight.z} },
          color     = {1,0,0},
          thickness = thick,
          rotation  = {0,0,0},
      },
      {
          points    = { {recallZone.upperLeft.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.lowerRight.z} },
          color     = {1,0,0},
          thickness = thick,
          rotation  = {0,0,0},
      },
      {
          points    = { {recallZone.lowerRight.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.upperLeft.z} },
          color     = {1,0,0},
          thickness = thick,
          rotation  = {0,0,0},
      },
      {
          points    = { {recallZone.lowerRight.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.upperLeft.z} },
          color     = {1,0,0},
          thickness = thick,
          rotation  = {0,0,0},
      },
      })
  end
end

-- Checks to see if the given object is in the current recall zone.  If there isn't a recall zone,
-- will return true so that everything can be easily cleaned up.
function isInRecallZone(obj)
  if (recallZone == nil) then
    return true
  end
  local pos = obj.getPosition()
  return (pos.x < recallZone.upperLeft.x and pos.x > recallZone.lowerRight.x
      and pos.z < recallZone.upperLeft.z and pos.z > recallZone.lowerRight.z)
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

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, rot, sort, callback)
  if (sort) then
    table.sort(cardList, Spawner.cardComparator)
  end

  local position = { x = startPos.x, y = startPos.y, z = startPos.z }
  for _, card in ipairs(cardList) do
    Spawner.spawn({ card }, position, rot, callback)
    position.z = position.z + SPREAD_Z_SHIFT
  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)
return __bundle_require("__root")