--[[
 exile_songs;
 Plays ambient music for Exile (or any game technically, though should be for Exile!!!)
 tracks specifically crafted for the environment of Exile
 
 even will play any music you add to /sounds/ ! So feel free to customize your experience :3
 
    Copyright (C) 2024 TPH (see readme.txt for more info)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
--]]

local is_v4 = minetest.get_modpath("tgcr") and true

local music_choices = {}

-- not fully sure how this precisely works, see my reference:
-- https://forums.x-plane.org/index.php?/forums/topic/183135-get-wav-file-duration-using-flywithlua/
-- depends on byte values obtained with string.byte() for data
local function BinToBit(data)
  local length = #data -- 4 or 8, 32 if 4, 64 if 8
  local multiplier = 256
  local bit = data[1] -- first byte value not being multiplied
  for i=2, length do
    if data[i] == 0 then break end -- no point to more calculation if it's a 0 anyways!
    bit = bit + (data[i]*multiplier) -- we add to the bit number by the byte value times the multiplier
    multiplier = multiplier^2 -- exponent the multiplier by 2
  end
  return bit
end

-- reference: https://stackoverflow.com/questions/20794204/how-to-determine-length-of-ogg-file
-- gets the duration of an audio file (ogg vorbis only, don't try this with anyone else lol)
-- round_decimal for how many decimals you want kept (integer)
local function get_song_duration(path, round_decimal)
  -- permit file being path if it's a file object
  local file = type(path) == "userdata" and path or io.open(path, "rb")
  local size = file:seek("end") -- go to end of file
  file:seek("end",-size) -- bring reader back to beginning
  local file_string = file:read(size) -- now utilize the size of the file to get the ENTIRE thing
  file:close() -- don't need this anymore, close it!
  -- as mentioned in the stackoverflow, we have to find the last OggS -- which we do by reversing the string
  -- and finding it, reversed! and we get its index, we only need the first return
  local ind = file_string:reverse():find("SggO")
  ind = size - ind -- we've gotta inverse it to get the position we want
  ind = ind + 3 -- so we skip a byte to get past the S of OggS, then 2 more to get past the 2 unused bytes
  -- now we create a table of bytes to be used in BinToBit
  local dur = {}
  -- duration will be 64bit (8 bytes)
  for i=1, 8 do
    i = ind + i -- add ind to i for sub
    dur[#dur + 1] = file_string:sub(i,i):byte() -- convert to byte value
  end
  --core.log(table.concat(dur," ")) -- see the 8 bytes in their glory!
  dur = BinToBit(dur)
  -- now to get rate -- we get the value after the first vorbis
  ind = file_string:find("vorbis")
  ind = ind + 10 -- we skip 10 bytes, not entirely sure why
  local rate = {}
  -- rate will be 32bit (4 bytes)
  for i=1, 4 do
    i = ind + i -- add ind to i for sub
    rate[#rate + 1] = file_string:sub(i,i):byte()
  end
  --core.log(table.concat(rate," ")) -- see the 4 bytes in their glory!
  rate = BinToBit(rate)
  -- now to divide it all together!
  local duration = dur/rate
  -- round the song's duration a bit so it isn't so chaotic
  round_decimal = round_decimal or 2
  duration = math.floor((duration * (10^round_decimal)) + 0.5)/(10^round_decimal)
  return duration
end

-- load up provided songs into music_choices
local sounds_path = core.get_modpath("exile_songs").."/sounds/"
local songs = core.get_dir_list(sounds_path)
for _,name in pairs(songs) do
  -- verify if supported file (.ogg)
  if name:sub(-4) == ".ogg" then
    -- add sounds_path as we just have the name and file extension
    local songpath = sounds_path..name
    -- add to music choices
    music_choices[#music_choices + 1] = {
      name = name:sub(1,-5), -- Luanti like no .ogg
      -- get the length!
      length = get_song_duration(songpath)
    }
    -- just shows the song's file name with its found duration
    --local songdata = music_choices[#music_choices]
    --core.log(songdata.name.." : "..songdata.length)
  end
end
--[[
  Feel free to put in your own songs (ogg vorbis) in the /sounds/ file! This code will properly get the duration and play it accordingly!
  I might make this into an API at some point because of how useful this can be in my opinion
--]]

-- utilizes os.time() to properly play songs even if game is paused
-- used to determine when to play a new song
-- core.after and globalstep do not account for when the game is paused
local base_time = os.time() -- utilize os.time() at load time to create a reference
local function get_time(since)
  -- time since base_time
  local return_time = os.time() - base_time
  -- if provided, use the provided get_time() value aka "since"
  -- to create a references of seconds since, well, the since
  if since then
    return_time = return_time - since
  end
  return return_time
end

-- just adds a quickie function for choosing a song
local function pick_song()
  return music_choices[math.random(1, #music_choices)]
end

-- basics for creating a data table for music
local function format_playable_music_table(data)
  data.gain = data.gain or 0.15
  data.pitch = data.pitch or 1
  -- divide length by pitch to get proper length
  -- lower pitches would make a longer length, higher ones a shorter length
  data.length = data.length/data.pitch
  data.started = get_time()
  return data
end

-- data, plrname, last_played
-- data is a sound spec, plrname is a player's name (used for to_player), and last_played is a string
-- last_played is used to avoid playing the same song after it had played
local function play_music(data, plrname, last_played)
  data = data or pick_song()
  if not (data.name or data.length) then return end
  -- we aint about to infinite loop
  if #music_choices > 1 then
    -- we basically just don't want to play the same song again lol
    while data.name == last_played do
      data = pick_song()
    end
  end
  data = table.copy(data)
  format_playable_music_table(data)
  data.to_player = plrname
  data.id = core.sound_play(data.name, data)
  return data
end

-- for determining if there's an exile_theme to check for and avoid playing music until over
local lore_path = core.get_modpath("lore") -- check for lore mod
local exile_theme
if lore_path then
  -- set up sound name, path
  exile_theme = "exile_theme"
  exile_theme = {path=lore_path.."/music/"..exile_theme..".ogg", name=exile_theme}
end

-- sneaky sneaky you!
local function unique_message()
  if lore_path then return end -- we're playing Exile, no need
  local username = "<TPH> "
  if core.is_singleplayer() then
    local messages = {
      "Huh... You're using my songs mod... But not for Exile..?",
      "I mean, yeah sure, it works without Exile, but it's surprising to see this somewhere else!",
      "Exile is free and open source, with more features to come :D but I'm glad you liked this mod enough to...",
      "Use it in another game!",
      "Hope these messages aren't too annoying - thought it'd be funny for a somewhat self-aware mod pfft x3",
      "Have a great day and especially have fun! :3",
    }
    local len = 0
    for _,mes in pairs(messages) do
      len = len + #mes/12
      minetest.after(len, function()
        core.chat_send_all(username..mes)
      end)
    end
  end
end


local listening = {}

-- check table
-- count is based off of dtime
-- verify checks through listening table to check whether or not someone needs a new song to play
local check = {count = 0, verify = -1}
-- only start music globalstep after all mods have loaded
core.register_on_mods_loaded(function()
  minetest.after(2.5, unique_message)
  core.register_globalstep(function(dtime)
    check.count = check.count + dtime
    -- music function
    if check.count > check.verify then
      -- check every 0.5 seconds
      check.verify = check.count + 0.5
      for name,stats in pairs(listening) do
        -- mechanics with when a song is playing
        if stats.song then
          local song = stats.song -- for easier handling
          -- if this song is over, stop and remove the song data from stats
          if get_time(song.started) > song.length then
            -- will crash if there's no ID, so let's avoid that if there isn't one
            if song.id then
                core.sound_stop(song.id)
            end
            stats.song = nil
          end
        end
        -- no song is playing, let's play one!
        if not stats.song then
          -- check last_played so that play_music doesn't play the same old song
          stats.song = play_music(nil, name, stats.last_played)
          -- set last_played to the newest song
          stats.last_played = stats.song.name
          -- add 2 second delay
          stats.song.length = stats.song.length + 2
        end
      end
    end
  end)
end)

-- ONLY use on new player event
local function get_exile_theme_data()
  if exile_theme then
    -- no duration was got, crack open the file!
    if not exile_theme.duration then
      exile_theme.file = io.open(exile_theme.path, "rb")
      -- no file, no worries, remove exile_theme reference
      if not exile_theme.file then
        exile_theme = nil
        return
      end
      -- file gets closed in get_song_duration
      exile_theme.duration = get_song_duration(exile_theme.file)
      -- remove reference
      exile_theme.file = nil
    end
    -- there obviously is a theme, return true!
    return true
  end
end

core.register_on_newplayer(function(plr)
  -- only get data if newplayer
  local is_theme = get_exile_theme_data()
  -- no theme, shouldn't do anything
  if not is_theme then return end
  -- beginning theme was found, get the data we need
  -- get information on duration (don't need name)
  local song = {
    length = exile_theme.duration -- check lore_path value
  }
  format_playable_music_table(song) -- format accordingly
  song.length = song.length + 9 -- 8 for minetest.after in lore/login (add a bit for potential lag)
  -- takes longer to spawn in v4, idk why, so add 45 instead of 20
  song.length = is_v4 and song.length + 45 or 20 -- 20 for whatever was being read
  listening[plr:get_player_name()] = {song = song}
end)

-- add stats table when player joins
core.register_on_joinplayer(function(plr)
  local pname = plr:get_player_name()
  -- do NOT conflict with newplayer
  if listening[pname] then return end
  listening[plr:get_player_name()] = {}
end)

-- clear out when player leaves
core.register_on_leaveplayer(function(plr)
  listening[plr:get_player_name()] = nil
end)

-- #TODO:
-- have certain songs be biome specific
-- have some system for where it's more customizable - having custom pitches and volumes be specified
