/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2022 Olive Team
  Modifications Copyright (C) 2025 mikesolar

  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 "serializer.h"

#include <QApplication>
#include <QFile>
#include <QXmlStreamReader>

#include "common/xmlutils.h"
#include "core.h"
#include "node/group/group.h"
#include "serializer190219.h"
#include "serializer210528.h"
#include "serializer210907.h"
#include "serializer211228.h"
#include "serializer220403.h"
#include "serializer230220.h"

namespace olive
{

QVector<ProjectSerializer *> ProjectSerializer::instances_;

void ProjectSerializer::Initialize()
{
	// Make sure to order these from oldest to newest

	// FIXME: Implement this - yes it's a 0.1 project loader
	//instances_.append(new ProjectSerializer190219);

	instances_.append(new ProjectSerializer210528);
	instances_.append(new ProjectSerializer210907);
	instances_.append(new ProjectSerializer211228);
	instances_.append(new ProjectSerializer220403);
	instances_.append(new ProjectSerializer230220);
}

void ProjectSerializer::Destroy()
{
	qDeleteAll(instances_);
	instances_.clear();
}

ProjectSerializer::Result ProjectSerializer::Load(Project *project,
												  const QString &filename,
												  LoadType load_type)
{
	QFile project_file(filename);

	if (project_file.open(QFile::ReadOnly)) {
		// Some project files are compressed, marked with "OVEC" at the beginning of the file. Check for
		// that signature now.
		std::unique_ptr<QXmlStreamReader> reader;
		if (CheckCompressedID(&project_file)) {
			// File is compressed, decompress into memory
			QByteArray b;
			b = qUncompress(project_file.readAll());
			reader.reset(new QXmlStreamReader(b));
		} else {
			project_file.seek(0);
			reader.reset(new QXmlStreamReader(&project_file));
		}

		Result inner_result = Load(project, reader.get(), load_type);

		project_file.close();

		if (inner_result.code() != kSuccess) {
			return inner_result;
		}

		if (reader->hasError()) {
			Result r(kXmlError);
			r.SetDetails(reader->errorString());
			return r;
		} else {
			return inner_result;
		}
	} else {
		Result r(kFileError);
		r.SetDetails(QStringLiteral("Unable to open '%1': %2")
					 .arg(filename, project_file.errorString()));
		return r;
	}
}

ProjectSerializer::Result ProjectSerializer::Load(Project *project,
												  QXmlStreamReader *reader,
												  LoadType load_type)
{
	// Determine project version
	uint version = 0;
	Result res = kUnknownVersion;

	while (XMLReadNextStartElement(reader)) {
		if (reader->name() == QStringLiteral("olive") ||
			reader->name() == QStringLiteral("project")) { // 0.1 projects only

			XMLAttributeLoop(reader, attr)
			{
				if (attr.name() ==
					QStringLiteral("version")) { // 230220+ projects
					version = attr.value().toUInt();
				} else if (reader->name() ==
						   QStringLiteral("url")) { // 230220+ projects
					project->SetSavedURL(attr.value().toString());
				}
			}

			while (XMLReadNextStartElement(reader)) {
				if (reader->name() ==
					QStringLiteral("version")) { // projects <= 220403
					version = reader->readElementText().toUInt();
				} else if (reader->name() ==
						   QStringLiteral("url")) { // projects <= 220403
					if (project) {
						project->SetSavedURL(reader->readElementText());
					} else {
						reader->skipCurrentElement();
					}
				} else {
					// Handle any other value with the serializer
					res = LoadWithSerializerVersion(version, project, reader,
													load_type);
				}
			}
		} else {
			reader->skipCurrentElement();
		}
	}

	return res;
}

ProjectSerializer::Result ProjectSerializer::Paste(LoadType load_type,
												   Project *project)
{
	QString clipboard = Core::PasteStringFromClipboard();
	if (clipboard.isEmpty()) {
		return kNoData;
	}

	QXmlStreamReader reader(clipboard);

	return ProjectSerializer::Load(project, &reader, load_type);
}

ProjectSerializer::Result ProjectSerializer::Save(const SaveData &data,
												  bool compress)
{
	QString temp_save =
		FileFunctions::GetSafeTemporaryFilename(data.GetFilename());

	QFile project_file(temp_save);

	if (project_file.open(QFile::WriteOnly)) {
		QByteArray b;
		QXmlStreamWriter writer(&b);

		Result inner_result = Save(&writer, data);

		if (writer.hasError()) {
			Result r(kXmlError);
			return r;
		}

		if (compress) {
			project_file.write("OVEC");
			project_file.write(qCompress(b));
		} else {
			project_file.write(b);
		}

		project_file.close();

		if (inner_result != kSuccess) {
			return inner_result;
		}

		// Save was successful, we can now rewrite the original file
		if (FileFunctions::RenameFileAllowOverwrite(temp_save,
													data.GetFilename())) {
			return kSuccess;
		} else {
			Result r(kOverwriteError);
			r.SetDetails(temp_save);
			return r;
		}
	} else {
		Result r(kFileError);
		r.SetDetails(temp_save);
		return r;
	}
}

ProjectSerializer::Result ProjectSerializer::Save(QXmlStreamWriter *writer,
												  const SaveData &data)
{
	writer->setAutoFormatting(true);

	writer->writeStartDocument();

	writer->writeStartElement("olive");

	// By default, save as last serializer which, assuming the instances are ordered correctly,
	// will be the newest file format. But we may allow saving as older versions later on.
	ProjectSerializer *serializer = instances_.last();

	// Version is stored in YYMMDD from whenever the project format was last changed
	// Allows easy integer math for checking project versions.
	writer->writeAttribute(QStringLiteral("version"),
						   QString::number(serializer->Version()));

	if (!data.GetFilename().isEmpty()) {
		writer->writeAttribute("url", data.GetFilename());
	}

	serializer->Save(writer, data, nullptr);

	writer->writeEndElement(); // olive

	writer->writeEndDocument();

	if (writer->hasError()) {
		return kXmlError;
	}

	return kSuccess;
}

ProjectSerializer::Result ProjectSerializer::Copy(const SaveData &data)
{
	QString copy_str;
	QXmlStreamWriter writer(&copy_str);

	ProjectSerializer::Result res = ProjectSerializer::Save(&writer, data);

	if (res == kSuccess) {
		Core::CopyStringToClipboard(copy_str);
	}

	return res;
}

bool ProjectSerializer::CheckCompressedID(QFile *file)
{
	QByteArray b = file->read(4);
	return !memcmp(b.data(), "OVEC", 4);
}

bool ProjectSerializer::IsCancelled() const
{
	return false;
}

ProjectSerializer::Result
ProjectSerializer::LoadWithSerializerVersion(uint version, Project *project,
											 QXmlStreamReader *reader,
											 LoadType load_type)
{
	// Failed to find version in file
	if (version == 0) {
		return kUnknownVersion;
	}

	// We should now have the version, if we have a serializer for it, use it to load the project
	ProjectSerializer *serializer = nullptr;

	foreach (ProjectSerializer *s, instances_) {
		if (version == s->Version()) {
			serializer = s;
			break;
		} else if (version < s->Version()) {
			// Assuming the instance list is in order, if the project version is less than any version
			// we find, we must not support it anymore
			return kProjectTooOld;
		}
	}

	if (serializer) {
		LoadData ld = serializer->Load(project, reader, load_type, nullptr);
		Result r(kSuccess);
		if (reader->hasError()) {
			r = Result(kXmlError);
			r.SetDetails(
				QCoreApplication::translate("Serializer", "%1 on line %2")
					.arg(reader->errorString(),
						 QString::number(reader->lineNumber())));
		}
		r.SetLoadData(ld);
		return r;
	} else {
		// Reached the end of the list with no serializer, assume too new
		return kProjectTooNew;
	}
}

void ProjectSerializer::SaveData::SetOnlySerializeNodesAndResolveGroups(
	QVector<Node *> nodes)
{
	// For any groups, add children
	for (int i = 0; i < nodes.size(); i++) {
		// If this is a group, add the child nodes too
		if (NodeGroup *g = dynamic_cast<NodeGroup *>(nodes.at(i))) {
			for (auto it = g->GetContextPositions().cbegin();
				 it != g->GetContextPositions().cend(); it++) {
				if (!nodes.contains(it.key())) {
					nodes.append(it.key());
				}
			}
		}
	}

	SetOnlySerializeNodes(nodes);
}

}
