//
//

#include "FFmpegWaveFile.h"

#include "FFmpegAudioReader.h"

#include "sound/ds.h"

// -- from parselo.cpp --
extern const char* stristr(const char* str, const char* substr);

namespace {
using namespace sound;
using namespace sound::ffmpeg;

AudioProperties getAudioProps(AVStream* stream)
{
	AudioProperties props;

	int channels;
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
	channels             = stream->codecpar->ch_layout.nb_channels;
	props.channel_layout = stream->codecpar->ch_layout.u.mask;
	props.sample_rate    = stream->codecpar->sample_rate;
	props.format         = (AVSampleFormat)stream->codecpar->format;
#elif LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
	channels             = stream->codecpar->channels;
	props.channel_layout = stream->codecpar->channel_layout;
	props.sample_rate    = stream->codecpar->sample_rate;
	props.format         = (AVSampleFormat)stream->codecpar->format;
#else
	channels             = stream->codec->channels;
	props.channel_layout = stream->codec->channel_layout;
	props.sample_rate    = stream->codec->sample_rate;
	props.format         = stream->codec->sample_fmt;
#endif

	if (props.channel_layout == 0) {
		// Use a default channel layout value
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
		AVChannelLayout ch_layout;

		av_channel_layout_default(&ch_layout, channels);
		props.channel_layout = ch_layout.u.mask;
#else
		props.channel_layout = av_get_default_channel_layout(channels);
#endif
	}

	return props;
}

AudioProperties getAdjustedAudioProps(const AudioProperties& baseProps)
{
	AudioProperties adjusted;
	adjusted.sample_rate = baseProps.sample_rate; // Don't adjust sample rate

	int channels = 2;
	adjusted.channel_layout = baseProps.channel_layout;

#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
	AVChannelLayout ch_layout;

	av_channel_layout_from_mask(&ch_layout, baseProps.channel_layout);

	channels = ch_layout.nb_channels;
#else
	channels = av_get_channel_layout_nb_channels(baseProps.channel_layout);
#endif

	if (channels > 2) {
		adjusted.channel_layout = AV_CH_LAYOUT_STEREO;
	}

	int max_bytes_per_sample;
	switch (Ds_sound_quality) {
	case DS_SQ_HIGH:
		max_bytes_per_sample = Ds_float_supported ? 4 : 2;
		break;

	case DS_SQ_MEDIUM:
		max_bytes_per_sample = 2;
		break;

	default:
		max_bytes_per_sample = 1;
		break;
	}

	// Determine the right format. Downsample if sound quality is low but don't increase size of samples if the source
	// doesn't support it
	auto bytes_per_sample = std::min(av_get_bytes_per_sample(baseProps.format), max_bytes_per_sample);

	switch (bytes_per_sample) {
	case 1:
		adjusted.format = AV_SAMPLE_FMT_U8;
		break;
	case 2:
		adjusted.format = AV_SAMPLE_FMT_S16;
		break;
	case 4:
		adjusted.format = AV_SAMPLE_FMT_FLT;
		break;
	default:
		UNREACHABLE("Unhandled switch value!");
		adjusted.format = AV_SAMPLE_FMT_NONE;
		break;
	}

	return adjusted;
}

SwrContext* getSWRContext(const AudioProperties& base, const AudioProperties& adjusted)
{
	SwrContext* swr = nullptr;
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
	AVChannelLayout base_layout, adjusted_layout;

	av_channel_layout_from_mask(&base_layout, base.channel_layout);
	av_channel_layout_from_mask(&adjusted_layout, adjusted.channel_layout);

	swr_alloc_set_opts2(&swr, &adjusted_layout, adjusted.format, adjusted.sample_rate, &base_layout,
							 base.format, base.sample_rate, 0, nullptr);
#else
	swr = swr_alloc_set_opts(swr, adjusted.channel_layout, adjusted.format, adjusted.sample_rate, base.channel_layout,
							 base.format, base.sample_rate, 0, nullptr);
#endif

	if (swr_init(swr) < 0) {
		return nullptr;
	}

	return swr;
}
} // namespace

namespace sound {
namespace ffmpeg {
FFmpegWaveFile::FFmpegWaveFile()
{
	// Init data members
	m_audioProps = AudioProperties();
	m_ctx.reset();

	m_decodeFrame = av_frame_alloc();
}

FFmpegWaveFile::~FFmpegWaveFile()
{
	av_frame_free(&m_decodeFrame);

	if (m_audioCodecCtx) {
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
		avcodec_free_context(&m_audioCodecCtx);
#else
		avcodec_close(m_audioCodecCtx);
#endif
		m_audioCodecCtx = nullptr;
	}

	if (m_resampleCtx) {
		swr_free(&m_resampleCtx);
	}

	// Free memory
	m_ctx.reset();
}

const AVCodec* sound::ffmpeg::FFmpegWaveFile::prepareOpened()
{
    using namespace libs::ffmpeg;
    auto ctx = m_ctx->ctx();

    const AVCodec* audio_codec = nullptr;

    m_audioStreamIndex   = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
    if (m_audioStreamIndex < 0) {
        throw FFmpegException("Failed to find audio stream in file.");
    }
    m_audioStream = ctx->streams[m_audioStreamIndex];

#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
	audio_codec = avcodec_find_decoder(m_audioStream->codecpar->codec_id);
#else
	audio_codec = avcodec_find_decoder(m_audioStream->codec->codec_id);
#endif

	if ( !audio_codec ) {
		throw FFmpegException("Failed to find decoder for audio stream in file.");
	}

    int err;
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
    m_audioCodecCtx = avcodec_alloc_context3(audio_codec);

    // Copy codec parameters from input stream to output codec context
    err = avcodec_parameters_to_context(m_audioCodecCtx, m_audioStream->codecpar);
    if (err < 0) {
        char errorStr[512];
        av_strerror(err, errorStr, sizeof(errorStr));
        throw FFmpegException(errorStr);
    }
#else
    m_audioCodecCtx = m_audioStream->codec;
#endif

    err = avcodec_open2(m_audioCodecCtx, audio_codec, nullptr);
    if (err < 0) {
        char errorStr[512];
        av_strerror(err, errorStr, sizeof(errorStr));
        throw FFmpegException(errorStr);
    }

    m_baseAudioProps = getAudioProps(m_audioStream);

    setAdjustedAudioProperties(getAdjustedAudioProps(m_baseAudioProps));

    return audio_codec;
}

// Open
bool FFmpegWaveFile::Open(const char* pszFilename, bool keep_ext)
{
	using namespace libs::ffmpeg;

	char filename[MAX_FILENAME_LEN];

	// NOTE: the extension will be removed and set to the actual extension of keep_ext is false, otherwise it's not
	// changed
	strcpy_s(filename, pszFilename);

	try {
		CFILE* cfp = nullptr;
		// if we are supposed to load the file as passed...
		if (keep_ext) {
			auto known_format = false;
			for (int i = 0; i < NUM_AUDIO_EXT; i++) {
				if (stristr(pszFilename, audio_ext_list[i])) {
					known_format = true;
					break;
				}
			}

			// not a supported extension format ... somebody screwed up their tbls :)
			if (!known_format) {
				throw FFmpegException("Unknown file extension.");
			}

			auto res = cf_find_file_location(pszFilename, CF_TYPE_ANY);
			if (!res.found) {
#ifndef NDEBUG
				// see if the file exists with a different extension
				res = cf_find_file_location_ext(filename, NUM_AUDIO_EXT, audio_ext_list, CF_TYPE_ANY);
				if (res.found) {
					Warning(LOCATION, "File %s was not found with its specified extension, but another file %s exists in the modpack.  Please update the extension and adjust audio specifications if necessary.", filename, res.name_ext.c_str());
				}
#endif

				throw FFmpegException("File not found.");
			}

			cfp = cfopen_special(res, "rb", CF_TYPE_ANY);
		} else {
			// ... otherwise we just find the best match
			auto res = cf_find_file_location_ext(filename, NUM_AUDIO_EXT, audio_ext_list, CF_TYPE_ANY);

			if (!res.found) {
				throw FFmpegException("File not found with any known extension.");
			}

			// set proper filename for later use
			strcpy_s(filename, res.name_ext.c_str());

			cfp = cfopen_special(res, "rb", CF_TYPE_ANY);
		}

		if (cfp == NULL) {
			throw FFmpegException("Failed to open file.");
		}

		m_ctx    = FFmpegContext::createContext(cfp);

        const AVCodec* audio_codec = prepareOpened();

		nprintf(("SOUND", "SOUND => %s => Using codec %s (%s)\n", filename, audio_codec->long_name, audio_codec->name));
	} catch (const FFmpegException& e) {
		mprintf(("SOUND ==> Could not open wave file %s for streaming. Reason: %s\n", filename, e.what()));
		return false;
	}

	// Cue for streaming
	Cue();
	m_frameReader.reset(new FFmpegAudioReader(m_ctx->ctx(), m_audioCodecCtx, m_audioStreamIndex));

	nprintf(("SOUND", "SOUND => Successfully opened: %s\n", filename));

	// If we are here it means that everything went fine
	return true;
}

// OpenMem
bool FFmpegWaveFile::OpenMem(const uint8_t* snddata, size_t snd_len)
{
    using namespace libs::ffmpeg;

	try {
		m_ctx    = FFmpegContext::createContextMem(snddata, snd_len);

        const AVCodec* audio_codec = prepareOpened();

		nprintf(("SOUND", "SOUND => %s => Using codec %s (%s)\n", "soundfile-in-memory", audio_codec->long_name, audio_codec->name));
	} catch (const FFmpegException& e) {
		mprintf(("SOUND ==> Could not open wave file %s for streaming. Reason: %s\n", "soundfile-in-memory", e.what()));
		return false;
	}

	// Cue for streaming
	Cue();
	m_frameReader.reset(new FFmpegAudioReader(m_ctx->ctx(), m_audioCodecCtx, m_audioStreamIndex));

	nprintf(("SOUND", "SOUND => Successfully opened: %s\n", "soundfile-in-memory"));

	// If we are here it means that everything went fine
	return true;
}

bool FFmpegWaveFile::Cue()
{
	auto err = av_seek_frame(m_ctx->ctx(), m_audioStreamIndex, 0, AVSEEK_FLAG_BYTE);

	if (err >= 0) {
		avcodec_flush_buffers(m_audioCodecCtx);
		avformat_flush(m_ctx->ctx());
	}

	return err >= 0;
}

void FFmpegWaveFile::setAdjustedAudioProperties(const AudioProperties& props)
{
	if (m_resampleCtx) {
		swr_free(&m_resampleCtx);
	}
	m_audioProps  = props;
	m_resampleCtx = getSWRContext(m_baseAudioProps, m_audioProps);

	Assertion(m_resampleCtx != nullptr, "Resample context creation failed! This should not happen!");
}

size_t FFmpegWaveFile::handleDecodedFrame(AVFrame* av_frame, uint8_t* out_buffer, size_t buffer_size)
{
	const auto sample_size = (av_get_bytes_per_sample(m_audioProps.format) * getNumChannels());

	int dest_num_samples = static_cast<int>(buffer_size / sample_size);
	auto written = swr_convert(m_resampleCtx, &out_buffer, dest_num_samples, (const uint8_t**)av_frame->extended_data,
							   av_frame->nb_samples);

	return (size_t)(written * sample_size);
}

size_t FFmpegWaveFile::getBufferedData(uint8_t* buffer, size_t buffer_size)
{
	// First determine if there is buffered data
	auto buffered = swr_get_out_samples(m_resampleCtx, 0);

	if (buffered > 0) {
		const auto sample_size = (av_get_bytes_per_sample(m_audioProps.format) * getNumChannels());
		int dest_num_samples   = static_cast<int>(buffer_size / sample_size);
		auto written           = swr_convert(m_resampleCtx, &buffer, dest_num_samples, nullptr, 0);

		auto advance = (size_t)(written * sample_size);
		Assertion(advance <= buffer_size,
				  "Buffer overrun!!! Decoding has written more data into the buffer than available!");

		return advance;
	}
	return 0;
}

int FFmpegWaveFile::Read(uint8_t* pbDest, size_t cbSize)
{
	size_t buffer_pos = 0;
	buffer_pos += getBufferedData(pbDest, cbSize);
	if (buffer_pos == cbSize) {
		return static_cast<int>(buffer_pos);
	}

	while (m_frameReader->readFrame(m_decodeFrame)) {
		// Got a new frame
		auto advance = handleDecodedFrame(m_decodeFrame, pbDest + buffer_pos, cbSize - buffer_pos);

		buffer_pos += advance;
		Assertion(buffer_pos <= cbSize,
				  "Buffer overrun!!! Decoding has written more data into the buffer than available!");

		if (buffer_pos == cbSize) {
			return static_cast<int>(buffer_pos);
		}
	}

	// If we are here then the audio stream is finished, write any buffered data
	buffer_pos += getBufferedData(pbDest + buffer_pos, cbSize - buffer_pos);

	if (buffer_pos == 0) {
		// End of stream has been reached
		return -1;
	}

	return static_cast<int>(buffer_pos);
}

int FFmpegWaveFile::getTotalSamples() const
{
	auto duration  = av_mul_q(av_make_q(static_cast<int>(m_audioStream->duration), 1), m_audioStream->time_base);
	auto samples_r = av_mul_q(av_make_q(m_audioProps.sample_rate, 1), duration);

	// Compute the actual sample number and round the result up
	auto samples = 1 + ((samples_r.num - 1) / samples_r.den);

	return samples;
}

int FFmpegWaveFile::getNumChannels() const
{
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
	AVChannelLayout ch_layout;

	av_channel_layout_from_mask(&ch_layout, m_audioProps.channel_layout);

	return ch_layout.nb_channels;
#else
	return av_get_channel_layout_nb_channels(m_audioProps.channel_layout);
#endif
}

AudioFileProperties FFmpegWaveFile::getFileProperties()
{
	AudioFileProperties props{};
	props.num_channels = getNumChannels();
	props.duration =
		av_q2d(av_mul_q(av_make_q(static_cast<int>(m_audioStream->duration), 1), m_audioStream->time_base));
	props.total_samples    = getTotalSamples();
	props.sample_rate      = m_audioProps.sample_rate;
	props.bytes_per_sample = av_get_bytes_per_sample(m_audioProps.format);
	return props;
}
void FFmpegWaveFile::setResamplingProperties(const ResampleProperties& resampleProps)
{
	using namespace libs::ffmpeg;

	auto current = m_audioProps;

	if (getNumChannels() == resampleProps.num_channels) {
		// No need to resample
		return;
	}

	if (resampleProps.num_channels == 1) {
		current.channel_layout = AV_CH_LAYOUT_MONO;

		setAdjustedAudioProperties(current);
	} else {
		throw FFmpegException("Number of channels to resample to is not supported!");
	}
}

} // namespace ffmpeg
} // namespace sound
