/*
 * Oak Video Editor - Non-Linear Video Editor
 * Copyright (C) 2025 Olive CE Team
 *
 * 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 "decoder.h"

#include <QCoreApplication>
#include <QDebug>
#include <QHash>

#include "codec/ffmpeg/ffmpegdecoder.h"
#include "codec/planarfiledevice.h"
#include "codec/oiio/oiiodecoder.h"
#include "conformmanager.h"

namespace olive
{

const rational Decoder::kAnyTimecode = RATIONAL_MIN;

Decoder::Decoder()
	: cached_texture_(nullptr)
{
	UpdateLastAccessed();
}

void Decoder::IncrementAccessTime(qint64 t)
{
	last_accessed_ += t;
}

bool Decoder::Open(const CodecStream &stream)
{
	QMutexLocker locker(&mutex_);

	UpdateLastAccessed();

	if (stream_.IsValid()) {
		// Decoder is already open. Return TRUE if the stream is the stream we have, or FALSE if not.
		if (stream_ == stream) {
			return true;
		} else {
			qWarning()
				<< "Tried to open a decoder that was already open with another stream";
			return false;
		}
	} else {
		// Stream was not open, try opening it now
		if (!stream.IsValid()) {
			// Cannot open null stream
			qCritical() << "Decoder attempted to open null stream";
			return false;
		}

		if (!stream.Exists()) {
			// Cannot open file that doesn't exist
			qCritical() << "Decoder attempted to open file that doesn't exist";
			return false;
		}

		// Set stream
		stream_ = stream;

		// Try open internal
		if (OpenInternal()) {
			return true;
		} else {
			// Unset stream
			qCritical() << "Failed to open" << stream_.filename() << "stream"
						<< stream_.stream();
			CloseInternal();
			stream_.Reset();
			return false;
		}
	}
}

TexturePtr Decoder::RetrieveVideo(const RetrieveVideoParams &p)
{
	QMutexLocker locker(&mutex_);

	UpdateLastAccessed();

	if (!stream_.IsValid()) {
		qCritical() << "Can't retrieve video on a closed decoder";
		return nullptr;
	}

	if (!SupportsVideo()) {
		qCritical() << "Decoder doesn't support video";
		return nullptr;
	}

	if (p.cancelled && p.cancelled->IsCancelled()) {
		return nullptr;
	}

	if (cached_texture_ && cached_time_ == p.time &&
		cached_divider_ == p.divider) {
		return cached_texture_;
	}

	cached_texture_ = RetrieveVideoInternal(p);
	cached_time_ = p.time;
	cached_divider_ = p.divider;

	return cached_texture_;
}

Decoder::RetrieveAudioStatus
Decoder::RetrieveAudio(SampleBuffer &dest, const TimeRange &range,
					   const AudioParams &params, const QString &cache_path,
					   LoopMode loop_mode, RenderMode::Mode mode)
{
	QMutexLocker locker(&mutex_);

	UpdateLastAccessed();

	if (!stream_.IsValid()) {
		qCritical() << "Can't retrieve audio on a closed decoder";
		return kInvalid;
	}

	if (!SupportsAudio()) {
		qCritical() << "Decoder doesn't support audio";
		return kInvalid;
	}

	// Get conform state from ConformManager
	ConformManager::Conform conform =
		ConformManager::instance()->GetConformState(
			id(), cache_path, stream_, params, (mode == RenderMode::kOnline));
	if (conform.state == ConformManager::kConformGenerating) {
		// If we need the task, it's available in `conform.task`
		return kWaitingForConform;
	}

	// See if we got the conform
	if (RetrieveAudioFromConform(dest, conform.filenames, range, loop_mode,
								 params)) {
		return kOK;
	} else {
		return kUnknownError;
	}
}

qint64 Decoder::GetLastAccessedTime()
{
	return last_accessed_;
}

void Decoder::Close()
{
	QMutexLocker locker(&mutex_);

	UpdateLastAccessed();

	cached_texture_ = nullptr;

	if (stream_.IsValid()) {
		CloseInternal();
		stream_.Reset();
	} else {
		qWarning() << "Tried to close a decoder that wasn't open";
	}
}

bool Decoder::ConformAudio(const QVector<QString> &output_filenames,
						   const AudioParams &params, CancelAtom *cancelled)
{
	return ConformAudioInternal(output_filenames, params, cancelled);
}

/*
 * DECODER STATIC PUBLIC MEMBERS
 */

QVector<DecoderPtr> Decoder::ReceiveListOfAllDecoders()
{
	QVector<DecoderPtr> decoders;

	// The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last,
	// since it supports so many formats and we presumably want to override those formats with a more specific decoder.
	decoders.append(std::make_shared<OIIODecoder>());
	decoders.append(std::make_shared<FFmpegDecoder>());

	return decoders;
}

DecoderPtr Decoder::CreateFromID(const QString &id)
{
	if (id.isEmpty()) {
		return nullptr;
	}

	// Create list to iterate through
	QVector<DecoderPtr> decoder_list = ReceiveListOfAllDecoders();

	foreach (DecoderPtr d, decoder_list) {
		if (d->id() == id) {
			return d;
		}
	}

	return nullptr;
}

void Decoder::SignalProcessingProgress(int64_t ts, int64_t duration)
{
	if (duration != AV_NOPTS_VALUE && duration != 0) {
		emit IndexProgress(static_cast<double>(ts) /
						   static_cast<double>(duration));
	}
}

QString Decoder::TransformImageSequenceFileName(const QString &filename,
												const int64_t &number)
{
	int digit_count = GetImageSequenceDigitCount(filename);

	QFileInfo file_info(filename);

	QString original_basename = file_info.completeBaseName();

	QString new_basename =
		original_basename.left(original_basename.size() - digit_count)
			.append(
				QStringLiteral("%1").arg(number, digit_count, 10, QChar('0')));

	return file_info.dir().filePath(
		file_info.fileName().replace(original_basename, new_basename));
}

int Decoder::GetImageSequenceDigitCount(const QString &filename)
{
	QString basename = QFileInfo(filename).completeBaseName();

	// See if basename contains a number at the end
	int digit_count = 0;

	for (int i = basename.size() - 1; i >= 0; i--) {
		if (basename.at(i).isDigit()) {
			digit_count++;
		} else {
			break;
		}
	}

	return digit_count;
}

int64_t Decoder::GetImageSequenceIndex(const QString &filename)
{
	int digit_count = GetImageSequenceDigitCount(filename);

	QFileInfo file_info(filename);

	QString original_basename = file_info.completeBaseName();

	QString number_only =
		original_basename.mid(original_basename.size() - digit_count);

	return number_only.toLongLong();
}

TexturePtr Decoder::RetrieveVideoInternal(const RetrieveVideoParams &p)
{
	Q_UNUSED(p)
	return nullptr;
}

bool Decoder::ConformAudioInternal(const QVector<QString> &filenames,
								   const AudioParams &params,
								   CancelAtom *cancelled)
{
	Q_UNUSED(filenames)
	Q_UNUSED(cancelled)
	Q_UNUSED(params)
	return false;
}

bool Decoder::RetrieveAudioFromConform(
	SampleBuffer &sample_buffer, const QVector<QString> &conform_filenames,
	TimeRange range, LoopMode loop_mode, const AudioParams &input_params)
{
	PlanarFileDevice input;
	if (input.open(conform_filenames, QFile::ReadOnly)) {
		// Offset range by audio start offset
		range -= GetAudioStartOffset();

		qint64 read_index = input_params.time_to_bytes(range.in()) /
							input_params.channel_count();
		qint64 write_index = 0;

		const qint64 buffer_length_in_bytes =
			sample_buffer.sample_count() *
			input_params.bytes_per_sample_per_channel();

		while (write_index < buffer_length_in_bytes) {
			if (loop_mode == LoopMode::kLoopModeLoop) {
				while (read_index >= input.size()) {
					read_index -= input.size();
				}

				while (read_index < 0) {
					read_index += input.size();
				}
			}

			qint64 write_count = 0;

			if (read_index < 0) {
				// Reading before 0, write silence here until audio data would actually start
				write_count = qMin(-read_index, buffer_length_in_bytes);
				sample_buffer.silence_bytes(write_index,
											write_index + write_count);
			} else if (read_index >= input.size()) {
				// Reading after data length, write silence until the end of the buffer
				write_count = buffer_length_in_bytes - write_index;
				sample_buffer.silence_bytes(write_index,
											write_index + write_count);
			} else {
				write_count = qMin(input.size() - read_index,
								   buffer_length_in_bytes - write_index);
				input.seek(read_index);
				input.read(reinterpret_cast<char **>(
							   sample_buffer.to_raw_ptrs().data()),
						   write_count, write_index);
			}

			read_index += write_count;
			write_index += write_count;
		}

		input.close();

		return true;
	}

	return false;
}

void Decoder::UpdateLastAccessed()
{
	last_accessed_ = QDateTime::currentMSecsSinceEpoch();
}

uint qHash(Decoder::CodecStream stream, uint seed)
{
	return qHash(stream.filename(), seed) ^ ::qHash(stream.stream(), seed) ^
		   qHash(stream.block(), seed);
}

}
