local bta = require('btafunctions')
local datasaver = require('lib_data_saver')
local md = require('debugFunctions')

local config = (function()
  local function parseTabledItem(source, label)
    return function()
      local name = parse.getString()
      local result = source[name]
      if not (result and result:isValid()) then
        parse.displayMessage('Unknown ' .. label .. '"' .. name .. '"', true)
      end
      return result
    end
  end

  local parseShip = parseTabledItem(tb.ShipClasses, 'ship class')
  local parseWeapon = parseTabledItem(tb.WeaponClasses, 'weapon class')
  local parseIntel = parseTabledItem(tb.IntelEntries, 'intel entry')

  local function parseCutscene()
    local result = {}
    local name = parse.getString()
    local scene = ui.TechRoom.Cutscenes[name]
    if not (scene and scene:isValid()) then
      parse.displayMessage('Unknown cutscene entry "' .. name .. '"', true)
    end
    result.scene = scene
    if parse.optionalString('++BtA Choice:') then
      result.key = parse.getString()
      parse.requiredString('++BtA Value:')
      result.value = parse.getString()
      result.default = false
      if parse.optionalString('++Default:') then
        result.default = parse.getBoolean()
      end
    end
    return result
  end

  local function parseInventory(callback)
    local count = parse.getInt()
    local class = callback()
    return { class = class, count = count }
  end

  local function parseRequisition(callback)
    local result = parseInventory(callback)
    parse.requiredString('++Faction:')
    result.faction = parse.getString()
    parse.requiredString('++Cost:')
    result.cost = parse.getInt()
    return result
  end

  local function parseSideStory()
    local result = {}
    result.title = parse.getString()
    parse.requiredString('++XSTR:')
    result.xstr = parse.getInt()
    parse.requiredString('++Filename:')
    result.filename = parse.getString()
    result.initial = parse.optionalString('++Initial:') and parse.getBoolean()
    return result
  end

  local function parseTrigger()
    local result = parse.getString()
    if bta:getGameProgress(result) <= 0 then
      parse.displayMessage('Unknown mission or campaign "' .. result .. '"', true)
    end
    return result
  end

  local tokens = {}
  local function parseToken(default)
    local token = parse.optionalString('+Token:') and parse.getString() or default
    if tokens[token] then
      parse.displayMessage('Multiple triggers share the token "' .. token .. '"', true)
    else
      tokens[token] = true
    end
    return token
  end

  local function parseEntry()
    local entry = {
      intel = {},
      staleIntel = {},
      equipment = {},
      headlines = {},
      editorials = {},
      inventory = {},
      requisitions = {},
      cutscenes = {},
      sideStories = {},
      updates = {}
    }
    local trigger = parseTrigger()
    entry.trigger = trigger
    entry.token = parseToken(trigger)
    while true do
      if parse.optionalString('+Show Ship:') then
        table.insert(entry.intel, parseShip())
      elseif parse.optionalString('+Show Weapon:') then
        table.insert(entry.intel, parseWeapon())
      elseif parse.optionalString('+Show Intel:') then
        table.insert(entry.intel, parseIntel())
      elseif parse.optionalString('+Hide Ship:') then
        table.insert(entry.staleIntel, parseShip())
      elseif parse.optionalString('+Hide Weapon:') then
        table.insert(entry.staleIntel, parseWeapon())
      elseif parse.optionalString('+Hide Intel:') then
        table.insert(entry.staleIntel, parseIntel())
      elseif parse.optionalString('+Show Cutscene:') then
        table.insert(entry.cutscenes, parseCutscene())
      elseif parse.optionalString('+Allow Ship:') then
        table.insert(entry.equipment, parseShip())
      elseif parse.optionalString('+Allow Weapon:') then
        table.insert(entry.equipment, parseWeapon())
      elseif parse.optionalString('+Show Headline:') then
        table.insert(entry.headlines, parse.getString())
      elseif parse.optionalString('+Show Editorial:') then
        table.insert(entry.editorials, parse.getString())
      elseif parse.optionalString('+Add Ship Inventory:') then
        table.insert(entry.inventory, parseInventory(parseShip))
      elseif parse.optionalString('+Add Weapon Inventory:') then
        table.insert(entry.inventory, parseInventory(parseWeapon))
      elseif parse.optionalString('+Add Ship Requisition:') then
        table.insert(entry.requisitions, parseRequisition(parseShip))
      elseif parse.optionalString('+Add Weapon Requisition:') then
        table.insert(entry.requisitions, parseRequisition(parseWeapon))
      elseif parse.optionalString('+Show Story:') then
        table.insert(entry.sideStories, parseSideStory())
      elseif parse.optionalString('+Send Update:') then
        table.insert(entry.updates, parse.getString())
      else
        return entry
      end
    end
  end

  local result = {}
  parse.readFileText('progress.tbl', 'data/tables')
  parse.requiredString('#Progress')
  while parse.optionalString('$Trigger:') do
    table.insert(result, parseEntry())
  end
  parse.requiredString('#End')
  parse.stop()
  return result
end)()

local loadState, saveState = (function()
  local function getSource()
    return 'progress' .. ba.getCurrentPlayer():getCampaignFilename()
  end

  local function loadState()
    return datasaver:loadDataFromFile(getSource(), true) or {}
  end

  local function saveState(state)
    datasaver:saveDataToFile(getSource(), state or {}, true)
  end

  return loadState, saveState
end)()

local run, getUpdated, clearUpdated, recordUpdate = (function()
  local sideStories = require('sidestories')

  local updated = {}

  local function getUpdated()
    local result = {}
    for _, v in pairs(updated) do
      table.insert(result, v)
    end
    return result
  end

  local function clearUpdated()
    updated = {}
  end

  local function recordUpdate(message, cancellation)
    for _, v in pairs(updated) do
      if v == message then return end
    end
    updated[cancellation or {}] = message
  end

  local function processIntel(class, toggle)
    local previous = class.InTechDatabase
    class.InTechDatabase = toggle
    md.print('PROGRESS: ' .. class.Name .. '.InTechDatabase is now ' .. tostring(toggle))
    if toggle and not previous then
      recordUpdate(ba.XSTR('New data is available in the tech room!', 11350), class.Name)
    end
  end

  local function processStaleIntel(class, toggle)
    class.InTechDatabase = not toggle
    md.print('PROGRESS: ' .. class.Name .. '.InTechDatabase is now ' .. tostring(not toggle))
    updated[class.Name] = nil
  end

  local function processCutscene(class, toggle)
    local val = toggle
    if class.key then
      local datasaver = require("lib_data_saver")
      local config = datasaver:loadDataFromFile("playerChoices", true)

      if config == nil then
        config = {}
      end
      if config[class.key] ~= nil then
        if config[class.key] == class.value then
          val = true
        else
          val = false
        end
      else
        val = class.default
      end
    end
    class.scene.isVisible = val
    md.print('PROGRESS: ' .. class.scene.Name .. '.isVisible is now ' .. tostring(val))
  end

  local function processEquipment(class, toggle)
    class.AllowedInCampaign = toggle
    md.print('PROGRESS: ' .. class.Name .. '.AllowedInCampaign is now ' .. tostring(toggle))
  end

  local function processJournal(section)
    return function(article, toggle)
      if ScpuiSystem.extensions.JournalUi:doesConfigExist() then
        if toggle then
          if ScpuiSystem.extensions.JournalUi:unlockEntry(section, article) then
            recordUpdate(ba.XSTR('New articles are available in the news room!', 11351))
          end
        else
          ScpuiSystem.extensions.JournalUi:lockEntry(section, article)
        end
        md.print('PROGRESS: ' .. article .. ' is now ' .. (toggle and 'unlocked' or 'locked'))
      end
    end
  end

  local processHeadline = processJournal('Headlines')
  local processEditorial = processJournal('Editorials')

  local function processInventory(inventory)
    local count = inventory.count
    local class = inventory.class
    md.print('PROGRESS: Granting ' .. count .. 'x ' .. class.Name)
    local current = Backstock:GetAmount(class)
    local total = current + count
    Backstock:SetAmount(class, total)
    Backstock.updated = true -- to make sure loadouts know that Inventory has changed
  end

  local function processRequisition(requisition)
    local count = requisition.count
    local class = requisition.class
    local cost = requisition.cost
    local faction = requisition.faction
    md.print('PROGRESS: Adding requisition for ' .. count .. 'x ' .. class.Name .. ' at ' .. cost .. ' CP each')
    if tb.ShipClasses[class.Name]:isValid() then
      if count >= 0 then
        Backstock:AddShipRequisition(class, count, faction, cost)
      else
        Backstock:RemoveShipRequisition(class, count, faction)
      end
    else
      if count >= 0 then
        Backstock:AddWeaponRequisition(class, count, faction, cost)
      else
        Backstock:RemoveWeaponRequisition(class, count, faction)
      end
    end
    if count > 0 then
      recordUpdate(ba.XSTR('New assets are available for requisition!', 11352))
    end
  end

  local function processSideStory(sideStory, campaignMatch)
    local initial = sideStory.initial
    if campaignMatch or initial then
      sideStories:unlock(sideStory.title, sideStory.xstr, sideStory.filename, initial)
    end
  end

  local function forEach(list, callback, toggle)
    for _, item in ipairs(list) do
      callback(item, toggle)
    end
    return #list > 0
  end

  local function isInCurrentCampaign(trigger)
    local campaign = ba.getCurrentPlayer():getCampaignFilename()
    local pattern = '^' .. campaign:gsub('%..+$', '')
    return trigger:find(pattern)
  end

  local function processEntry(entry, progress, state)
    -- Since the configuration might change in development, this function tries
    -- to process as much as it can each time, so long as doing so won't lead to
    -- inaccurate results during normal play.
    local trigger = entry.trigger
    local token = entry.token
    local toggle = progress >= bta:getGameProgress(entry.trigger)
    -- These can always be run safely (they're "idempotent").
    forEach(entry.intel, processIntel, toggle)
    forEach(entry.staleIntel, processStaleIntel, toggle)
    forEach(entry.cutscenes, processCutscene, toggle)
    forEach(entry.equipment, processEquipment, toggle)
    forEach(entry.headlines, processHeadline, toggle)
    forEach(entry.editorials, processEditorial, toggle)
    if toggle then
      local campaignMatch = isInCurrentCampaign(trigger)
      forEach(entry.sideStories, processSideStory, campaignMatch)
    end
    -- The remaining items touch campaign assets. They can only
    -- run once and only if we're in the appropriate campaign.
    if state[token] then
      md.print('PROGRESS: Already processed token ' .. token)
    elseif toggle and isInCurrentCampaign(trigger) then
      local i = forEach(entry.inventory, processInventory)
      local r = forEach(entry.requisitions, processRequisition)
      if i or r then
        Backstock:SaveChanges()
        state[token] = true
        md.print('PROGRESS: Ran one-time unlocks for ' .. token)
      else
        md.print('PROGRESS: ' .. token .. ' has no one-time unlocks')
      end
      forEach(entry.updates, recordUpdate)
    end
  end

  local function run(reset)
    if reset then saveState({}) end
    local progress = bta:getGameProgress()
    local state = loadState()
    for _, entry in ipairs(config) do
      processEntry(entry, progress, state)
    end
    saveState(state)
    ui.OptionsMenu.savePlayerData()
  end

  return run, getUpdated, clearUpdated, recordUpdate
end)()

local sendUpdate, processUpdates = (function()
  local queue = {}

  local function sendUpdate(message)
    table.insert(queue, message)
  end

  local function processUpdates(send)
    if send then
      for _, message in ipairs(queue) do
        recordUpdate(message)
      end
    end
    queue = {}
  end

  return sendUpdate, processUpdates
end)()

return {
  run = run,
  getUpdated = getUpdated,
  clearUpdated = clearUpdated,
  sendUpdate = sendUpdate,
  processUpdates = processUpdates
}

