/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */
/* Handles Music Files */

#include "C4Include.h"
#include "platform/C4MusicFile.h"

#include "game/C4Application.h"

#if AUDIO_TK == AUDIO_TK_OPENAL
#if defined(__APPLE__)
#import <CoreFoundation/CoreFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
#else
#ifdef _WIN32
// This is an ugly hack to make FreeALUT not dllimport everything.
#define _XBOX
#endif
#include <AL/alut.h>
#undef _XBOX
#endif
#define alErrorCheck(X) do { X; { ALenum err = alGetError(); if (err) LogF("al error: %s (%x)", #X, err); } } while (0)
#endif

/* helpers */

void C4MusicFile::Announce()
{
	LogF(LoadResStr("IDS_PRC_PLAYMUSIC"), GetFilename(FileName));
	announced = true;
}

bool C4MusicFile::ExtractFile()
{
	// safety
	if (SongExtracted) return true;
	// extract entry
	if (!C4Group_CopyItem(FileName, Config.AtTempPath(C4CFN_TempMusic2))) return false;
	// ok
	SongExtracted = true;
	return true;
}

bool C4MusicFile::RemTempFile()
{
	if (!SongExtracted) return true;
	// delete it
	EraseFile(Config.AtTempPath(C4CFN_TempMusic2));
	SongExtracted = false;
	return true;
}

bool C4MusicFile::Init(const char *szFile)
{
	SCopy(szFile, FileName);
	return true;
}

#if AUDIO_TK == AUDIO_TK_SDL_MIXER
C4MusicFileSDL::C4MusicFileSDL():
		Data(nullptr),
		Music(nullptr)
{
}

C4MusicFileSDL::~C4MusicFileSDL()
{
	Stop();
}

bool C4MusicFileSDL::Play(bool loop, double max_resume_time)
{
	const SDL_version * link_version = Mix_Linked_Version();
	if (link_version->major < 1
	    || (link_version->major == 1 && link_version->minor < 2)
	    || (link_version->major == 1 && link_version->minor == 2 && link_version->patch < 7))
	{
		// Check existance and try extracting it
		if (!FileExists(FileName)) if (!ExtractFile())
				// Doesn't exist - or file is corrupt
			{
				LogF("Error reading %s", FileName);
				return false;
			}
		// Load
		Music = Mix_LoadMUS(SongExtracted ? Config.AtTempPath(C4CFN_TempMusic2) : FileName);
		// Load failed
		if (!Music)
		{
			LogF("SDL_mixer: %s", SDL_GetError());
			return false;
		}
		// Play Song
		if (Mix_PlayMusic(Music, loop? -1 : 1) == -1)
		{
			LogF("SDL_mixer: %s", SDL_GetError());
			return false;
		}
	}
	else
	{
		// Load Song
		// Fixme: Try loading this from the group incrementally for less lag
		size_t filesize;
		if (!C4Group_ReadFile(FileName, &Data, &filesize))
		{
			LogF("Error reading %s", FileName);
			return false;
		}
		// Mix_FreeMusic frees the RWop
		Music = Mix_LoadMUS_RW(SDL_RWFromConstMem(Data, filesize), 1);
		if (!Music)
		{
			LogF("SDL_mixer: %s", SDL_GetError());
			return false;
		}
		if (Mix_PlayMusic(Music, loop? -1 : 1) == -1)
		{
			LogF("SDL_mixer: %s", SDL_GetError());
			return false;
		}
	}
	return true;
}

void C4MusicFileSDL::Stop(int fadeout_ms)
{
	if (fadeout_ms && Music)
	{
		// Don't really stop yet
		Mix_FadeOutMusic(fadeout_ms);
		return;
	}
	if (Music)
	{
		Mix_FreeMusic(Music);
		Music = nullptr;
	}
	RemTempFile();
	if (Data)
	{
		delete[] Data;
		Data = nullptr;
	}
}

void C4MusicFileSDL::CheckIfPlaying()
{
	if (!Mix_PlayingMusic())
		Application.MusicSystem.NotifySuccess();
}

void C4MusicFileSDL::SetVolume(int iLevel)
{
	Mix_VolumeMusic((int) ((iLevel * MIX_MAX_VOLUME) / 100));
}

#elif AUDIO_TK == AUDIO_TK_OPENAL

/* Ogg Vobis */

C4MusicFileOgg::C4MusicFileOgg() :
	last_interruption_time()
{
	for (unsigned int & buffer : buffers)
		buffer = 0;
}

C4MusicFileOgg::~C4MusicFileOgg()
{
	Clear();
	Stop();
}

void C4MusicFileOgg::Clear()
{
	// clear ogg file
	if (loaded)
	{
		ov_clear(&ogg_file);
		loaded = false;
	}
	categories.clear();
	is_loading_from_file = false;
	source_file.Close();
	last_source_file_pos = 0;
	last_playback_pos_sec = 0;
	last_interruption_time = C4TimeMilliseconds();
}

bool C4MusicFileOgg::Init(const char *strFile)
{
	// Clear previous
	Clear();
	// Base init file
	if (!C4MusicFile::Init(strFile)) return false;
	// Prepare ogg reader
	vorbis_info* info;
	memset(&ogg_file, 0, sizeof(ogg_file));
	ov_callbacks callbacks;
	// Initial file loading
	// For packed groups, the whole compressed file is kept in memory because reading/seeking inside C4Group is problematic. Uncompress while playing.
	// This increases startup time a bit.
	// Later, this could be replaced with proper random access in c4group. Either replacing the file format or e.g. storing the current zlib state here
	//  and then updating callbacks.read/seek/close/tell_func to read data from the group directly as needed
	bool is_loading_from_file = FileExists(strFile);
	void *data_source;
	if (!is_loading_from_file)
	{
		char *file_contents;
		size_t file_size;
		if (!C4Group_ReadFile(FileName, &file_contents, &file_size))
			return false;
		data.SetOwnedData((BYTE *)file_contents, file_size);
		// C4Group preloaded ogg reader
		callbacks.read_func = &::C4SoundLoaders::VorbisLoader::mem_read_func;
		callbacks.seek_func = &::C4SoundLoaders::VorbisLoader::mem_seek_func;
		callbacks.close_func = &::C4SoundLoaders::VorbisLoader::mem_close_func;
		callbacks.tell_func = &::C4SoundLoaders::VorbisLoader::mem_tell_func;
		data_source = &data;
	}
	else
	{
		// Load directly from file
		if (!source_file.Open(FileName))
			return false;
		// Uncompressed file ogg reader
		callbacks.read_func = &::C4SoundLoaders::VorbisLoader::file_read_func;
		callbacks.seek_func = &::C4SoundLoaders::VorbisLoader::file_seek_func;
		callbacks.close_func = &::C4SoundLoaders::VorbisLoader::file_close_func;
		callbacks.tell_func = &::C4SoundLoaders::VorbisLoader::file_tell_func;
		data_source = this;
	}

	// open using callbacks either to memory or to file loader
	if (ov_open_callbacks(data_source, &ogg_file, nullptr, 0, callbacks) != 0)
	{
		ov_clear(&ogg_file);
		return false;
	}

	// get information about music
	info = ov_info(&ogg_file, -1);
	if (info->channels == 1)
		ogg_info.format = AL_FORMAT_MONO16;
	else
		ogg_info.format = AL_FORMAT_STEREO16;
	ogg_info.sample_rate = info->rate;
	ogg_info.sample_length = ov_time_total(&ogg_file, -1) / 1000.0;
	
	// Get categories from ogg comment header
	vorbis_comment *comment = ov_comment(&ogg_file, -1);
	const char *comment_id = "COMMENT=";
	int comment_id_len = strlen(comment_id);
	for (int i = 0; i < comment->comments; ++i)
	{
		if (comment->comment_lengths[i] > comment_id_len)
		{
			if (SEqual2NoCase(comment->user_comments[i], comment_id, comment_id_len))
			{
				// Add all categories delimeted by ';'
				const char *categories_string = comment->user_comments[i] + comment_id_len;
				for (;;)
				{
					int delimeter = SCharPos(';', categories_string);
					StdCopyStrBuf category;
					category.Copy(categories_string, delimeter >= 0 ? delimeter : SLen(categories_string));
					categories.push_back(category);
					if (delimeter < 0) break;
					categories_string += delimeter+1;
				}
			}
		}
	}

	// File not needed for now
	UnprepareSourceFileReading();

	// mark successfully loaded
	return loaded = true;
}

StdStrBuf C4MusicFileOgg::GetDebugInfo() const
{
	StdStrBuf result;
	result.Append(FileName);
	result.AppendFormat("[%.0lf]", last_playback_pos_sec);
	result.AppendChar('[');
	bool sec = false;
	for (const auto & category : categories)
	{
		if (sec) result.AppendChar(',');
		result.Append(category.getData());
		sec = true;
	}
	result.AppendChar(']');
	return result;
}

void C4MusicFileOgg::UnprepareSourceFileReading()
{
	// The file loader could just keep all files open. But if someone symlinks
	// Music.ocg into their music folder with a million files in it, we would
	// crash with too many open file handles. So close it for now and reopen
	// when that piece is actually requested.
	if (is_loading_from_file && source_file.IsOpen())
	{
		last_source_file_pos = source_file.Tell();
		source_file.Close();
	}
}

bool C4MusicFileOgg::PrepareSourceFileReading()
{
	// mem loading always OK
	if (!is_loading_from_file) return true;
	// ensure file is open
	if (!source_file.IsOpen())
	{
		if (!source_file.Open(FileName)) return false;
		if (last_source_file_pos) if (source_file.Seek(last_source_file_pos, SEEK_SET) < 0) return false;
	}
	return true;
}

bool C4MusicFileOgg::Play(bool loop, double max_resume_time)
{
	// Valid file?
	if (!loaded) return false;
	// stop previous
	if (playing)
	{
		if (max_resume_time > 0.0) return true; // no-op
		Stop();
	}
	// Ensure data reading is ready
	PrepareSourceFileReading();
	// Get channel to use
	alGenSources(1, (ALuint*)&channel);
	if (!channel) return false;

	playing = true;
	streaming_done = false;
	this->loop = loop;
	byte_pos_total = 0;

	// Resume setting
	if (max_resume_time > 0)
	{
		// Only resume if significant amount of data is left to be played
		double time_remaining_sec = GetRemainingTime();
		if (time_remaining_sec < max_resume_time) last_playback_pos_sec = 0.0;
	}
	else
	{
		last_playback_pos_sec = 0;
	}

	// initial volume setting
	SetVolume(float(::Config.Sound.MusicVolume) / 100.0f);

	// prepare read
	ogg_info.sound_data.resize(num_buffers * buffer_size);
	alGenBuffers(num_buffers, buffers);
	ov_time_seek(&ogg_file, last_playback_pos_sec);

	// Fill initial buffers
	for (size_t i=0; i<num_buffers; ++i)
		if (!FillBuffer(i)) break; // if this fails, the piece is shorter than the initial buffers

	// play!
	alErrorCheck(alSourcePlay(channel));
	
	return true;
}

double C4MusicFileOgg::GetRemainingTime()
{
	// Note: Only valid after piece has been stopped
	return ov_time_total(&ogg_file, -1) - last_playback_pos_sec;
}

void C4MusicFileOgg::Stop(int fadeout_ms)
{
	if (playing)
	{
		// remember position for eventual later resume
		ALfloat playback_pos_in_buffer = 0;
		alErrorCheck(alGetSourcef(channel, AL_SEC_OFFSET, &playback_pos_in_buffer));
		last_playback_pos_sec += playback_pos_in_buffer;
		last_interruption_time = C4TimeMilliseconds::Now();
		// stop!
		alSourceStop(channel);
		// clear queue
		ALint num_queued=0;
		alErrorCheck(alGetSourcei(channel, AL_BUFFERS_QUEUED, &num_queued));
		ALuint buffer;
		for (size_t i = 0; i < (size_t)num_queued; ++i)
			alErrorCheck(alSourceUnqueueBuffers(channel, 1, &buffer));
	}
	// clear buffers
	if (channel) 
	{
		alSourcei(channel, AL_BUFFER, 0);
		alDeleteBuffers(num_buffers, buffers);
		alDeleteSources(1, &channel);
	}
	playing = false;
	channel = 0;
	// close file
	UnprepareSourceFileReading();
}

void C4MusicFileOgg::CheckIfPlaying()
{
	Execute();
	if (!playing) Application.MusicSystem.NotifySuccess();
}

void C4MusicFileOgg::SetVolume(int iLevel)
{
	volume = ((float) iLevel) / 100.0f;
	if (channel) alSourcef(channel, AL_GAIN, volume);
}

bool C4MusicFileOgg::HasCategory(const char *szcat) const
{
	if (!szcat) return false;
	// check all stored categories
	for (const auto & category : categories)
		if (WildcardMatch(szcat, category.getData()))
			return true;
	return false;
}

bool C4MusicFileOgg::FillBuffer(size_t idx)
{
	// uncompress from ogg data
	int endian = 0;
	long bytes_read_total = 0, bytes_read;
	char uncompressed_data[buffer_size];
	do {
		bytes_read = ov_read(&ogg_file, uncompressed_data+bytes_read_total, (buffer_size-bytes_read_total)*sizeof(BYTE), endian, 2, 1, &current_section);
		bytes_read_total += bytes_read;
	} while (bytes_read > 0 && bytes_read_total < buffer_size);
	// buffer data
	if (bytes_read_total)
	{
		byte_pos_total += bytes_read_total;
		ALuint buffer = buffers[idx];
		alErrorCheck(alBufferData(buffer, ogg_info.format, uncompressed_data, bytes_read_total, ogg_info.sample_rate));
		// queue buffer
		alErrorCheck(alSourceQueueBuffers(channel, 1, &buffer));
	}
	// streaming done?
	if (bytes_read_total < buffer_size)
	{
		// streaming done. loop or done.
		if (loop)
		{
			// reset pos in ogg file
			ov_raw_seek(&ogg_file, 0);
			// if looping and nothing has been committed to this buffer yet, try again
			// except if byte_pos_total==0, i.e. if the piece is completely empty
			size_t prev_bytes_total = byte_pos_total;
			byte_pos_total = 0;
			if (!bytes_read_total && prev_bytes_total) return FillBuffer(idx);
			return true;
		}
		else
		{
			// non-looping: we're done.
			return false;
		}
	}
	else
	{
		// might have more data to stream
		return true;
	}
}

void C4MusicFileOgg::Execute()
{
	if (playing)
	{
		// get processed buffer count
		ALint num_processed = 0;
		alErrorCheck(alGetSourcei(channel, AL_BUFFERS_PROCESSED, &num_processed));
		bool done = false;
		while (num_processed--)
		{
			// release processed buffer
			ALuint buffer; 
			alErrorCheck(alSourceUnqueueBuffers(channel, 1, &buffer));
			// add playback time of processed buffer to total playback time
			ALint buf_bits = 16, buf_chans = 2, buf_freq = 44100;
			alErrorCheck(alGetBufferi(buffer, AL_BITS, &buf_bits));
			alErrorCheck(alGetBufferi(buffer, AL_CHANNELS, &buf_chans));
			alErrorCheck(alGetBufferi(buffer, AL_FREQUENCY, &buf_freq));
			double buffer_secs = double(buffer_size) / buf_bits / buf_chans / buf_freq * 8;
			last_playback_pos_sec += buffer_secs;
			// refill processed buffer
			size_t buffer_idx;
			for (buffer_idx=0; buffer_idx<num_buffers; ++buffer_idx)
				if (buffers[buffer_idx] == buffer) break;
			if (!done) done = !FillBuffer(buffer_idx);
		}
		if (done) streaming_done = true;
		// check if done
		ALint state = 0;
		alErrorCheck(alGetSourcei(channel, AL_SOURCE_STATE, &state));
		if (state != AL_PLAYING && streaming_done)
		{
			Stop();
			// reset playback to beginning for next time this piece is playing
			last_playback_pos_sec = 0.0;
		}
		else if (state == AL_STOPPED)
		{
			alErrorCheck(alSourcePlay(channel));
		}
	}
}

#endif
