-- 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)
---------------------------------------------------------
-- specific setup (different for each playmat)
---------------------------------------------------------

PLAYER_COLOR          = "Orange"
PLAY_ZONE_POSITION    = { x = -54.5, y = 4, z = -21 }
PLAY_ZONE_SCALE       = { x = 36, y = 5.1, z = 14.5 }
DRAW_DECK_POSITION    = { x = -54.86, y = 2.5, z = -27.82 }
DISCARD_PILE_POSITION = { x = -58.96, y = 4, z = -27.82 }

TRASHCAN_GUID         = "f7b6c8"
STAT_TRACKER_GUID     = "b4a5f7"
RESOURCE_COUNTER_GUID = "816d84"

require("playermat/Playmat")
end)
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
-- set true to enable debug logging and show Physics.cast()
local DEBUG = false

-- we use this to turn off collision handling until onLoad() is complete
local COLLISION_ENABLED = false

-- position offsets relative to mat [x, y, z]
local DRAWN_ENCOUNTER_CARD_OFFSET = {1.365, 0.5, -0.635}
local DRAWN_CHAOS_TOKEN_OFFSET = {-1.55, 0.5, -0.58}
local DISCARD_BUTTON_OFFSETS = {
  {-1.365, 0.1, -0.945},
  {-0.91, 0.1, -0.945},
  {-0.455, 0.1, -0.945},
  {0, 0.1, -0.945},
  {0.455, 0.1, -0.945},
  {0.91, 0.1, -0.945}
}

local PLAY_ZONE_ROTATION = self.getRotation()

local TRASHCAN
local STAT_TRACKER
local RESOURCE_COUNTER

-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"

function onSave() return JSON.encode({zoneID = zoneID, playerColor = PLAYER_COLOR, activeInvestigatorId = activeInvestigatorId}) end

function onLoad(save_state)
  self.interactable = DEBUG
  DATA_HELPER = getObjectFromGUID('708279')
  PLAYER_CARDS = DATA_HELPER.getTable('PLAYER_CARD_DATA')
  PLAYER_CARD_TOKEN_OFFSETS = DATA_HELPER.getTable('PLAYER_CARD_TOKEN_OFFSETS')

  TRASHCAN = getObjectFromGUID(TRASHCAN_GUID)
  STAT_TRACKER = getObjectFromGUID(STAT_TRACKER_GUID)
  RESOURCE_COUNTER = getObjectFromGUID(RESOURCE_COUNTER_GUID)

  for i = 1, 6 do
    makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], {-3.85, 3, 10.38}, i)
  end

  self.createButton({
    click_function = "drawEncountercard",
    function_owner = self,
    position = {-1.84, 0, -0.65},
    rotation = {0, 80, 0},
    width = 265,
    height = 190
  })

  self.createButton({
    click_function = "drawChaostokenButton",
    function_owner = self,
    position = {1.85, 0, -0.74},
    rotation = {0, -45, 0},
    width = 135,
    height = 135
  })

  self.createButton({
    label = "Upkeep",
    click_function = "doUpkeep",
    function_owner = self,
    position = {1.84, 0.1, -0.44},
    scale = {0.12, 0.12, 0.12},
    width = 800,
    height = 280,
    font_size = 180
  })

  local state = JSON.decode(save_state)
  if state ~= nil then
    zoneID = state.zoneID
    PLAYER_COLOR = state.playerColor
    activeInvestigatorId = state.activeInvestigatorId
  end

  if getObjectFromGUID(zoneID) == nil then spawnDeckZone() end
  COLLISION_ENABLED = true
end

---------------------------------------------------------
-- utility functions
---------------------------------------------------------

function log(message)
  if DEBUG then print(message) end
end

-- send messages to player who clicked button if no seated player found
function setMessageColor(color)
  messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color
end

function spawnDeckZone()
  spawnObject({
    position = self.positionToWorld({-1.4, 0, 0.3 }),
    scale = {3, 5, 8 },
    type = 'ScriptingTrigger',
    callback = function (zone) zoneID = zone.getGUID() end,
    callback_owner = self,
    rotation = self.getRotation()
  })
end

function searchArea(origin, size)
  return Physics.cast({
    origin       = origin,
    direction    = {0, 1, 0},
    orientation  = PLAY_ZONE_ROTATION,
    type         = 3,
    size         = size,
    max_distance = 1,
    debug        = DEBUG
  })
end

function doNotReady(card) return card.getVar("do_not_ready") or false end

---------------------------------------------------------
-- Discard buttons
---------------------------------------------------------

-- builds a function that discards things in searchPosition to discardPosition
-- stuff on the card/deck will be put into the local trashcan
function makeDiscardHandlerFor(searchPosition, discardPosition)
  return function ()
    for _, hitObj in ipairs(findObjectsAtPosition(searchPosition)) do
      local obj = hitObj.hit_object
      if obj.tag == "Deck" or obj.tag == "Card" then
        if obj.hasTag("PlayerCard") then
          obj.setPositionSmooth(DISCARD_PILE_POSITION, false, true)
          obj.setRotation(PLAY_ZONE_ROTATION)
        else
          obj.setPositionSmooth(discardPosition, false, true)
          obj.setRotation({0, -90, 0})
        end
      -- don't touch the table or this playmat itself
      elseif obj.guid ~= "4ee1f2" and obj ~= self then
        TRASHCAN.putObject(obj)
      end
    end
  end
end

-- build a discard button to discard from searchPosition to discardPosition (number must be unique)
function makeDiscardButton(position, discardPosition, number)
  local searchPosition = {-position[1], position[2], position[3] + 0.32}
  local handler = makeDiscardHandlerFor(searchPosition, discardPosition)
  local handlerName = 'handler' .. number
  self.setVar(handlerName, handler)
  self.createButton({
    label = "Discard",
    click_function = handlerName,
    function_owner = self,
    position = {position[1], position[2], position[3] + 0.6},
    scale = {0.12, 0.12, 0.12},
    width = 800,
    height = 280,
    font_size = 180
  })
end

function findObjectsAtPosition(localPos)
  return Physics.cast({
    origin       = self.positionToWorld(localPos),
    direction    = {0, 1, 0},
    orientation  = {0, PLAY_ZONE_ROTATION.y + 90, 0},
    type         = 3,
    size         = {3.2, 1, 2},
    max_distance = 0,
    debug        = DEBUG
  })
end

---------------------------------------------------------
-- Upkeep button
---------------------------------------------------------

function doUpkeep(_, color, alt_click)
  setMessageColor(color)

  -- right-click binds to new player color
  if alt_click then
    PLAYER_COLOR = color
    printToColor("Upkeep button bound to " .. color, color)
    return
  end

  local forcedLearning = false

  -- unexhaust cards in play zone, flip action tokens and find forcedLearning
  for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
    local obj = v.hit_object
    if obj.getDescription() == "Action Token" and obj.is_face_down then
      obj.flip()
    elseif obj.tag == "Card" and not obj.is_face_down then
      local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
      if not doNotReady(obj) and cardMetadata.type ~= "Investigator" then
        obj.setRotation(PLAY_ZONE_ROTATION)
      end
      if cardMetadata.id == "08031" then
        forcedLearning = true
      end
      if cardMetadata.uses ~= nil then
        -- check for cards with 'replenish' in their metadata
        local count
        local replenish
        -- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
        -- a single entry or an array.  TODO: Clean this up when 2.4.0 has been out long
        -- enough that saved decks don't have old data
        if cardMetadata.uses.count ~= nil then
          count = cardMetadata.uses.count
          replenish = cardMetadata.uses.replenish
        else
          count = cardMetadata.uses[1].count
          replenish = cardMetadata.uses[1].replenish
        end
        if count and replenish then
          replenishTokens(obj, count, replenish)
        end
      end
    end
  end

  -- flip investigator mini-card if found
  -- flip summoned servitor mini-cards (To-Do: don't flip all of them)
  if activeInvestigatorId ~= nil then
    local miniId = string.match(activeInvestigatorId, "%d%d%d%d%d") .. "-m"
    for _, obj in ipairs(getObjects()) do
      if obj.tag == "Card" and obj.is_face_down then
        local notes = JSON.decode(obj.getGMNotes())
        if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then
          obj.flip()
        end
      end
    end
  end

  -- gain a resource
  RESOURCE_COUNTER.call("addOrSubtract")

  -- gain an additional resource for Jenny Barnes
  if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
    RESOURCE_COUNTER.call("addOrSubtract")
    printToColor("Gaining 2 resources (Jenny)", messageColor)
  end

  -- draw a card (with handling for Patrice and Forced Learning)
  if activeInvestigatorId == "06005" then
    if forcedLearning then
      printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor)
    else
      local handSize = #Player[PLAYER_COLOR].getHandObjects()
      if handSize < 5 then
        local cardsToDraw = 5 - handSize
        printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
        drawCardsWithReshuffle(cardsToDraw)
      end
    end
  elseif forcedLearning then
    printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
    drawCardsWithReshuffle(2)
  else
    drawCardsWithReshuffle(1)
  end
end

-- legacy function for "add draw 1 button"
function doDrawOne(_, color)
  setMessageColor(color)
  drawCardsWithReshuffle(1)
end

-- draw X cards (shuffle discards if necessary)
function drawCardsWithReshuffle(numCards)
  getDrawDiscardDecks()

  -- Norman Withers handling
  if string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
    local harbinger = false
    if topCard ~= nil and topCard.getName() == "The Harbinger" then harbinger = true
    elseif drawDeck ~= nil and not drawDeck.is_face_down then
      local cards = drawDeck.getObjects()
      if cards[#cards].name == "The Harbinger" then harbinger = true end
    end

    if harbinger then
      printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
      return
    end

    if topCard ~= nil then
      topCard.deal(numCards, PLAYER_COLOR)
      numCards = numCards - 1
      if numCards == 0 then return end
    end
  end

  local deckSize = 1
  if drawDeck == nil then
    deckSize = 0
  elseif drawDeck.tag == "Deck" then
    deckSize = #drawDeck.getObjects()
  end

  if deckSize >= numCards then
    drawCards(numCards)
    return
  end

  drawCards(deckSize)
  if discardPile ~= nil then
    shuffleDiscardIntoDeck()
    Wait.time(|| drawCards(numCards - deckSize), 1)
  end
  printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
end

-- get the draw deck and discard pile objects
function getDrawDiscardDecks()
  drawDeck = nil
  discardPile = nil
  topCard = nil

  local zone = getObjectFromGUID(zoneID)
  if zone == nil then return end

  for _, object in ipairs(zone.getObjects()) do
    if object.tag == "Deck" or object.tag == "Card" then
      if self.positionToLocal(object.getPosition()).z > 0.5 then
        discardPile = object
      -- Norman Withers handling
      elseif string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" and object.tag == "Card" and not object.is_face_down then
        topCard = object
      else
        drawDeck = object
      end
    end
  end
end

function drawCards(numCards)
  if drawDeck == nil then return end
  drawDeck.deal(numCards, PLAYER_COLOR)
end

function shuffleDiscardIntoDeck()
  if not discardPile.is_face_down then discardPile.flip() end
  discardPile.shuffle()
  discardPile.setPositionSmooth(DRAW_DECK_POSITION, false, false)
  drawDeck = discardPile
  discardPile = nil
end

function spawnTokenOn(object, offsets, tokenType)
  local tokenPosition = object.positionToWorld(offsets)
  spawnToken(tokenPosition, tokenType)
end

-- Spawn a group of tokens of the given type on the object
-- @param object Object to spawn the tokens on
-- @param tokenType Type of token to be spawned
-- @param tokenCount Number of tokens to spawn
-- @param shiftDown Amount to shift this group down to avoid spawning multiple token groups on
--     top of each other.  Negative values are allowed, and will move the group up instead.  This is
--     a static value and is unaware of how many tokens were spawned previously; callers should
--     ensure the proper shift.
function spawnTokenGroup(object, tokenType, tokenCount, shiftDown)
  if (tokenCount < 1 or tokenCount > 12) then return end
  local offsets = PLAYER_CARD_TOKEN_OFFSETS[tokenCount]
  if shiftDown ~= nil then
    -- Copy the offsets to make sure we don't change the static values
    local baseOffsets = offsets
    offsets = { }
    for i, baseOffset in ipairs(baseOffsets) do
      offsets[i] = baseOffset
      offsets[i][3] = offsets[i][3] + shiftDown
    end
  end
  if offsets == nil then error("couldn't find offsets for " .. tokenCount .. ' tokens') end

  for i = 1, tokenCount do
    spawnTokenOn(object, offsets[i], tokenType)
  end
end

---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------

-- replenish Tokens for specific cards (like 'Physical Training (4)')
function replenishTokens(card, count, replenish)
  local cardPos = card.getPosition()

  -- don't continue for cards on your deck (Norman) or in your discard pile
  if self.positionToLocal(cardPos).x < -1 then return end

  -- get current amount of resource tokens on the card
  local search = searchArea(cardPos, { 2.5, 0.5, 3.5 })
  local foundTokens = 0
  for _, obj in ipairs(search) do
    local obj = obj.hit_object
    if obj.getCustomObject().image == "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/" then
      foundTokens = foundTokens + math.abs(obj.getQuantity())
      obj.destruct()
    end
  end

  -- handling Runic Axe upgrade sheet for additional replenish
  if card.getName() == "Runic Axe" then
    for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
      local obj = v.hit_object
      if obj.tag == "Card" then
        local notes = JSON.decode(obj.getGMNotes()) or {}
        if notes ~= nil and notes.id == "09022-c" then
          if obj.getVar("markedBoxes")[7] == 3 then replenish = 2 end
          break
        end
      end
    end
  end

  local newCount = foundTokens + replenish
  if newCount > count then newCount = count end
  spawnTokenGroup(card, "resource", newCount)
end

function getPlayerCardData(object)
  return PLAYER_CARDS[object.getName()..':'..object.getDescription()] or PLAYER_CARDS[object.getName()]
end

function shouldSpawnTokens(object)
  -- don't spawn tokens if in doubt, this should only ever happen onLoad and prevents respawns
  local spawned = DATA_HELPER.call('getSpawnedPlayerCardGuid', {object.getGUID()})
  local hasDataHelperData = getPlayerCardData(object)
  local cardMetadata = JSON.decode(object.getGMNotes()) or {}
  local hasUses = cardMetadata.uses ~= nil
  return not spawned and (hasDataHelperData or hasUses)
end

function markSpawned(object)
  local saved = DATA_HELPER.call('setSpawnedPlayerCardGuid', {object.getGUID(), true})
  if not saved then error('attempt to mark player card spawned before data loaded') end
end

-- contains position and amount of boxes for the upgradesheets that change uses
-- Alchemical Distillation, Damning Testimony, Living Ink and Hyperphysical Shotcaster
local customizationsTable = {
  ["09040"] = {5, 2},
  ["09059"] = {2, 2},
  ["09079"] = {3, 2},
  ["09119"] = {6, 4}
}

function spawnTokensFor(object)
  local cardMetadata = JSON.decode(object.getGMNotes()) or {}
  local type = nil
  local token = nil
  local tokenCount = 0
  if cardMetadata.uses ~= nil then
    -- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
    -- a single entry or an array.  This is ugly and duplicated, but impossible to replicate the
    -- multi-spawn vs. single spawn otherwise.  TODO: Clean this up when 2.4.0 has been out long
    -- enough that saved decks don't have old data
    if cardMetadata.uses.count != nil then
      type = cardMetadata.uses.type
      token = cardMetadata.uses.token
      tokenCount = cardMetadata.uses.count
      if activeInvestigatorId == "03004" and type == "Charge" then tokenCount = tokenCount + 1 end

      log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
      spawnTokenGroup(object, token, tokenCount)
    else
      for i, useInfo in ipairs(cardMetadata.uses) do
        type = useInfo.type
        token = useInfo.token
        tokenCount = useInfo.count

        -- additional uses for certain customizable cards (by checking the upgradesheets)
        if customizationsTable[cardMetadata.id] ~= nil then
          for _, obj in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
            local obj = obj.hit_object
            if obj.tag == "Card" then
              local notes = JSON.decode(obj.getGMNotes()) or {}
              if notes.id == (cardMetadata.id .. "-c") then
                local pos = customizationsTable[cardMetadata.id][1]
                local boxes = customizationsTable[cardMetadata.id][2]
                if obj.getVar("markedBoxes")[pos] == boxes then tokenCount = tokenCount + 2 end
                break
              end
            end
          end
        end

        -- additional charge for Akachi
        if activeInvestigatorId == "03004" and type == "Charge" then tokenCount = tokenCount + 1 end

        log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
        -- Shift each spawned group after the first down so they don't pile on each other
        spawnTokenGroup(object, token, tokenCount, (i - 1) * 0.6)
      end
    end
  else
    local data = getPlayerCardData(object)
    token = data['tokenType']
    tokenCount = data['tokenCount']
    log("Spawning tokens for "..object.getName()..'['..object.getDescription()..']: '..tokenCount.."x "..token)
    spawnTokenGroup(object, token, tokenCount)
  end
  markSpawned(object)
end

function resetSpawnState()
  local zone = getObjectFromGUID(zoneID)
  if zone == nil then return end

  for _, object in ipairs(zone.getObjects()) do
    if object.tag == "Card" then
      unmarkSpawned(object.getGUID(), true)
    elseif object.tag == "Deck" then
      local cards = object.getObjects()
      for _, v in ipairs(cards) do
        unmarkSpawned(v.guid)
      end
    end
  end
end

function unmarkSpawned(guid, force)
  if not force and getObjectFromGUID(guid) ~= nil then return end
  DATA_HELPER.call('setSpawnedPlayerCardGuid', {guid, false})
end

function onCollisionEnter(collision_info)
  if not COLLISION_ENABLED then return end
  Wait.time(resetSpawnState, 1)
  local object = collision_info.collision_object

  -- only continue for cards
  if object.name ~= "Card" and object.name ~= "CardCustom" then return end
  maybeUpdateActiveInvestigator(object)

  -- don't spawn tokens for cards in discard pile / threat area
  local localpos = self.positionToLocal(object.getPosition())
  if localpos.x < -0.7 or localpos.z < -0.3 then
    log('Not spawning tokens, relative coordinates are x: ' .. localpos.x .. ' z: ' .. localpos.z)
  elseif not object.is_face_down and shouldSpawnTokens(object) then
    spawnTokensFor(object)
  end
end

---------------------------------------------------------
-- investigator ID grabbing and stat tracker
---------------------------------------------------------

function maybeUpdateActiveInvestigator(card)
  local notes = JSON.decode(card.getGMNotes())
  if notes ~= nil and notes.type == "Investigator" and notes.id ~= activeInvestigatorId then
    activeInvestigatorId = notes.id
    STAT_TRACKER.call("updateStats", {notes.willpowerIcons, notes.intellectIcons, notes.combatIcons, notes.agilityIcons})

    -- change state of action tokens
    local search = searchArea(self.positionToWorld({-1.25, 0.05, -1.11}), {4, 1, 1})
    local small_token = nil
    local state_table = {
      ["Guardian"] = 1,
      ["Seeker"]   = 2,
      ["Rogue"]    = 3,
      ["Mystic"]   = 4,
      ["Survivor"] = 5,
      ["Neutral"]  = 6
    }

    for _, obj in ipairs(search) do
      local obj = obj.hit_object
      if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then
        if obj.getScale().x < 0.4 then
          small_token = obj
        else
          setObjectState(obj, state_table[notes.class])
        end
      end
    end

    -- update the small token with special action for certain investigators
    -- Ursula Downs: Investigate action
    if activeInvestigatorId == "04002" then
      setObjectState(small_token, 8)
    -- Daisy Walker (only for normal front, not parallel): Tome action
    elseif activeInvestigatorId == "01002" or activeInvestigatorId == "01502" or activeInvestigatorId == "01002-pb" then
      setObjectState(small_token, 9)
    -- Tony Morgan: Engage/Fight action
    elseif activeInvestigatorId == "06003" then
      setObjectState(small_token, 10)
    -- Finn Edwards: Evade action
    elseif activeInvestigatorId == "04003" then
      setObjectState(small_token, 11)
    -- Bob Jenkins: Play Item action
    elseif activeInvestigatorId == "08016" then
      setObjectState(small_token, 14)
    else
      setObjectState(small_token, state_table[notes.class])
    end
  end
end

function setObjectState(obj, stateId)
  if obj.getStateId() ~= stateId then obj.setState(stateId) end
end

---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------

function drawChaostokenButton(_, _, isRightClick)
  Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick})
end

function drawEncountercard(object, player, isRightClick)
  Global.call("drawEncountercard", PLAYER_COLOR)
end

function spawnToken(position, tokenType)
  Global.call('spawnToken', {position, tokenType, PLAY_ZONE_ROTATION})
end

-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
function updatePlayerCards(args)
  local custom_data_helper = getObjectFromGUID(args[1])
  data_player_cards = custom_data_helper.getTable("PLAYER_CARD_DATA")
  for k, v in pairs(data_player_cards) do
    PLAYER_CARDS[k] = v
  end
end
end)
return __bundle_require("__root")
