/*
 * Copyright 2016 - 2023 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 <keel/typestore.hpp>
#include <keel/media.hpp>

namespace 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::StringView> fileExt(ox::CRStringView 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 substr(path, extStart + 1);
}

class Project {
	private:
		keel::Context &m_ctx;
		ox::String m_path;
		ox::String m_projectDataDir;
		mutable keel::TypeStore m_typeStore;
		ox::FileSystem &m_fs;
		ox::HashMap<ox::String, ox::Vector<ox::String>> m_fileExtFileMap;

	public:
		explicit Project(keel::Context &ctx, ox::String path, ox::CRStringView projectDataDir) noexcept;

		ox::Error create() noexcept;

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

		ox::Error mkdir(ox::CRStringView path) const noexcept;

		/**
		 * Writes a MetalClaw object to the project at the given path.
		 */
		template<typename T>
		ox::Error writeObj(
				ox::CRStringView path,
				T const&obj,
				ox::ClawFormat fmt = ox::ClawFormat::Metal) noexcept;

		template<typename T>
		ox::Result<T> loadObj(ox::CRStringView path) const noexcept;

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

		[[nodiscard]]
		bool exists(ox::CRStringView 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(ox::CRStringView ext) noexcept;

	private:
		void buildFileIndex() noexcept;

		void indexFile(ox::CRStringView path) noexcept;

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

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

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

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

	// signals
	public:
		ox::Signal<ox::Error(ProjectEvent, ox::CRStringView)> fileEvent;
		ox::Signal<ox::Error(ox::CRStringView)> 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(ox::CRStringView)> fileRecognized;
		ox::Signal<ox::Error(ox::CRStringView)> fileDeleted;
		ox::Signal<ox::Error(ox::CRStringView)> fileUpdated;

};

template<typename T>
ox::Error Project::writeObj(ox::CRStringView path, T const&obj, ox::ClawFormat fmt) noexcept {
	oxRequireM(buff, ox::writeClaw(obj, fmt));
	// write to FS
	oxReturnError(writeBuff(path, buff));
	// write type descriptor
	if (m_typeStore.get<T>().error) {
		oxReturnError(ox::buildTypeDef(&m_typeStore, &obj));
	}
	// write out type store
	const auto descPath = ox::sfmt("/{}/type_descriptors", m_projectDataDir);
	oxReturnError(mkdir(descPath));
	for (auto const&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, buildTypeId(*t));
		oxReturnError(writeBuff(typePath, typeOut));
	}
	oxReturnError(keel::setAsset(m_ctx, path, obj));
	fileUpdated.emit(path);
	return {};
}

template<typename T>
ox::Result<T> Project::loadObj(ox::CRStringView path) const noexcept {
	oxRequire(buff, loadBuff(path));
	if constexpr(ox::is_same_v<T, ox::ModelObject>) {
		return keel::readAsset(&m_typeStore, buff);
	} else {
		return keel::readAsset<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 (auto const&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 {};
}

}
