-- Clean Up Helper
-- made by:         Chr1Z
-- description:     Cleans up the table for the next scenario in a campaign:
--                  - sets counters to default values (resources and doom) or trauma values (health and sanity, if not disabled) from campaign log
--                  - puts everything on playmats and hands into respective trashcans
--                  - use the IGNORE_TAG to exclude objects from tidying (default: "CleanUpHelper_Ignore")
information = {
  version = "2.4",
  last_updated = "24.11.2022"
}

-- enable this for debugging
local SHOW_RAYS = false

-- these objects will be ignored
local IGNORE_GUIDS = {
  -- big playmat, change image panel and investigator counter
  "b7b45b"; "f182ee"; "721ba2";
  -- bless/curse manager
  "afa06b"; "bd0253"; "5933fb";
  -- stuff on agenda/act playmat
  "85c4c6"; "4a3aa4"; "fea079"; "b015d8"; "11e0cf"; "9f334f"; "70b9f6"; "0a5a29";
  -- doom/location token bag
  "47ffc3"; "170f10";
  -- table
  "4ee1f2"
}

-- objects with this tag will be ignored
local IGNORE_TAG = "CleanUpHelper_ignore"

-- colors and order for following tables
local COLORS = { "White"; "Orange"; "Green"; "Red"; "Agenda" }

-- counter GUIDS (4x damage, 4x sanity and 4x resource)
local TOKEN_GUIDS = {
  "eb08d6"; "e64eec"; "1f5a0a"; "591a45";
  "468e88"; "0257d9"; "7b5729"; "beb964";
  "4406f0"; "816d84"; "cd15ac"; "a4b60d"
}

-- default values (4x damage, 4x horror, 4x resources)
local DEFAULT_VALUES = {
  0; 0; 0; 0;
  0; 0; 0; 0;
  5; 5; 5; 5
}

local PLAYERMAT_GUIDS = { "8b081b"; "bd0ff4"; "383d8b"; "0840d5" }
local TRACKER_GUIDS   = { "e598c2"; "b4a5f7"; "af7ed7"; "e74881" }
local CLUE_GUIDS      = { "d86b7c"; "1769ed"; "032300"; "37be78" }
local TRASHCAN_GUIDS  = { "147e80"; "f7b6c8"; "5f896a"; "4b8594"; "70b9f6" }

-- values for physics.cast (4 entries for player zones, 5th entry for agenda/act deck, 6th for campaign log)
local PHYSICS_POSITION = {
  { -54.5, 2, 21 };
  { -54.5, 2, -21 };
  { -25.0, 2, 26 };
  { -25.0, 2, -26 };
  { -02.0, 2, 10 };
  { -00.0, 2, -27 }
}

local PHYSICS_ROTATION = { 270, 270, 0, 180, 270, 0 }

local PHYSICS_SCALE = {
  { 36.6, 1, 14.5 };
  { 36.6, 1, 14.5 };
  { 28.0, 1, 14.5 };
  { 28.0, 1, 14.5 };
  { 55.0, 1, 13.5 };
  { 05.0, 1, 05.0 }
}

local tidyPlayermats = true
local importTrauma   = true
local resetResources = true

local buttonParameters          = {}
buttonParameters.function_owner = self

---------------------------------------------------------
-- option loading and GUI setup
---------------------------------------------------------

function onSave() return JSON.encode({ tidyPlayermats, importTrauma, resetResources }) end

function onLoad(saved_data)
  local loaded_data = JSON.decode(saved_data)
  if loaded_data ~= nil then
    tidyPlayermats = loaded_data[1]
    importTrauma   = loaded_data[2]
    resetResources = loaded_data[3]
  end

  -- context menu and buttons
  self.addContextMenuItem("More Information", function()
    printToAll("------------------------------", "White")
    printToAll("Clean Up Helper v" .. information["version"] .. " by Chr1Z", "Orange")
    printToAll("last updated: " .. information["last_updated"], "White")
    printToAll("ignore tag: " .. IGNORE_TAG, "White")
    printToAll("Player order in the campaign log for trauma import:\nWhite, Orange, Green, Red", "White")
  end)

  -- index 0: button as label
  buttonParameters.label          = "Clean Up Helper v" .. information["version"]
  buttonParameters.click_function = "none"
  buttonParameters.position       = { x = 0, y = 0.1, z = -1.525 }
  buttonParameters.height         = 0
  buttonParameters.width          = 0
  buttonParameters.font_size      = 165
  buttonParameters.font_color     = "Black"
  self.createButton(buttonParameters)

  -- index 1: option button for playermats
  buttonParameters.label          = "Tidy playermats: " .. (tidyPlayermats and "✓" or "✗")
  buttonParameters.color          = { 0, 0, 0, 0.95 }
  buttonParameters.click_function = "toggle_tidyPlayermats"
  buttonParameters.position.z     = buttonParameters.position.z + 0.7
  buttonParameters.height         = 275
  buttonParameters.width          = 1550
  buttonParameters.font_size      = 165
  buttonParameters.font_color     = "White"
  self.createButton(buttonParameters)

  -- index 2: option button for trauma import
  buttonParameters.label          = "Import trauma:   " .. (importTrauma and "✓" or "✗")
  buttonParameters.click_function = "toggle_importTrauma"
  buttonParameters.position.z     = buttonParameters.position.z + 0.7
  self.createButton(buttonParameters)

  -- index 3: option button for resources
  buttonParameters.label          = "Reset resources:  " .. (resetResources and "✓" or "✗")
  buttonParameters.click_function = "toggle_resetResources"
  buttonParameters.position.z     = buttonParameters.position.z + 0.7
  self.createButton(buttonParameters)

  -- index 4: start button
  buttonParameters.label          = "Start!"
  buttonParameters.click_function = "cleanUp"
  buttonParameters.position.z     = buttonParameters.position.z + 0.7
  buttonParameters.width          = 775
  self.createButton(buttonParameters)

  -- create single table for ignoring
  for _, v in ipairs(CLUE_GUIDS) do table.insert(IGNORE_GUIDS, v) end
  for _, v in ipairs(TRASHCAN_GUIDS) do table.insert(IGNORE_GUIDS, v) end
  for _, v in ipairs(PLAYERMAT_GUIDS) do table.insert(IGNORE_GUIDS, v) end
  for _, v in ipairs(TOKEN_GUIDS) do table.insert(IGNORE_GUIDS, v) end
end

---------------------------------------------------------
-- click functions for option buttons
---------------------------------------------------------

function toggle_tidyPlayermats()
  tidyPlayermats = not tidyPlayermats
  self.editButton({ index = 1, label = "Tidy playermats: " .. (tidyPlayermats and "✓" or "✗") })
end

function toggle_importTrauma()
  importTrauma = not importTrauma
  self.editButton({ index = 2, label = "Import trauma:   " .. (importTrauma and "✓" or "✗") })
end

function toggle_resetResources()
  resetResources = not resetResources
  self.editButton({ index = 3, label = "Reset resources:  " .. (resetResources and "✓" or "✗") })
end

---------------------------------------------------------
-- main function
---------------------------------------------------------

function cleanUp()
  printToAll("------------------------------", "White")
  printToAll("Clean up started!", "Orange")
  printToAll("Resetting counters...", "White")

  getTrauma()
  resetCounters()
  removeBlessCurse()

  printToAll("Discarding player hands...", "White")
  discardHands()

  printToAll("Tidying big playmat...", "White")
  startLuaCoroutine(self, "tidyPlaymatCoroutine")
end

---------------------------------------------------------
-- modular functions, called by other functions
---------------------------------------------------------

-- set counters to reset values
function resetCounters()
  for i, guid in ipairs(TOKEN_GUIDS) do
    -- skip this step for resource tokens when option disabled (token number 9-12)
    if resetResources or (i < 9 or i > 12) then
      local TOKEN = getObjectFromGUID(guid)
      if TOKEN ~= nil then
        TOKEN.call("updateVal", RESET_VALUES[i])
      else
        printToAll("Token number " .. i .. " could not be found and was skipped.", "Yellow")
      end
    end
  end

  -- reset doom on agenda
  local doomcounter = getObjectFromGUID("85c4c6")
  if doomcounter ~= nil then
    doomcounter.call("setToZero")
  end

  for i, guid in ipairs(TRACKER_GUIDS) do
    local obj = getObjectFromGUID(guid)
    if obj ~= nil then
      obj.call("updateStats", { 1, 1, 1, 1 })
    else
      printToAll("Stat tracker number " .. i .. " could not be found and was skipped.", "Yellow")
    end
  end
end

-- read values for trauma from campaign log if enabled
function getTrauma()
  -- load default values
  RESET_VALUES = {}
  for k, v in pairs(DEFAULT_VALUES) do
    RESET_VALUES[k] = v
  end

  -- stop here if trauma import is disabled
  if not importTrauma then
    printToAll("Default values for health and sanity loaded.", "Yellow")
    return
  end

  -- get campaign log
  local campaignLog = findObjects(6)[1]
  if campaignLog == nil then
    printToAll("Campaign log not found in standard position!", "Yellow")
    printToAll("Default values for health and sanity loaded.", "Yellow")
    return
  end

  -- get data from campaign log if possible
  local counterData = campaignLog.hit_object.getVar("ref_buttonData")
  if counterData ~= nil then
    printToAll("Trauma values found in campaign log!", "Green")
    for i = 1, 10, 3 do
      RESET_VALUES[1 + (i - 1) / 3] = counterData.counter[i].value
      RESET_VALUES[5 + (i - 1) / 3] = counterData.counter[i + 1].value
    end
  else
    printToAll("Trauma values could not be found in campaign log!", "Yellow")
    printToAll("Default values for health and sanity loaded.", "Yellow")
  end
end

-- get rid of bless/curse tokens via bless / curse manager
function removeBlessCurse()
  local BlessCurseManager = getObjectFromGUID("5933fb")
  if BlessCurseManager ~= nil then
    BlessCurseManager.call("doRemove", "White")
  end
end

-- discard all hand objects
function discardHands()
  for i = 1, 4 do
    local trashcan = getObjectFromGUID(TRASHCAN_GUIDS[i])
    local hand = Player[COLORS[i]].getHandObjects()
    if trashcan == nil then return end
    for j = #hand, 1, -1 do trashcan.putObject(hand[j]) end
  end
end

-- clean up for big playmat
function tidyPlaymatCoroutine()
  local trashcan = getObjectFromGUID(TRASHCAN_GUIDS[5])
  local PLAYMATZONE = getObjectFromGUID("a2f932")

  if PLAYMATZONE == nil then
    printToAll("Scripting zone for big playmat could not be found!", "Red")
  elseif trashcan == nil then
    printToAll("Trashcan for big playmat could not be found!", "Red")
  else
    for _, obj in ipairs(PLAYMATZONE.getObjects()) do
      -- ignore these elements
      if indexOf(IGNORE_GUIDS, obj.getGUID()) == nil and obj.hasTag(IGNORE_TAG) == false then
        coroutine.yield(0)
        trashcan.putObject(obj)
      end
    end
  end

  local PLAYMAT = getObjectFromGUID('721ba2')
  if PLAYMAT then
    PLAYMAT.setTable("SPAWNED_LOCATION_GUIDS", {})
  end

  printToAll("Tidying playermats and agenda mat...", "White")
  startLuaCoroutine(self, "tidyPlayerMatCoroutine")
  return 1
end

-- clean up for the four playermats and the agenda/act playmat
function tidyPlayerMatCoroutine()
  for i = 1, 5 do
    -- skip playermat (1-4) if option disabled
    if tidyPlayermats or i == 5 then
      -- delay for animation purpose
      for k = 1, 30 do coroutine.yield(0) end

      -- get respective trashcan
      local trashcan = getObjectFromGUID(TRASHCAN_GUIDS[i])
      if trashcan == nil then
        printToAll("Trashcan for " .. COLORS[i] .. " playmat could not be found!", "Red")
        return
      end

      for _, entry in ipairs(findObjects(i)) do
        local obj = entry.hit_object
        local desc_low = string.lower(obj.getDescription())

        -- ignore these elements
        if indexOf(IGNORE_GUIDS, obj.getGUID()) == nil and obj.hasTag(IGNORE_TAG) == false and
            desc_low ~= "chaos bag" and desc_low ~= "action token" then
          coroutine.yield(0)
          trashcan.putObject(obj)

          -- flip action tokens back to ready
        elseif desc_low == "action token" and obj.is_face_down then
          obj.flip()
        end
      end

      -- reset "activeInvestigatorId"
      if i < 5 then
        local playermat = getObjectFromGUID(PLAYERMAT_GUIDS[i])
        if playermat then
          playermat.setVar("activeInvestigatorId", "00000")
        end
      end
    end
  end

  local datahelper = getObjectFromGUID("708279")
  if datahelper then
    datahelper.setTable("SPAWNED_PLAYER_CARD_GUIDS", {})
  end

  printToAll("Clean up completed!", "Green")
  return 1
end

---------------------------------------------------------
-- helper functions
---------------------------------------------------------

-- find objects depending on index (1 to 4 for playermats, 5 for agenda/act playmat, 6 for campaign log)
function findObjects(num)
  return Physics.cast({
    direction    = { 0, 1, 0 },
    max_distance = 1,
    type         = 3,
    size         = PHYSICS_SCALE[num],
    origin       = PHYSICS_POSITION[num],
    orientation  = { 0, PHYSICS_ROTATION[num], 0 },
    debug        = SHOW_RAYS
  })
end

-- helper to search array
function indexOf(array, value)
  for i, v in ipairs(array) do
    if v == value then
      return i
    end
  end
end