/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */

#include "ags/shared/core/platform.h"
#include "ags/engine/media/audio/audio.h"
#include "ags/shared/ac/audio_clip_type.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/dynobj/cc_audio_clip.h"
#include "ags/engine/ac/dynobj/cc_audio_channel.h"
#include "ags/engine/ac/game_state.h"
#include "ags/engine/script/script_runtime.h"
#include "ags/engine/ac/audio_channel.h"
#include "ags/engine/ac/audio_clip.h"
#include "ags/engine/ac/game_setup.h"
#include "ags/engine/ac/path_helper.h"
#include "ags/engine/media/audio/sound.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/engine/debugging/debugger.h"
#include "ags/engine/platform/base/sys_main.h"
#include "ags/shared/ac/common.h"
#include "ags/engine/ac/file.h"
#include "ags/engine/ac/global_audio.h"
#include "ags/shared/util/stream.h"
#include "ags/shared/core/asset_manager.h"
#include "ags/engine/ac/timer.h"
#include "ags/engine/main/game_run.h"
#include "ags/globals.h"
#include "ags/ags.h"

namespace AGS3 {

using namespace AGS::Shared;
using namespace AGS::Engine;

//-----------------------
//sound channel management; all access goes through here, which can't be done without a lock

SOUNDCLIP *AudioChans::GetChannel(int index) {
	return _GP(audioChannels)[index];
}

SOUNDCLIP *AudioChans::GetChannelIfPlaying(int index) {
	auto *ch = _GP(audioChannels)[index];
	return (ch != nullptr && ch->is_playing()) ? ch : nullptr;
}

SOUNDCLIP *AudioChans::SetChannel(int index, SOUNDCLIP *ch) {
	SoundClipWaveBase *wavClip = dynamic_cast<SoundClipWaveBase *>(ch);
	if (wavClip) {
		switch (index) {
		case SCHAN_SPEECH:
			wavClip->setType(Audio::Mixer::kSpeechSoundType);
			break;
		case SCHAN_MUSIC:
			wavClip->setType(Audio::Mixer::kMusicSoundType);
			break;
		default:
			wavClip->setType(Audio::Mixer::kSFXSoundType);
			break;
		}
	}

	// TODO: store clips in smart pointers
	if (_GP(audioChannels)[index] == ch)
		Debug::Printf(kDbgMsg_Warn, "WARNING: channel %d - same clip assigned", index);
	else if (_GP(audioChannels)[index] != nullptr && ch != nullptr)
		Debug::Printf(kDbgMsg_Warn, "WARNING: channel %d - clip overwritten", index);
	_GP(audioChannels)[index] = ch;
	return ch;
}

SOUNDCLIP *AudioChans::MoveChannel(int to, int from) {
	auto from_ch = _GP(audioChannels)[from];
	_GP(audioChannels)[from] = nullptr;
	return SetChannel(to, from_ch);
}

void calculate_reserved_channel_count() {
	int reservedChannels = 0;
	for (size_t i = 0; i < _GP(game).audioClipTypes.size(); i++) {
		reservedChannels += _GP(game).audioClipTypes[i].reservedChannels;
	}
	_G(reserved_channel_count) = reservedChannels;
}

void update_clip_default_volume(ScriptAudioClip *audioClip) {
	if (_GP(play).default_audio_type_volumes[audioClip->type] >= 0) {
		audioClip->defaultVolume = _GP(play).default_audio_type_volumes[audioClip->type];
	}
}

void start_fading_in_new_track_if_applicable(int fadeInChannel, ScriptAudioClip *newSound) {
	int crossfadeSpeed = _GP(game).audioClipTypes[newSound->type].crossfadeSpeed;
	if (crossfadeSpeed > 0) {
		update_clip_default_volume(newSound);
		_GP(play).crossfade_in_volume_per_step = crossfadeSpeed;
		_GP(play).crossfade_final_volume_in = newSound->defaultVolume;
		_GP(play).crossfading_in_channel = fadeInChannel;
	}
}

static void move_track_to_crossfade_channel(int currentChannel, int crossfadeSpeed, int fadeInChannel, ScriptAudioClip *newSound) {
	stop_and_destroy_channel(SPECIAL_CROSSFADE_CHANNEL);
	auto *cfade_clip = AudioChans::MoveChannel(SPECIAL_CROSSFADE_CHANNEL, currentChannel);
	if (!cfade_clip)
		return;

	_GP(play).crossfading_out_channel = SPECIAL_CROSSFADE_CHANNEL;
	_GP(play).crossfade_step = 0;
	_GP(play).crossfade_initial_volume_out = cfade_clip->get_volume100();
	_GP(play).crossfade_out_volume_per_step = crossfadeSpeed;

	_GP(play).crossfading_in_channel = fadeInChannel;
	if (newSound != nullptr) {
		start_fading_in_new_track_if_applicable(fadeInChannel, newSound);
	}
}

// NOTE: this function assumes one of the user channels
void stop_or_fade_out_channel(int fadeOutChannel, int fadeInChannel, ScriptAudioClip *newSound) {
	ScriptAudioClip *sourceClip = AudioChannel_GetPlayingClip(&_G(scrAudioChannel)[fadeOutChannel]);
	if ((sourceClip != nullptr) && (_GP(game).audioClipTypes[sourceClip->type].crossfadeSpeed > 0)) {
		move_track_to_crossfade_channel(fadeOutChannel, _GP(game).audioClipTypes[sourceClip->type].crossfadeSpeed, fadeInChannel, newSound);
	} else {
		stop_and_destroy_channel(fadeOutChannel);
	}
}

static int find_free_audio_channel(ScriptAudioClip *clip, int priority, bool interruptEqualPriority) {
	int lowestPrioritySoFar = 9999999;
	int lowestPriorityID = -1;
	int channelToUse = -1;

	if (!interruptEqualPriority)
		priority--;

	// NOTE: in backward compat mode we allow to place sound on a crossfade channel
	int startAtChannel = _G(reserved_channel_count);
	int endBeforeChannel = _GP(game).numCompatGameChannels;

	if (_GP(game).audioClipTypes[clip->type].reservedChannels > 0) {
		startAtChannel = 0;
		for (int i = 0; i < clip->type; i++) {
			startAtChannel += MIN(MAX_SOUND_CHANNELS,
				_GP(game).audioClipTypes[i].reservedChannels);
		}
		endBeforeChannel = MIN(_GP(game).numCompatGameChannels,
			startAtChannel + _GP(game).audioClipTypes[clip->type].reservedChannels);
	}

	for (int i = startAtChannel; i < endBeforeChannel; i++) {
		auto *ch = AudioChans::GetChannelIfPlaying(i);
		if (ch == nullptr) {
			channelToUse = i;
			stop_and_destroy_channel(i);
			break;
		}
		if ((ch->_priority < lowestPrioritySoFar) &&
		        (ch->_sourceClipType == clip->type)) {
			lowestPrioritySoFar = ch->_priority;
			lowestPriorityID = i;
		}
	}

	if ((channelToUse < 0) && (lowestPriorityID >= 0) &&
	        (lowestPrioritySoFar <= priority)) {
		stop_or_fade_out_channel(lowestPriorityID, lowestPriorityID, clip);
		channelToUse = lowestPriorityID;
	} else if ((channelToUse >= 0) && (_GP(play).crossfading_in_channel < 1)) {
		start_fading_in_new_track_if_applicable(channelToUse, clip);
	}
	return channelToUse;
}

bool is_audiotype_allowed_to_play(AudioFileType /*type*/) {
	// TODO: this is a remnant of an old audio logic, think this function over
	return _GP(usetup).audio_enabled;
}

SOUNDCLIP *load_sound_clip(ScriptAudioClip *audioClip, bool repeat) {
	if (!is_audiotype_allowed_to_play((AudioFileType)audioClip->fileType)) {
		return nullptr;
	}

	update_clip_default_volume(audioClip);

	SOUNDCLIP *soundClip = nullptr;
	AssetPath asset_name = get_audio_clip_assetpath(audioClip->bundlingType, audioClip->fileName);
	switch (audioClip->fileType) {
	case eAudioFileOGG:
		soundClip = my_load_static_ogg(asset_name, repeat);
		break;
	case eAudioFileMP3:
		soundClip = my_load_static_mp3(asset_name, repeat);
		break;
	case eAudioFileWAV:
	case eAudioFileVOC:
		soundClip = my_load_wave(asset_name, repeat);
		break;
	case eAudioFileMIDI:
		soundClip = my_load_midi(asset_name, repeat);
		break;
	case eAudioFileMOD:
		soundClip = my_load_mod(asset_name, repeat);
		break;
	default:
		quitprintf("AudioClip.Play: invalid audio file type encountered: %d", audioClip->fileType);
	}
	if (soundClip != nullptr) {
		soundClip->set_volume100(audioClip->defaultVolume);
		soundClip->_sourceClipID = audioClip->id;
		soundClip->_sourceClipType = audioClip->type;
	}
	return soundClip;
}

static void audio_update_polled_stuff() {
	///////////////////////////////////////////////////////////////////////////
	// Do crossfade
	_GP(play).crossfade_step++;

	if (_GP(play).crossfading_out_channel > 0 && !AudioChans::GetChannelIfPlaying(_GP(play).crossfading_out_channel))
		_GP(play).crossfading_out_channel = 0;

	if (_GP(play).crossfading_out_channel > 0) {
		SOUNDCLIP *ch = AudioChans::GetChannel(_GP(play).crossfading_out_channel);
		int newVolume = ch ? ch->get_volume100() - _GP(play).crossfade_out_volume_per_step : 0;
		if (newVolume > 0) {
			ch->set_volume100(newVolume);
		} else {
			stop_and_destroy_channel(_GP(play).crossfading_out_channel);
			_GP(play).crossfading_out_channel = 0;
		}
	}

	if (_GP(play).crossfading_in_channel > 0 && !AudioChans::GetChannelIfPlaying(_GP(play).crossfading_in_channel))
		_GP(play).crossfading_in_channel = 0;

	if (_GP(play).crossfading_in_channel > 0) {
		SOUNDCLIP *ch = AudioChans::GetChannel(_GP(play).crossfading_in_channel);
		int newVolume = ch ? ch->get_volume100() + _GP(play).crossfade_in_volume_per_step : 0;
		if (newVolume > _GP(play).crossfade_final_volume_in) {
			newVolume = _GP(play).crossfade_final_volume_in;
		}

		ch->set_volume100(newVolume);

		if (newVolume >= _GP(play).crossfade_final_volume_in) {
			_GP(play).crossfading_in_channel = 0;
		}
	}

	///////////////////////////////////////////////////////////////////////////
	// Do audio queue
	if (_GP(play).new_music_queue_size > 0) {
		for (int i = 0; i < _GP(play).new_music_queue_size; i++) {
			ScriptAudioClip *clip = &_GP(game).audioClips[_GP(play).new_music_queue[i].audioClipIndex];
			int channel = find_free_audio_channel(clip, clip->defaultPriority, false);
			if (channel >= 0) {
				QueuedAudioItem itemToPlay = _GP(play).new_music_queue[i];

				_GP(play).new_music_queue_size--;
				for (int j = i; j < _GP(play).new_music_queue_size; j++) {
					_GP(play).new_music_queue[j] = _GP(play).new_music_queue[j + 1];
				}

				play_audio_clip_on_channel(channel, clip, itemToPlay.priority, itemToPlay.repeat, 0, itemToPlay.cachedClip);
				i--;
			}
		}
	}

	///////////////////////////////////////////////////////////////////////////
	// Do non-blocking voice speech
	// NOTE: there's only one speech channel, therefore it's either blocking
	// or non-blocking at any given time. If it's changed, we'd need to keep
	// record of every channel, or keep a count of active channels.
	if (_GP(play).IsNonBlockingVoiceSpeech()) {
		if (!AudioChans::ChannelIsPlaying(SCHAN_SPEECH)) {
			stop_voice_nonblocking();
		}
	}
}

// Applies a volume drop modifier to the clip, in accordance to its audio type
static void apply_volume_drop_to_clip(SOUNDCLIP *clip) {
	int audiotype = clip->_sourceClipType;
	clip->apply_volume_modifier(-(_GP(game).audioClipTypes[audiotype].volume_reduction_while_speech_playing * 255 / 100));
}

static void queue_audio_clip_to_play(ScriptAudioClip *clip, int priority, int repeat) {
	if (_GP(play).new_music_queue_size >= MAX_QUEUED_MUSIC) {
		debug_script_log("Too many queued music, cannot add %s", clip->scriptName.GetCStr());
		return;
	}

	SOUNDCLIP *cachedClip = load_sound_clip(clip, (repeat != 0));
	if (cachedClip != nullptr) {
		_GP(play).new_music_queue[_GP(play).new_music_queue_size].audioClipIndex = clip->id;
		_GP(play).new_music_queue[_GP(play).new_music_queue_size].priority = priority;
		_GP(play).new_music_queue[_GP(play).new_music_queue_size].repeat = (repeat != 0);
		_GP(play).new_music_queue[_GP(play).new_music_queue_size].cachedClip = cachedClip;
		_GP(play).new_music_queue_size++;
	}
}

ScriptAudioChannel *play_audio_clip_on_channel(int channel, ScriptAudioClip *clip, int priority, int repeat, int fromOffset, SOUNDCLIP *soundfx) {
	if (soundfx == nullptr) {
		soundfx = load_sound_clip(clip, (repeat) ? true : false);
	}
	if (soundfx == nullptr) {
		debug_script_log("AudioClip.Play: unable to load sound file");
		if (_GP(play).crossfading_in_channel == channel) {
			_GP(play).crossfading_in_channel = 0;
		}
		return nullptr;
	}
	soundfx->_priority = priority;

	if (_GP(play).crossfading_in_channel == channel) {
		soundfx->set_volume100(0);
	}

	// Mute the audio clip if fast-forwarding the cutscene
	if (_GP(play).fast_forward) {
		soundfx->set_mute(true);

		// CHECKME!!
		// [IKM] According to the 3.2.1 logic the clip will restore
		// its value after cutscene, but only if originalVolAsPercentage
		// is not zeroed. Something I am not sure about: why does it
		// disable the clip under condition that there's more than one
		// channel for this audio type? It does not even check if
		// anything of this type is currently playing.
		if (_GP(game).audioClipTypes[clip->type].reservedChannels != 1)
			soundfx->set_volume100(0);
	}

	if (soundfx->play_from(fromOffset) == 0) {
		// not assigned to a channel, so clean up manually.
		delete soundfx;
		soundfx = nullptr;
		debug_script_log("AudioClip.Play: failed to play sound file");
		return nullptr;
	}

	// Apply volume drop if any speech voice-over is currently playing
	// NOTE: there is a confusing logic in sound clip classes, that they do not use
	// any modifiers when begin playing, therefore we must apply this only after
	// playback was started.
	if (!_GP(play).fast_forward && _GP(play).speech_has_voice)
		apply_volume_drop_to_clip(soundfx);

	AudioChans::SetChannel(channel, soundfx);
	return &_G(scrAudioChannel)[channel];
}

void remove_clips_of_type_from_queue(int audioType) {
	int aa;
	for (aa = 0; aa < _GP(play).new_music_queue_size; aa++) {
		ScriptAudioClip *clip = &_GP(game).audioClips[_GP(play).new_music_queue[aa].audioClipIndex];
		if (clip->type == audioType) {
			_GP(play).new_music_queue_size--;
			for (int bb = aa; bb < _GP(play).new_music_queue_size; bb++)
				_GP(play).new_music_queue[bb] = _GP(play).new_music_queue[bb + 1];
			aa--;
		}
	}
}

void update_queued_clips_volume(int audioType, int new_vol) {
	for (int i = 0; i < _GP(play).new_music_queue_size; ++i) {
		// NOTE: if clip is uncached, the volume will be set from defaults when it is loaded
		SOUNDCLIP *sndclip = _GP(play).new_music_queue[i].cachedClip;
		if (sndclip) {
			ScriptAudioClip *clip = &_GP(game).audioClips[_GP(play).new_music_queue[i].audioClipIndex];
			if (clip->type == audioType)
				sndclip->set_volume100(new_vol);
		}
	}
}

ScriptAudioChannel *play_audio_clip(ScriptAudioClip *clip, int priority, int repeat, int fromOffset, bool queueIfNoChannel) {
	if (!queueIfNoChannel)
		remove_clips_of_type_from_queue(clip->type);

	if (priority == SCR_NO_VALUE)
		priority = clip->defaultPriority;
	if (repeat == SCR_NO_VALUE)
		repeat = clip->defaultRepeat;

	int channel = find_free_audio_channel(clip, priority, !queueIfNoChannel);
	if (channel < 0) {
		if (queueIfNoChannel)
			queue_audio_clip_to_play(clip, priority, repeat);
		else
			debug_script_log("AudioClip.Play: no channels available to interrupt PRI:%d TYPE:%d", priority, clip->type);

		return nullptr;
	}

	return play_audio_clip_on_channel(channel, clip, priority, repeat, fromOffset);
}

ScriptAudioChannel *play_audio_clip_by_index(int audioClipIndex) {
	if ((audioClipIndex >= 0) && ((size_t)audioClipIndex < _GP(game).audioClips.size()))
		return AudioClip_Play(&_GP(game).audioClips[audioClipIndex], SCR_NO_VALUE, SCR_NO_VALUE);
	else
		return nullptr;
}

void stop_and_destroy_channel_ex(int chid, bool resetLegacyMusicSettings) {
	if ((chid < 0) || (chid >= TOTAL_AUDIO_CHANNELS))
		quit("!StopChannel: invalid channel ID");

	SOUNDCLIP *ch = AudioChans::GetChannel(chid);

	if (ch != nullptr) {
		delete ch;
		AudioChans::SetChannel(chid, nullptr);
		ch = nullptr;
	}

	if (_GP(play).crossfading_in_channel == chid)
		_GP(play).crossfading_in_channel = 0;
	if (_GP(play).crossfading_out_channel == chid)
		_GP(play).crossfading_out_channel = 0;
	// don't update '_G(crossFading)' here as it is updated in all the cross-fading functions.

	// destroyed an ambient sound channel
	if (chid < _GP(game).numGameChannels) {
		if (_GP(ambient)[chid].channel > 0)
			_GP(ambient)[chid].channel = 0;
	}

	if ((chid == SCHAN_MUSIC) && (resetLegacyMusicSettings)) {
		_GP(play).cur_music_number = -1;
		_G(current_music_type) = 0;
	}
}

void stop_and_destroy_channel(int chid) {
	stop_and_destroy_channel_ex(chid, true);
}



// ***** BACKWARDS COMPATIBILITY WITH OLD AUDIO SYSTEM ***** //

int get_old_style_number_for_sound(int sound_number) {
	// In the legacy audio system treat sound_number as an old style number
	if (_GP(game).IsLegacyAudioSystem()) {
		return sound_number;
	}

	// Treat sound number as a real clip index
	if (sound_number >= 0) {
		int old_style_number = 0;
		if (sscanf(_GP(game).audioClips[sound_number].scriptName.GetCStr(), "aSound%d", &old_style_number) == 1)
			return old_style_number;
	}

	return 0;
}

SOUNDCLIP *load_sound_clip_from_old_style_number(bool isMusic, int indexNumber, bool repeat) {
	ScriptAudioClip *audioClip = GetAudioClipForOldStyleNumber(_GP(game), isMusic, indexNumber);

	if (audioClip != nullptr) {
		return load_sound_clip(audioClip, repeat);
	}

	return nullptr;
}

//=============================================================================

int get_volume_adjusted_for_distance(int volume, int sndX, int sndY, int sndMaxDist) {
	int distx = _G(playerchar)->x - sndX;
	int disty = _G(playerchar)->y - sndY;
	// it uses Allegro's "fix" sqrt without the ::
	int dist = (int)::sqrt((double)(distx * distx + disty * disty));

	// if they're quite close, full volume
	int wantvol = volume;

	if (dist >= AMBIENCE_FULL_DIST) {
		// get the relative volume
		wantvol = ((dist - AMBIENCE_FULL_DIST) * volume) / sndMaxDist;
		// closer is louder
		wantvol = volume - wantvol;
	}

	return wantvol;
}

void update_directional_sound_vol() {
	for (int chnum = NUM_SPEECH_CHANS; chnum < _GP(game).numGameChannels; chnum++) {
		auto *ch = AudioChans::GetChannelIfPlaying(chnum);
		if ((ch != nullptr) && (ch->_xSource >= 0)) {
			ch->apply_directional_modifier(
				get_volume_adjusted_for_distance(ch->get_volume255(),
					ch->_xSource,
					ch->_ySource,
					ch->_maximumPossibleDistanceAway) -
				ch->get_volume255());
		}
	}
}

void update_ambient_sound_vol() {
	for (int chan = NUM_SPEECH_CHANS; chan < _GP(game).numGameChannels; chan++) {
		AmbientSound *thisSound = &_GP(ambient)[chan];

		if (thisSound->channel == 0)
			continue;

		int sourceVolume = thisSound->vol;

		if (_GP(play).speech_has_voice) {
			// Negative value means set exactly; positive means drop that amount
			if (_GP(play).speech_music_drop < 0)
				sourceVolume = -_GP(play).speech_music_drop;
			else
				sourceVolume -= _GP(play).speech_music_drop;

			if (sourceVolume < 0)
				sourceVolume = 0;
			if (sourceVolume > 255)
				sourceVolume = 255;
		}

		// Adjust ambient volume so it maxes out at overall sound volume
		int ambientvol = (sourceVolume * _GP(play).sound_volume) / 255;

		int wantvol;

		if ((thisSound->x == 0) && (thisSound->y == 0)) {
			wantvol = ambientvol;
		} else {
			wantvol = get_volume_adjusted_for_distance(ambientvol, thisSound->x, thisSound->y, thisSound->maxdist);
		}

		auto *ch = AudioChans::GetChannelIfPlaying(thisSound->channel);
		if (ch)
			ch->set_volume255(wantvol);
	}
}

SOUNDCLIP *load_sound_and_play(ScriptAudioClip *aclip, bool repeat) {
	SOUNDCLIP *soundfx = load_sound_clip(aclip, repeat);
	if (!soundfx) {
		return nullptr;
	}

	if (soundfx->play() == 0) {
		// not assigned to a channel, so clean up manually.
		delete soundfx;
		return nullptr;
	}

	return soundfx;
}

void stop_all_sound_and_music() {
	stopmusic();
	stop_voice_nonblocking();
	// make sure it doesn't start crossfading when it comes back
	_G(crossFading) = 0;
	// any ambient sound will be aborted
	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i)
		stop_and_destroy_channel(i);
}

void shutdown_sound() {
	stop_all_sound_and_music(); // game logic
#if !AGS_PLATFORM_SCUMMVM
	audio_core_shutdown(); // audio core system
#endif
	sys_audio_shutdown(); // backend
	_GP(usetup).audio_enabled = false;
}

// the sound will only be played if there is a free channel or
// it has a priority >= an existing sound to override
static int play_sound_priority(int val1, int priority) {
	int lowest_pri = 9999, lowest_pri_id = -1;

	// find a free channel to play it on
	for (int i = SCHAN_NORMAL; i < _GP(game).numGameChannels; i++) {
		auto *ch = AudioChans::GetChannelIfPlaying(i);
		if (val1 < 0) {
			// Playing sound -1 means iterate through and stop all sound
			if (ch)
				stop_and_destroy_channel(i);
		} else if (ch == nullptr || !ch->is_playing()) {
			// PlaySoundEx will destroy the previous channel value.
			const int usechan = PlaySoundEx(val1, i);
			if (usechan >= 0) {
				// channel will hold a different clip here
				assert(usechan == i);
				auto *chan = AudioChans::GetChannel(usechan);
				if (chan)
					chan->_priority = priority;
			}
			return usechan;
		} else if (ch->_priority < lowest_pri) {
			lowest_pri = ch->_priority;
			lowest_pri_id = i;
		}

	}
	if (val1 < 0)
		return -1;

	// no free channels, see if we have a high enough priority
	// to override one
	if (priority >= lowest_pri) {
		const int usechan = PlaySoundEx(val1, lowest_pri_id);
		if (usechan >= 0) {
			assert(usechan == lowest_pri_id);
			auto *ch = AudioChans::GetChannel(usechan);
			if (ch)
				ch->_priority = priority;
			return usechan;
		}
	}

	return -1;
}

int play_sound(int val1) {
	return play_sound_priority(val1, 10);
}

//=============================================================================

void cancel_scheduled_music_update() {
	_G(music_update_scheduled) = false;
}

void schedule_music_update_at(AGS_Clock::time_point at) {
	_G(music_update_scheduled) = true;
	_G(music_update_at) = at;
}

void postpone_scheduled_music_update_by(std::chrono::milliseconds duration) {
	if (!_G(music_update_scheduled)) {
		return;
	}
	_G(music_update_at) += duration;
}

void process_scheduled_music_update() {
	if (!_G(music_update_scheduled)) {
		return;
	}
	if (_G(music_update_at) > AGS_Clock::now()) {
		return;
	}
	cancel_scheduled_music_update();
	update_music_volume();
	apply_volume_drop_modifier(false);
	update_ambient_sound_vol();
}
// end scheduled music update functions
//=============================================================================

void clear_music_cache() {

	if (_G(cachedQueuedMusic) != nullptr) {
		delete _G(cachedQueuedMusic);
		_G(cachedQueuedMusic) = nullptr;
	}

}

static void play_new_music(int mnum, SOUNDCLIP *music);

void play_next_queued() {
	// check if there's a queued one to play
	if (_GP(play).music_queue_size > 0) {

		int tuneToPlay = _GP(play).music_queue[0];

		if (tuneToPlay >= QUEUED_MUSIC_REPEAT) {
			// Loop it!
			_GP(play).music_repeat++;
			play_new_music(tuneToPlay - QUEUED_MUSIC_REPEAT, _G(cachedQueuedMusic));
			_GP(play).music_repeat--;
		} else {
			// Don't loop it!
			int repeatWas = _GP(play).music_repeat;
			_GP(play).music_repeat = 0;
			play_new_music(tuneToPlay, _G(cachedQueuedMusic));
			_GP(play).music_repeat = repeatWas;
		}

		// don't free the memory, as it has been transferred onto the
		// main music channel
		_G(cachedQueuedMusic) = nullptr;

		_GP(play).music_queue_size--;
		for (int i = 0; i < _GP(play).music_queue_size; i++)
			_GP(play).music_queue[i] = _GP(play).music_queue[i + 1];

		if (_GP(play).music_queue_size > 0)
			_G(cachedQueuedMusic) = load_music_from_disk(_GP(play).music_queue[0], 0);
	}

}

int calculate_max_volume() {
	// quieter so that sounds can be heard better
	int newvol = _GP(play).music_master_volume + ((int)_GP(thisroom).Options.MusicVolume) * LegacyRoomVolumeFactor;
	if (newvol > 255) newvol = 255;
	if (newvol < 0) newvol = 0;

	if (_GP(play).fast_forward)
		newvol = 0;

	return newvol;
}

// add/remove the volume drop to the audio channels while speech is playing
void apply_volume_drop_modifier(bool applyModifier) {
	for (int i = NUM_SPEECH_CHANS; i < _GP(game).numGameChannels; i++) {
		auto *ch = AudioChans::GetChannelIfPlaying(i);
		if (ch && ch->_sourceClipID >= 0) {
			if (applyModifier)
				apply_volume_drop_to_clip(ch);
			else
				ch->apply_volume_modifier(0); // reset modifier
		}
	}
}

// Checks if speech voice-over is currently playing, and reapply volume drop to all other active clips
void update_volume_drop_if_voiceover() {
	apply_volume_drop_modifier(_GP(play).speech_has_voice);
}

// Update the music, and advance the crossfade on a step
// (this should only be called once per game loop)
void update_audio_system_on_game_loop() {
	update_polled_stuff_if_runtime();

	process_scheduled_music_update();

	_G(audio_doing_crossfade) = true;

	audio_update_polled_stuff();

	if (_G(crossFading)) {
		_G(crossFadeStep)++;
		update_music_volume();
	}

	// Check if the current music has finished playing
	if ((_GP(play).cur_music_number >= 0) && (_GP(play).fast_forward == 0)) {
		if (IsMusicPlaying() == 0) {
			// The current music has finished
			_GP(play).cur_music_number = -1;
			play_next_queued();
		} else if ((_GP(game).options[OPT_CROSSFADEMUSIC] > 0) &&
		           (_GP(play).music_queue_size > 0) && (!_G(crossFading))) {
			// want to crossfade, and new tune in the queue
			auto *ch = AudioChans::GetChannel(SCHAN_MUSIC);
			if (ch) {
				int curpos = ch->get_pos_ms();
				int muslen = ch->get_length_ms();
				if ((curpos > 0) && (muslen > 0)) {
					// we want to crossfade, and we know how far through
					// the tune we are
					int takesSteps = calculate_max_volume() / _GP(game).options[OPT_CROSSFADEMUSIC];
					int takesMs = ::lround(takesSteps * 1000.0f / get_current_fps());
					if (curpos >= muslen - takesMs)
						play_next_queued();
				}
			}
		}
	}

	_G(audio_doing_crossfade) = false;

	if (_G(loopcounter) % 5 == 0) {
		update_ambient_sound_vol();
		update_directional_sound_vol();
	}

	// Update and sync logical game channels with the audio backend
	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
		auto *ch = AudioChans::GetChannel(i);
		if (ch) { // update the playing channel, and if it's finished then dispose it
			if (ch->is_ready() && !ch->update()) {
				delete ch;
				AudioChans::SetChannel(i, nullptr);
			}
		}
	}
}

void stopmusic() {
	if (_G(crossFading) > 0) {
		// stop in the middle of a new track fading in
		// Abort the new track, and let the old one finish fading out
		stop_and_destroy_channel(_G(crossFading));
		_G(crossFading) = -1;
	} else if (_G(crossFading) < 0) {
		// the music is already fading out
		if (_GP(game).options[OPT_CROSSFADEMUSIC] <= 0) {
			// If they have since disabled crossfading, stop the fadeout
			stop_and_destroy_channel(SCHAN_MUSIC);
			_G(crossFading) = 0;
			_G(crossFadeStep) = 0;
			update_music_volume();
		}
	} else if ((_GP(game).options[OPT_CROSSFADEMUSIC] > 0)
	           && (AudioChans::GetChannelIfPlaying(SCHAN_MUSIC) != nullptr)
	           && (_G(current_music_type) != 0)
	           && (_G(current_music_type) != MUS_MIDI)
	           && (_G(current_music_type) != MUS_MOD)) {

		_G(crossFading) = -1;
		_G(crossFadeStep) = 0;
		_G(crossFadeVolumePerStep) = _GP(game).options[OPT_CROSSFADEMUSIC];
		_G(crossFadeVolumeAtStart) = calculate_max_volume();
	} else
		stop_and_destroy_channel(SCHAN_MUSIC);

	_GP(play).cur_music_number = -1;
	_G(current_music_type) = 0;
}

void update_music_volume() {
	if ((_G(current_music_type)) || (_G(crossFading) < 0)) {
		// targetVol is the maximum volume we're fading in to
		// newvol is the starting volume that we faded out from
		int targetVol = calculate_max_volume();
		int newvol;
		if (_G(crossFading))
			newvol = _G(crossFadeVolumeAtStart);
		else
			newvol = targetVol;

		// fading out old track, target volume is silence
		if (_G(crossFading) < 0)
			targetVol = 0;

		if (_G(crossFading)) {
			int curvol = _G(crossFadeVolumePerStep) * _G(crossFadeStep);

			if ((curvol > targetVol) && (curvol > newvol)) {
				// it has fully faded to the new track
				newvol = targetVol;
				stop_and_destroy_channel_ex(SCHAN_MUSIC, false);
				if (_G(crossFading) > 0) {
					AudioChans::MoveChannel(SCHAN_MUSIC, _G(crossFading));
				}
				_G(crossFading) = 0;
			} else {
				if (_G(crossFading) > 0) {
					auto *ch = AudioChans::GetChannel(_G(crossFading));
					if (ch)
						ch->set_volume255((curvol > targetVol) ? targetVol : curvol);
				}

				newvol -= curvol;
				if (newvol < 0)
					newvol = 0;
			}
		}
		auto *ch = AudioChans::GetChannel(SCHAN_MUSIC);
		if (ch)
			ch->set_volume255(newvol);
	}
}

// Ensures crossfader is stable after loading (or failing to load) new music
// NOTE: part of the legacy audio logic
void post_new_music_check() {
	if ((_G(crossFading) > 0) && (AudioChans::GetChannel(_G(crossFading)) == nullptr)) {
		_G(crossFading) = 0;
		// Was fading out but then they played invalid music, continue to fade out
		if (AudioChans::GetChannel(SCHAN_MUSIC) != nullptr)
			_G(crossFading) = -1;
	}
}

int prepare_for_new_music() {
	int useChannel = SCHAN_MUSIC;

	if ((_GP(game).options[OPT_CROSSFADEMUSIC] > 0)
	        && (AudioChans::GetChannelIfPlaying(SCHAN_MUSIC) != nullptr)
	        && (_G(current_music_type) != MUS_MIDI)
	        && (_G(current_music_type) != MUS_MOD)) {

		if (_G(crossFading) > 0) {
			// It's still crossfading to the previous track
			stop_and_destroy_channel_ex(SCHAN_MUSIC, false);
			AudioChans::MoveChannel(SCHAN_MUSIC, _G(crossFading));
			_G(crossFading) = 0;
			update_music_volume();
		} else if (_G(crossFading) < 0) {
			// an old track is still fading out, no new music yet
			// Do nothing, and keep the current crossfade step
		} else {
			// start crossfading
			_G(crossFadeStep) = 0;
			_G(crossFadeVolumePerStep) = _GP(game).options[OPT_CROSSFADEMUSIC];
			_G(crossFadeVolumeAtStart) = calculate_max_volume();
		}
		useChannel = SPECIAL_CROSSFADE_CHANNEL;
		_G(crossFading) = useChannel;
	} else {
		// crossfading is now turned off
		stopmusic();
		// ensure that any traces of old tunes fading are eliminated
		// (otherwise the new track will be faded out)
		_G(crossFading) = 0;
	}

	// Just make sure, because it will be overwritten in a sec
	if (AudioChans::GetChannel(useChannel) != nullptr)
		stop_and_destroy_channel(useChannel);

	return useChannel;
}

ScriptAudioClip *get_audio_clip_for_music(int mnum) {
	if (mnum >= QUEUED_MUSIC_REPEAT)
		mnum -= QUEUED_MUSIC_REPEAT;
	return GetAudioClipForOldStyleNumber(_GP(game), true, mnum);
}

SOUNDCLIP *load_music_from_disk(int mnum, bool doRepeat) {
	if (mnum >= QUEUED_MUSIC_REPEAT) {
		mnum -= QUEUED_MUSIC_REPEAT;
		doRepeat = true;
	}

	SOUNDCLIP *loaded = load_sound_clip_from_old_style_number(true, mnum, doRepeat);

	if ((loaded == nullptr) && (mnum > 0)) {
		debug_script_warn("Music %d not found", mnum);
		debug_script_log("FAILED to load music %d", mnum);
	}

	return loaded;
}

static void play_new_music(int mnum, SOUNDCLIP *music) {
	if (_G(debug_flags) & DBG_NOMUSIC)
		return;

	if ((_GP(play).cur_music_number == mnum) && (music == nullptr)) {
		debug_script_log("PlayMusic %d but already playing", mnum);
		return;  // don't play the music if it's already playing
	}

	ScriptAudioClip *aclip = get_audio_clip_for_music(mnum);
	if (aclip && !is_audiotype_allowed_to_play((AudioFileType)aclip->fileType))
		return;

	int useChannel = SCHAN_MUSIC;
	debug_script_log("Playing music %d", mnum);

	if (mnum < 0) {
		stopmusic();
		return;
	}

	if (_GP(play).fast_forward) {
		// while skipping cutscene, don't change the music
		_GP(play).end_cutscene_music = mnum;
		return;
	}

	useChannel = prepare_for_new_music();
	_GP(play).cur_music_number = mnum;
	_G(current_music_type) = 0;

	_GP(play).current_music_repeating = _GP(play).music_repeat;
	// now that all the previous music is unloaded, load in the new one

	SOUNDCLIP *new_clip;
	if (music != nullptr)
		new_clip = music;
	else
		new_clip = load_music_from_disk(mnum, (_GP(play).music_repeat > 0));

	auto *ch = AudioChans::SetChannel(useChannel, new_clip);
	if (ch != nullptr) {
		if (!ch->play()) {
			// previous behavior was to set channel[] to null on error, so continue to do that here.
			delete ch;
			ch = nullptr;
			AudioChans::SetChannel(useChannel, nullptr);
		} else
			_G(current_music_type) = ch->get_sound_type();
	}

	post_new_music_check();
	update_music_volume();
}

void newmusic(int mnum) {
	play_new_music(mnum, nullptr);
}

} // namespace AGS3
