local logger = require("common.lib.logger")
local tableUtils = require("common.lib.tableUtils")
local fileUtils = require("client.src.FileUtils")
local consts = require("common.engine.consts")
local GraphicsUtil = require("client.src.graphics.graphics_util")
local StageTrack = require("client.src.music.StageTrack")
local DynamicStageTrack = require("client.src.music.DynamicStageTrack")
local RelayStageTrack = require("client.src.music.RelayStageTrack")
local class = require("common.lib.class")
local Mod = require("client.src.mods.Mod")
local UpdatingImage = require("client.src.graphics.UpdatingImage")
local SoundController = require("client.src.music.SoundController")
local Music = require("client.src.music.Music")

-- Stuff defined in this file:
--  . the data structure that store a stage's data

local basic_images = {"thumbnail"}
local allImages = {"thumbnail", "background"}
local defaulted_images = {thumbnail = true, background = true} -- those images will be defaulted if missing
local basic_musics = {}
local other_musics = {"normal_music", "danger_music"}

local randomStage = nil -- acts as the bundle stage for all theme stages

---@class Stage : Mod
---@field display_name string
---@field music_style string defines the behaviour for music when switching between normal and danger
---@field music_volume number defines a multiplier to apply to the StageTrack
---@field images table<string, love.Texture> graphical assets of the stage
---@field musics table<string, Music> music of the stage
---@field hasMusic boolean? if the stage has any music
---@field stageTrack StageTrack? the StageTrack constructed from the stage's music assets
local Stage =
  class(
  function(s, full_path, folder_name)
    s.path = full_path -- string | path to the stage folder content
    s.id = folder_name -- string | id of the stage, specified in config.json
    s.display_name = s.id -- string | display name of the stage
    s.images = {} -- images 
    s.musics = {} -- music
    s.music_style = "normal"
    s.music_volume = 1
    s.stageTrack = nil
  end,
  Mod
)

Stage.TYPE = "stage"
-- name of the top level save directory for mods of this type
Stage.SAVE_DIR = "stages"

function Stage.json_init(self)
  local read_data = fileUtils.readJsonFile(self.path .. "/config.json")

  if read_data then
    if read_data.id and type(read_data.id) == "string" then
      self.id = read_data.id

      -- sub ids for bundles
      if read_data.sub_ids and type(read_data.sub_ids) == "table" then
        self.subIds = read_data.sub_ids
      end

      -- display name
      if read_data.name and type(read_data.name) == "string" then
        self.display_name = read_data.name
      end
      -- is visible
      if read_data.visible ~= nil and type(read_data.visible) == "boolean" then
        self.isVisible = read_data.visible
      elseif read_data.visible and type(read_data.visible) == "string" then
        self.isVisible = read_data.visible == "true"
      end

      --music style
      if read_data.music_style and type(read_data.music_style) == "string" then
        self.music_style = read_data.music_style
      end

      if read_data.music_volume and type(read_data.music_volume) == "number" then
        self.music_volume = read_data.music_volume
      end

      return true
    end
  end

  return false
end

-- preemptively loads a stage
function Stage.preload(self)
  logger.debug("preloading stage " .. self.id)
  self:graphics_init(false, false)
  self:sound_init(false, false)
end

-- loads a stage
function Stage.load(self, instant)
  self:graphics_init(true, (not instant))
  self:sound_init(true, (not instant))
  self.fullyLoaded = true
  logger.debug("loaded stage " .. self.id)
end

-- unloads a stage
function Stage.unload(self)
  logger.debug("unloading stage " .. self.id)
  self:graphics_uninit()
  self:sound_uninit()
  self.fullyLoaded = false
  logger.debug("unloaded stage " .. self.id)
end

-- for reloading the graphics if the window was resized
function stages_reload_graphics()
  if stages then
    -- lazy load everything
    for _, stage in pairs(stages) do
      stage:graphics_init(false, false)
    end
    require("client.src.mods.StageLoader").loadBundleThumbnails()

    -- reload the current stage graphics immediately
    local match = GAME.battleRoom and GAME.battleRoom.match
    if match and match.stageId then
      if stages[match.stageId] then
        stages[match.stageId]:graphics_init(true, false)
        -- for reasons, this is not drawn directly from the stage but from background image
        -- so override this while in a match
        GAME.backgroundImage = UpdatingImage(stages[match.stageId].images.background, false, 0, 0, consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT)
      end
    end
  end
end

function Stage:getSubMods()
  local m = {}
  for _, id in ipairs(self.subIds) do
    if stages[id] then
      m[#m + 1] = stages[id]
    end
  end
  return m
end

function Stage:enable(enable)
  if enable and not stages[self.id] then
    stages[self.id] = self
    visibleStages[#visibleStages+1] = self.id
  elseif not enable and stages[self.id] then
    local i = tableUtils.indexOf(visibleStages, self.id)
    if i then
      table.remove(visibleStages, i)
    end
    stages[self.id] = nil
  end

  require("client.src.mods.ModLoader").updateBlacklist(self, enable)
end

function Stage.loadDefaultMod()
  themes[config.theme]:loadDefaultStage()
end

-- initalizes stage graphics
function Stage.graphics_init(self, full, yields)
  local stage_images = full and allImages or basic_images
  for _, image_name in ipairs(stage_images) do
    self.images[image_name] = GraphicsUtil.loadImageFromSupportedExtensions(self.path .. "/" .. image_name)
    if not self.images[image_name] and defaulted_images[image_name] and not self:isBundle() then
      self.images[image_name] = themes[config.theme].defaultStage.images[image_name]
      if not self.images[image_name] then
        error("Could not find default stage image")
      end
    end
    if yields then
      coroutine.yield()
    end
  end
end

-- bundles without stage thumbnail display up to 4 thumbnails of their substages
function Stage:createBundleThumbnail()
  local canvas = love.graphics.newCanvas(2 * 80, 2 * 45)
  canvas:renderTo(function()
    for i, substageId in ipairs(self.subIds) do
      if i <= 4 and (stages[substageId] or (allStages[substageId] and #self:getSubMods() == 0)) then
        local stage = allStages[substageId]
        local x = 0
        local y = 0
        if i % 2 == 0 then
          x = 80
        end
        if i > 2 then
          y = 45
        end
        local width, height = stage.images.thumbnail:getDimensions()
        love.graphics.draw(stage.images.thumbnail, x, y, 0, 80 / width, 45 / height)
      end
    end
  end)
  return canvas
end

-- uninits stage graphics
function Stage.graphics_uninit(self)
  for imageName, _ in pairs(self.images) do
    if not tableUtils.contains(basic_images, imageName) then
      self.images[imageName] = nil
    end
  end
end

-- applies the current configuration volume to a stage
function Stage.applyConfigVolume(self)
  SoundController:applyMusicVolume(self.musics)
end

-- initializes stage music
function Stage.sound_init(self, full, yields)
  self.hasMusic = not not fileUtils.getSoundFileName("normal_music", self.path)
  if self:isBundle() then
    return
  end
  local stage_musics = full and other_musics or basic_musics
  for _, music in ipairs(stage_musics) do
    self.musics[music] = Music.load(self.path, music)

    if yields then
      coroutine.yield()
    end
  end

  if full and self.musics.normal_music then
    if self.music_style == "normal" then
      self.stageTrack = StageTrack(self.musics.normal_music, self.musics.danger_music, self.music_volume)
    elseif self.music_style == "dynamic" then
      if self.musics.danger_music then
        self.stageTrack = DynamicStageTrack(self.musics.normal_music, self.musics.danger_music, self.music_volume)
      else
        -- DynamicStageTrack HAVE to have danger music
        -- default back to a regular stage track if there is none
        self.stageTrack = StageTrack(self.musics.normal_music, nil, self.music_volume)
      end
    elseif self.music_style == "relay" then
      self.stageTrack = RelayStageTrack(self.musics.normal_music, self.musics.danger_music, self.music_volume)
    end
  end

  self:applyConfigVolume()
end

-- uninits stage music
function Stage.sound_uninit(self)
  -- music
  for _, music in ipairs(other_musics) do
    self.musics[music] = nil
  end
end

function Stage:validate()
  -- validate that the mod has both normal and danger music if it is dynamic
  -- do this on initialization so modders get a crash on load and know immediately what to fix
  local valid, err
  if self.music_style == "dynamic" then
    local normalMusicFile = fileUtils.getSoundFileName("normal_music", self.path)
    local dangerMusicFile = fileUtils.getSoundFileName("danger_music", self.path)
    if normalMusicFile and not dangerMusicFile then
      err = "Error loading stage " .. self.id .. "\n at "
                  .. self.path ..
                  ":\n Stages with dynamic music must have a normal_music and danger_music file"
      return false, err
    end
  end

  valid, err = Music.validate(self.path, "normal_music")
  if not valid then
    return valid, err
  end

  valid, err = Music.validate(self.path, "danger_music")
  if not valid then
    return valid, err
  end

  return true
end

local function loadRandomStage(visibleStages)
  local randomStage = Stage("stages/__default", consts.RANDOM_STAGE_SPECIAL_VALUE)
  randomStage.images.thumbnail = themes[config.theme].images.IMG_random_stage
  randomStage.display_name = "random"
  randomStage.subIds = visibleStages
  -- we need to shadow some stage functions to correct load behaviour for the random stage
  randomStage.preload = function() end
  randomStage.load = function() end
  randomStage.unload = function() end
  randomStage.graphics_init = function(stage, full, yields)
    stage.images.thumbnail = themes[config.theme].images.IMG_random_stage
  end
  return randomStage
end

function Stage.getRandom(visibleStages)
  if not randomStage then
    randomStage = loadRandomStage(visibleStages)
  elseif visibleStages then
    randomStage.subIds = visibleStages
  end

  return randomStage
end

return Stage