/*
 * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#pragma once

#include <ox/claw/read.hpp>
#include <ox/claw/write.hpp>
#include <ox/event/signal.hpp>
#include <ox/fs/fs.hpp>
#include <ox/mc/mc.hpp>
#include <ox/model/descwrite.hpp>
#include <ox/std/hashmap.hpp>

#include <nostalgia/core/typestore.hpp>

#include "nostalgiastudio_export.h"

namespace nostalgia::studio {

enum class ProjectEvent {
	None,
	FileAdded,
	// FileRecognized is triggered for all matching files upon a new
	// subscription to a section of the project and upon the addition of a file.
	FileRecognized,
	FileDeleted,
	FileUpdated,
};

[[nodiscard]]
constexpr ox::Result<ox::String> fileExt(const ox::String &path) noexcept {
	const auto extStart = ox::find(path.crbegin(), path.crend(), '.').offset();
	if (!extStart) {
		return OxError(1, "Cannot open a file without valid extension.");
	}
	return path.substr(extStart + 1);
}

class NOSTALGIASTUDIO_EXPORT Project {
	private:
		ox::String m_path;
		mutable core::TypeStore m_typeStore;
		mutable ox::FileSystem *m_fs = nullptr;
		ox::HashMap<ox::String, ox::Vector<ox::String>> m_fileExtFileMap;

	public:
		explicit Project(ox::FileSystem *fs, ox::String path) noexcept;

		ox::Error create() noexcept;

		[[nodiscard]]
		ox::FileSystem *romFs() noexcept;

		ox::Error mkdir(const ox::String &path) const noexcept;

		/**
		 * Writes a MetalClaw object to the project at the given path.
		 */
		ox::Error writeObj(const ox::String &path, auto *obj, ox::ClawFormat fmt = ox::ClawFormat::Metal) noexcept;

		template<typename T>
		ox::Result<ox::UniquePtr<T>> loadObj(const ox::String &path) const noexcept;

		ox::Result<ox::FileStat> stat(const ox::String &path) const noexcept;

		[[nodiscard]]
		bool exists(const ox::String& path) const noexcept;

		template<typename Functor>
		ox::Error subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept;

		[[nodiscard]]
		const ox::Vector<ox::String> &fileList(const char *ng) noexcept;

	private:
		void buildFileIndex() noexcept;

		void indexFile(const ox::String &path) noexcept;

		ox::Error writeBuff(const ox::String &path, const ox::Buffer &buff) noexcept;

		ox::Result<ox::Buffer> loadBuff(const ox::String &path) const noexcept;

		ox::Error lsProcDir(ox::Vector<ox::String> *paths, const ox::String &path) const noexcept;

		ox::Result<ox::Vector<ox::String>> listFiles(const ox::String &path = "") const noexcept;

	// signals
	public:
		ox::Signal<ox::Error(ProjectEvent, const ox::String&)> fileEvent;
		ox::Signal<ox::Error(const ox::String&)> fileAdded;
		// FileRecognized is triggered for all matching files upon a new
		// subscription to a section of the project and upon the addition of a
		// file.
		ox::Signal<ox::Error(const ox::String&)> fileRecognized;
		ox::Signal<ox::Error(const ox::String&)> fileDeleted;
		ox::Signal<ox::Error(const ox::String&)> fileUpdated;

};

ox::Error Project::writeObj(const ox::String &path, auto *obj, ox::ClawFormat fmt) noexcept {
	// write MetalClaw
	oxRequireM(buff, ox::writeClaw(obj, fmt));
	// write to FS
	oxReturnError(writeBuff(path, buff));
	// write type descriptor
	oxReturnError(ox::buildTypeDef(&m_typeStore, obj));
	// write out type store
	static constexpr auto descPath = "/.nostalgia/type_descriptors";
	oxReturnError(mkdir(descPath));
	for (const auto &t : m_typeStore.typeList()) {
		oxRequireM(typeOut, ox::writeClaw(t, ox::ClawFormat::Organic));
		// replace garbage last character with new line
		typeOut.back().value = '\n';
		// write to FS
		const auto typePath = ox::sfmt("{}/{};{}", descPath, t->typeName, t->typeVersion);
		oxReturnError(writeBuff(typePath, typeOut));
	}
	fileUpdated.emit(path);
	return OxError(0);
}

template<typename T>
ox::Result<ox::UniquePtr<T>> Project::loadObj(const ox::String &path) const noexcept {
	auto obj = ox::make_unique<T>();
	oxRequire(buff, loadBuff(path));
	return ox::readClaw<T>(buff);
}

template<typename Functor>
ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept {
	switch (e) {
		case ProjectEvent::None:
			break;
		case ProjectEvent::FileAdded:
			connect(this, &Project::fileAdded, tgt, slot);
			break;
		case ProjectEvent::FileRecognized:
		{
			oxRequire(files, listFiles());
			for (const auto &f : files) {
				slot(f);
			}
			connect(this, &Project::fileRecognized, tgt, slot);
			break;
		}
		case ProjectEvent::FileDeleted:
			connect(this, &Project::fileDeleted, tgt, slot);
			break;
		case ProjectEvent::FileUpdated:
			connect(this, &Project::fileUpdated, tgt, slot);
			break;
	}
	return OxError(0);
}

}
