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

#include <algorithm>
#include <filesystem>

#include <ox/std/std.hpp>

#include <keel/module.hpp>

#include <studio/project.hpp>

namespace studio {

static_assert(fileExt("main.c").value == "c");
static_assert(fileExt("a.b.c").value == "c");
static_assert(parentDir("/a/b/c") == "/a/b");
static_assert(parentDir("/a/b/c/") == "/a/b");

static void generateTypes(ox::TypeStore &ts) noexcept {
	for (auto const mod : keel::modules()) {
		for (auto gen : mod->types()) {
			oxLogError(gen(ts));
		}
	}
}

Project::Project(keel::Context &ctx, ox::String path, ox::CRStringView projectDataDir):
	m_ctx(ctx),
	m_path(std::move(path)),
	m_projectDataDir(projectDataDir),
	m_typeDescPath(ox::sfmt("/{}/type_descriptors", m_projectDataDir)),
	m_typeStore(*m_ctx.rom, ox::sfmt("/{}/type_descriptors", projectDataDir)),
	m_fs(*m_ctx.rom) {
	oxTracef("studio", "Project: {}", m_path);
	generateTypes(m_typeStore);
	if (ox::defines::Debug) {
		oxThrowError(writeTypeStore());
	}
	buildFileIndex();
}

ox::Error Project::create() noexcept {
	std::error_code ec;
	std::filesystem::create_directory(m_path.toStdString(), ec);
	return OxError(static_cast<ox::ErrorCode>(ec.value()), "PassThroughFS: mkdir failed");
}

ox::String const&Project::projectPath() const noexcept {
	return m_path;
}

ox::FileSystem *Project::romFs() noexcept {
	return &m_fs;
}

ox::Error Project::mkdir(ox::CRStringView path) const noexcept {
	oxReturnError(m_fs.mkdir(path, true));
	fileUpdated.emit(path);
	return {};
}

ox::Result<ox::FileStat> Project::stat(ox::CRStringView path) const noexcept {
	return m_fs.stat(path);
}

bool Project::exists(ox::CRStringView path) const noexcept {
	return m_fs.stat(path).error == 0;
}

ox::Vector<ox::String> const&Project::fileList(ox::CRStringView ext) noexcept {
	return m_fileExtFileMap[ext];
}

ox::Error Project::writeTypeStore() noexcept {
	// write all descriptors because we don't know which types T depends on
	oxReturnError(mkdir(m_typeDescPath));
	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
		auto const typePath = ox::sfmt("{}/{}", m_typeDescPath, buildTypeId(*t));
		oxReturnError(writeBuff(typePath, typeOut));
	}
	return {};
}

void Project::buildFileIndex() noexcept {
	auto [files, err] = listFiles();
	if (err) {
		oxLogError(err);
		return;
	}
	m_fileExtFileMap.clear();
	std::sort(files.begin(), files.end());
	for (auto const&file : files) {
		if (!beginsWith(file, ox::sfmt("/.{}/", m_projectDataDir))) {
			indexFile(file);
		}
	}
}

void Project::indexFile(ox::CRStringView path) noexcept {
	auto const [ext, err] = fileExt(path);
	if (err) {
		return;
	}
	m_fileExtFileMap[ext].emplace_back(path);
}

ox::Error Project::writeBuff(ox::CRStringView path, ox::Buffer const&buff) noexcept {
	constexpr auto HdrSz = 40;
	ox::Buffer outBuff;
	outBuff.reserve(buff.size() + HdrSz);
	ox::BufferWriter writer(&outBuff);
	auto const [uuid, err] = m_ctx.pathToUuid.at(path);
	if (!err) {
		oxReturnError(keel::writeUuidHeader(writer, *uuid));
	}
	oxReturnError(writer.write(buff.data(), buff.size()));
	auto const newFile = m_fs.stat(path).error != 0;
	oxReturnError(m_fs.write(path, outBuff.data(), outBuff.size(), ox::FileType::NormalFile));
	if (newFile) {
		fileAdded.emit(path);
		indexFile(path);
	} else {
		fileUpdated.emit(path);
	}
	return {};
}

ox::Result<ox::Buffer> Project::loadBuff(ox::CRStringView path) const noexcept {
	return m_fs.read(path);
}

ox::Error Project::lsProcDir(ox::Vector<ox::String> *paths, ox::CRStringView path) const noexcept {
	oxRequire(files, m_fs.ls(path));
	for (auto const&name : files) {
		auto fullPath = ox::sfmt("{}/{}", path, name);
		oxRequire(stat, m_fs.stat(ox::StringView(fullPath)));
		switch (stat.fileType) {
			case ox::FileType::NormalFile:
				paths->emplace_back(std::move(fullPath));
				break;
			case ox::FileType::Directory:
				oxReturnError(lsProcDir(paths, fullPath));
				break;
			case ox::FileType::None:
				break;
		}
	}
	return {};
}

ox::Result<ox::Vector<ox::String>> Project::listFiles(ox::CRStringView path) const noexcept {
	ox::Vector<ox::String> paths;
	oxReturnError(lsProcDir(&paths, path));
	return paths;
}

}
