/*
 * Copyright 2016 - 2025 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/model/descwrite.hpp>
#include <ox/std/hashmap.hpp>

#include <keel/typestore.hpp>
#include <keel/media.hpp>

namespace studio {

enum class ProjectEvent {
	None,
	DirAdded,
	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,
	DirDeleted,
	FileDeleted,
	FileUpdated,
	FileMoved,
};

[[nodiscard]]
constexpr ox::Result<ox::StringView> fileExt(ox::StringViewCR path) noexcept {
	auto const extStart = ox::find(path.crbegin(), path.crend(), '.').offset();
	if (!extStart) {
		return ox::Error(1, "file path does not have valid extension");
	}
	return substr(path, extStart + 1);
}

[[nodiscard]]
constexpr ox::StringView parentDir(ox::StringView path) noexcept {
	if (path.size() && path[path.size() - 1] == '/') {
		path = substr(path, 0, path.size() - 1);
	}
	auto const extStart = ox::find(path.crbegin(), path.crend(), '/').offset();
	return substr(path, 0, extStart);
}

class Project: public ox::SignalHandler {
	private:
		ox::SmallMap<ox::String, ox::Optional<ox::ClawFormat>> m_typeFmt;
		keel::Context &m_kctx;
		ox::String m_path;
		ox::String m_projectDataDir;
		ox::String m_typeDescPath;
		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::StringParam path, ox::StringViewCR projectDataDir);

		ox::Error create() noexcept;

		[[nodiscard]]
		ox::String const &projectPath() const noexcept;

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

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

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

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

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

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

		ox::Error copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept;

		ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept;

		ox::Error moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept;

		ox::Error deleteItem(ox::StringViewCR path) noexcept;

		[[nodiscard]]
		keel::Context &kctx() noexcept {
			return m_kctx;
		}

		[[nodiscard]]
		keel::Context const &kctx() const noexcept {
			return m_kctx;
		}

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

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

		[[nodiscard]]
		ox::Vector<ox::String> const &fileList(ox::StringViewCR ext) noexcept;

		ox::Error writeTypeStore() noexcept;

		[[nodiscard]]
		constexpr ox::TypeStore &typeStore() noexcept {
			return m_typeStore;
		}

		[[nodiscard]]
		constexpr ox::TypeStore const &typeStore() const noexcept {
			return m_typeStore;
		}

	private:
		void buildFileIndex() noexcept;

		void indexFile(ox::StringViewCR path) noexcept;

		ox::Error writeBuff(ox::StringViewCR path, ox::BufferView const &buff) noexcept;

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

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

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

	// signals
	public:
		ox::Signal<ox::Error(ProjectEvent, ox::StringViewCR)> fileEvent;
		ox::Signal<ox::Error(ox::StringViewCR)> dirAdded;
		ox::Signal<ox::Error(ox::StringViewCR)> 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::StringViewCR)> fileRecognized;
		ox::Signal<ox::Error(ox::StringViewCR)> fileDeleted;
		ox::Signal<ox::Error(ox::StringViewCR)> dirDeleted;
		ox::Signal<ox::Error(ox::StringViewCR path, ox::UUID const &id)> fileUpdated;
		ox::Signal<ox::Error(ox::StringViewCR oldPath, ox::StringViewCR newPath, ox::UUID const &id)> fileMoved;

};

template<typename T>
ox::Error Project::writeObj(ox::StringViewCR path, T const &obj, ox::ClawFormat fmt) noexcept {
	OX_REQUIRE_M(buff, ox::writeClaw(obj, fmt));
	if (fmt == ox::ClawFormat::Organic) {
		*buff.back().value = '\n';
	}
	// write to FS
	OX_RETURN_ERROR(mkdir(parentDir(path)));
	OX_RETURN_ERROR(writeBuff(path, buff));
	// write type descriptor
	if (m_typeStore.get<T>().error) {
		OX_RETURN_ERROR(ox::buildTypeDef(m_typeStore, obj));
	}
	OX_REQUIRE(desc, m_typeStore.get<T>());
	auto const descPath = ox::sfmt("{}/{}", m_typeDescPath, buildTypeId(*desc));
	auto const descExists = m_fs.exists(descPath);
	if (!descExists) {
		OX_RETURN_ERROR(writeTypeStore());
	}
	OX_RETURN_ERROR(keel::reloadAsset(m_kctx, path));
	OX_REQUIRE(uuid, pathToUuid(m_kctx, path));
	fileUpdated.emit(path, uuid);
	return {};
}

template<typename T>
ox::Error Project::writeObj(ox::StringViewCR path, T const &obj) noexcept {
	OX_REQUIRE(ext, fileExt(path));
	auto const fmt = m_typeFmt[ext].or_value(ox::ClawFormat::Metal);
	return writeObj(path, obj, fmt);
}

template<typename T>
ox::Result<T> Project::loadObj(ox::StringViewCR path) const noexcept {
	OX_REQUIRE(buff, loadBuff(path));
	if constexpr(ox::is_same_v<T, ox::ModelObject>) {
		return keel::readAsset(m_typeStore, buff);
	} else {
		OX_REQUIRE(typeId, keel::readAssetTypeId(buff));
		if (typeId != ox::ModelTypeId_v<T>) {
			return keel::convert<T>(m_kctx, buff);
		}
		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::DirAdded:
			connect(this, &Project::dirAdded, tgt, slot);
			break;
		case ProjectEvent::FileAdded:
			connect(this, &Project::fileAdded, tgt, slot);
			break;
		case ProjectEvent::FileRecognized:
		{
			OX_REQUIRE(files, listFiles());
			for (auto const &f : files) {
				slot(f);
			}
			connect(this, &Project::fileRecognized, tgt, slot);
			break;
		}
		case ProjectEvent::DirDeleted:
			connect(this, &Project::dirDeleted, tgt, slot);
			break;
		case ProjectEvent::FileDeleted:
			connect(this, &Project::fileDeleted, tgt, slot);
			break;
		case ProjectEvent::FileUpdated:
			connect(this, &Project::fileUpdated, tgt, slot);
			break;
		case ProjectEvent::FileMoved:
			connect(this, &Project::fileMoved, tgt, slot);
			break;
	}
	return {};
}

}
