/*
Crafter® Build
Copyright (C) 2025 Catcrafts®
Catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/
module;
#include "../lib/json.hpp"
#include <dlfcn.h>
#include <stdlib.h>
module Crafter.Build:Project_impl;
import :Project;
import std;
import :Configuration;
import :Command;
namespace fs = std::filesystem;

namespace Crafter {
    Project::Project(std::string&& name, fs::path&& path, std::vector<Configuration>&& configurations) : name(std::move(name)), path(std::move(path)), configurations(std::move(configurations)) {}
    Project::Project(std::string&& name, fs::path&& path, std::vector<Configuration>&& configurations, fs::path&& binDir, fs::path&& buildDir) : name(std::move(name)), path(std::move(path)), configurations(std::move(configurations)), binDir(std::move(binDir)), buildDir(std::move(buildDir)) {}
    
    Configuration& Project::Build(std::string_view configuration) {
        for(Configuration& config : configurations) {
            if(config.name == configuration){
                Build(config);
                return config;
            }
        }
        throw std::runtime_error(std::format("Configuration: {} not found.", configuration));
    }
    Configuration& Project::Build(std::string_view configuration, const fs::path& binDir, const fs::path& outputDir, const fs::path& buildDir, std::string outputName) {
        for(Configuration& config : configurations) {
            if(config.name == configuration){
                Build(config, binDir, binDir, outputDir, outputName);
                return config;
            }
        }
        throw std::runtime_error(std::format("Configuration: {} not found.", configuration));
    }
    void Project::Build(Configuration& config) const {
        Build(config, binDir/config.name, binDir/config.name, buildDir/config.name, name);
    }
    void Project::Build(Configuration& config, const fs::path& binDir, const fs::path& outputDir, const fs::path& buildDir, std::string outputName) const {
        if (!fs::exists(binDir)) {
            fs::create_directories(binDir);
        }

        if (!fs::exists(outputDir)) {
            fs::create_directories(outputDir);
        }

        if (!fs::exists(buildDir)) {
            fs::create_directories(buildDir);
        }

        std::vector<std::thread> threads;
        for(const Shader& shader : config.shaders) {
            if(shader.Check(outputDir)) {
                threads.emplace_back(&Shader::Compile, &shader, outputDir);
            }
        }

        std::thread fileThread([&config, &outputDir](){
            for (const fs::path& additionalFile : config.additionalFiles) {
                fs::path destination = outputDir / additionalFile.filename();
                std::cout << destination << std::endl;

                if (fs::is_directory(additionalFile)) {
                    if (!fs::exists(destination)) {
                        fs::copy(additionalFile, destination, fs::copy_options::recursive);
                    } else if (fs::last_write_time(additionalFile) > fs::last_write_time(destination)) {
                        fs::remove_all(destination);
                        fs::copy(additionalFile, destination, fs::copy_options::recursive);
                    }
                } else {
                    // Handle regular file
                    if (!fs::exists(destination)) {
                        fs::copy(additionalFile, destination);
                    } else if (fs::last_write_time(additionalFile) > fs::last_write_time(destination)) {
                        fs::remove(destination);
                        fs::copy(additionalFile, destination);
                    }
                }
            }
        });

        char path[PATH_MAX];
        ssize_t count = readlink("/proc/self/exe", path, PATH_MAX);
        if (count == -1) {
            throw std::runtime_error("Failed to get executable path");
        }
        path[count] = '\0';
        const std::string exeDir = fs::path(path).parent_path().parent_path().string();
        std::string command = "clang++";
        if(!config.target.empty()) {
            command += std::format(" --target={}", config.target);
            if(config.target == "wasm32-wasi") {
                command += std::format(" --sysroot={}/wasi-sysroot-28.0 -fno-exceptions -fno-c++-static-destructors", exeDir);
            }
        }
        if(!config.march.empty()) {
            command += std::format(" -march={}", config.march);
        } 
        if(!config.standard.empty()) {
            command += std::format(" -std={}", config.standard);
        } else {
            command += std::format(" -std=c++26");
        }
        for(const Define& define : config.defines) {
            command += std::format(" -D {}={}", define.name, define.value);
        }
    
        fs::path pcmDir;

        if(config.type == CRAFTER_CONFIGURATION_TYPE_SHARED_LIBRARY) {
            command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
            pcmDir = binDir;
        } else if(config.type == CRAFTER_CONFIGURATION_TYPE_LIBRARY) {
            command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
            pcmDir = binDir;
        } else {
            command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
            pcmDir = buildDir;
        }

        if(config.debug) {
            command += " -g -D CRAFTER_BUILD_CONFIGURATION_DEBUG";
        } else {
            command += " -O3";
        }

        if(config.target != "wasm32-wasi") {
            const std::string stdPcm = std::format("{}/std.pcm", exeDir);
            std::string gccVersion = RunCommand("g++ -dumpversion");
            gccVersion.pop_back();
            fs::path stdCc = fs::path(std::format("/usr/include/c++/{}/bits/std.cc", gccVersion));

            if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) {
                std::string result = RunCommand(std::format("cp {} {}/std.cppm\nclang++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {}/std.cppm -o {}", stdCc.string(), exeDir, exeDir, stdPcm));
                if(result != "") {
                    throw std::runtime_error(result);
                }
            }
            command += std::format(" -fprebuilt-module-path={} -fprebuilt-module-path={}", pcmDir.string(), exeDir);
        } else {
            const std::string stdPcm = std::format("{}/wasi-sysroot-28.0/share/libc++/v1/std.pcm", exeDir);
            fs::path stdCc = fs::path(std::format("{}/wasi-sysroot-28.0/share/libc++/v1/std.cppm", exeDir));
            if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) {
                std::string result = RunCommand(std::format("clang++ -fno-exceptions --target=wasm32-wasi -nodefaultlibs --sysroot={}/wasi-sysroot-28.0 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier -fno-c++-static-destructors --precompile {} -o {}", exeDir, stdCc.string(), stdPcm));
                if(result != "") {
                    throw std::runtime_error(result);
                }
            }
            command += std::format(" -fprebuilt-module-path={} -fprebuilt-module-path={}/wasi-sysroot-28.0/share/libc++/v1", pcmDir.string(), exeDir);


            const fs::path indexPath = std::format("{}/index.html", exeDir);
            const fs::path indexDstPath = binDir/"index.html";
            if(!fs::exists(indexDstPath)) {
                fs::copy(indexPath, indexDstPath);
            } else if(fs::last_write_time(indexDstPath) < fs::last_write_time(indexPath)) {
                fs::remove(indexDstPath);
                fs::copy(indexPath, indexDstPath);
            }

            const fs::path runtimePath = std::format("{}/runtime.js", exeDir);
            const fs::path runtimeDstPath = binDir/"runtime.js";
            if(!fs::exists(runtimeDstPath)) {
                fs::copy(runtimePath, runtimeDstPath);
            } else if(fs::last_write_time(runtimeDstPath) < fs::last_write_time(runtimePath)) {
                fs::remove(runtimeDstPath);
                fs::copy(runtimePath, runtimeDstPath);
            }
        }


        std::unordered_set<std::string> depLibSet;
        std::vector<std::thread> depThreads = std::vector<std::thread>(config.dependencies.size());
        std::mutex libMutex;
        std::string libsString;

        for(std::uint_fast32_t i = 0; i < depThreads.size(); i++) {
            depThreads[i] = std::thread([i, &config, &libMutex, &depLibSet, &buildDir, &pcmDir, &libsString, &binDir, this](){
                if(config.dependencies[i].path.ends_with(".git")) {
                    fs::path name = fs::path(config.dependencies[i].path).filename();
                    name.replace_extension();
                    if(!fs::exists(buildDir/name)) {
                        if(!config.dependencies[i].branch.empty()) {
                            system(std::format("cd {} && git clone {} && cd {} && git switch {}", buildDir.string(), config.dependencies[i].path, (buildDir/name).string(), config.dependencies[i].branch).c_str());
                        } else if(!config.dependencies[i].commit.empty()){
                            system(std::format("cd {} && git clone {} && cd {} && git checkout {}", buildDir.string(), config.dependencies[i].path, (buildDir/name).string(), config.dependencies[i].commit).c_str());
                        } else {
                            system(std::format("cd {} && git clone {}", buildDir.string(), config.dependencies[i].path).c_str());
                        }
                    } else if(config.dependencies[i].commit.empty()) {
                        system(std::format("cd {} && git pull", (buildDir/name).string()).c_str());
                    }
                    config.dependencies[i].path = buildDir/name/"project.json";
                }
                if(fs::path(config.dependencies[i].path).is_relative()) {
                    config.dependencies[i].path = this->path/config.dependencies[i].path;
                }
                Project project = Project::LoadFromJSON(config.dependencies[i].path);
                for(Configuration& depConfig : project.configurations) {
                    if(depConfig.name == config.dependencies[i].configuration){
                        fs::path depBuildDir = fs::path(config.dependencies[i].path).parent_path()/project.buildDir/depConfig.name;
                        project.Build(depConfig, pcmDir, binDir, depBuildDir, project.name);
                        libMutex.lock();
                        if (depLibSet.insert(project.name).second) {
                            libsString+=std::format(" -l{}", project.name);
                        }
                        for(const std::string& lib2 : depConfig.libs) {
                            if (depLibSet.insert(lib2).second) {
                                libsString+=std::format(" -l{}", lib2);
                            }
                        }
                        for(const Dependency& dep2 : depConfig.dependencies) {
                            if (depLibSet.insert(project.name).second) {
                                libsString+=std::format(" -l{}", project.name);
                            }
                        }
                        libMutex.unlock();
                        return;
                    }
                }
                throw std::runtime_error(std::format("Configuration: {} not found.", config.dependencies[i].configuration)); 
            });
        }

        for(std::thread& thread : depThreads) {
            thread.join();
        }

        std::string files;
        bool repack = false;
        for(std::unique_ptr<Module>& modulee : config.interfaces) {
            if(modulee->Check(pcmDir)) {
                threads.emplace_back(&Module::Compile, modulee.get(), command, pcmDir, buildDir);
                repack = true;
            }
            files += std::format(" {}/{}.o", buildDir.string(),  modulee->path.filename().string());
            for(std::unique_ptr<ModulePartition>& part : modulee->partitions) {
                files += std::format(" {}/{}.o", buildDir.string(),  part->path.filename().string());
            }
        }

        for(const Implementation& implementation : config.implementations) {
            if(implementation.Check(buildDir, pcmDir)) {
                repack = true;
                threads.emplace_back(&Implementation::Compile, &implementation, command, buildDir);
            }
            files += std::format(" {}/{}_impl.o", buildDir.string(),  implementation.path.filename().string());
        }
        
        for(std::thread& thread : threads) {
            thread.join();
        }

        if(config.target != "wasm32-wasi") {
            command += " -L/usr/local/lib";
        }

        command += libsString;

        for(const std::string& lib : config.libs) {
            depLibSet.insert(lib);
            command += std::format(" -l{}", lib);
        }

        fileThread.join();

        if(config.dependencies.size() > 0){
            command += std::format(" -L{}", pcmDir.string());
        }

        if(repack) {
            if(config.type == CRAFTER_CONFIGURATION_TYPE_EXECUTABLE){
                if(config.target == "wasm32-wasi") {
                    outputName += ".wasm";
                }
                RunClang(std::format("{}{} -o {} -fuse-ld=lld", command, files, (binDir/outputName).string()));
            } else if(config.type == CRAFTER_CONFIGURATION_TYPE_LIBRARY){
                std::cout << std::format("ar r {}.a {}", (binDir/fs::path(std::string("lib")+outputName)).string(), files) << std::endl;
                RunCommandIgnore(std::format("ar r {}.a {}", (binDir/fs::path(std::string("lib")+outputName)).string(), files));
            } else {
                RunClang(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld", command, files, (binDir/(std::string("lib")+outputName)).string()));
            }
        }
    }

    Project Project::LoadFromJSON(const fs::path& path) {
        if (!fs::exists(path)) {
            throw std::runtime_error(std::format("Project file: {} not found.", path.generic_string()));
        }

        std::ifstream f(path);
        nlohmann::json data = nlohmann::json::parse(f);
        std::string name = data["name"].get<std::string>();

        fs::path workingDir = path;
        workingDir.remove_filename();

        std::vector<Configuration> configurations;
        nlohmann::json configs = data["configurations"];
        std::vector<Test> tests;
        nlohmann::json testJson = data["tests"];

        for (nlohmann::json::iterator it = configs.begin(); it != configs.end(); ++it) {
            configurations.emplace_back(configs, (*it), workingDir);
        }

        for (nlohmann::json::iterator it = testJson.begin(); it != testJson.end(); ++it) {
            tests.emplace_back(configs, (*it), workingDir);
        }
        
        Project project(std::move(name), std::move(workingDir), std::move(configurations));
        if(data.contains("bin_dir")) {
            project.binDir = data["bin_dir"].get<std::string>();
        } else {
            project.binDir = "bin";
        }
        if(data.contains("build_dir")) {
            project.buildDir = data["build_dir"].get<std::string>();
        } else {
            project.buildDir = "build";
        }
        project.tests = std::move(tests);
        return project;
    }

    std::vector<TestResult> Project::RunTests() {
        std::vector<TestResult> results;
        for(Test& test : tests) {
            results.push_back(RunTest(test));
        }
        return results;
    }

    TestResult Project::RunTest(const std::string_view testName) {
        for(Test& test : tests) {
            if(test.config.name == testName){
                return RunTest(test);
            }
        }
        throw std::runtime_error(std::format("Test: {} not found.", testName));
    }

    TestResult Project::RunTest(Test& test) const {
        fs::path binDir = path/this->binDir/test.config.name;
        fs::path buildDir = path/this->buildDir/test.config.name;

        try {
            Build(test.config, binDir, binDir, buildDir, test.config.name);
        } catch(std::exception& e) {
            return {test.config.name, std::string(e.what())};
        }

        std::string lib = std::format("{}/lib{}.so", binDir.string(), test.config.name);
        void* handle = dlopen(lib.c_str(), RTLD_NOW);
        if (!handle) {
           throw std::runtime_error(std::format("Failed to load test library, {}, {}", lib, dlerror()));
        }
        dlerror();
        typedef std::string* (*RunTestFunc)();
        RunTestFunc loadedTest = (RunTestFunc) dlsym(handle, "RunTest");
        const char* dlsym_error = dlerror();
        if (dlsym_error) {
            std::string msg = std::string(dlsym_error);
            dlclose(handle);
            throw std::runtime_error(std::format("Cannot load symbol 'RunTest': {}", msg));
        }
        std::string* testResult;
        try {
            testResult = loadedTest();
        } catch(std::exception& e) {
            return {test.config.name, std::string(e.what())};
        }
        if(testResult != nullptr) {
            TestResult result = {test.config.name, *testResult};
            delete testResult;
            return result;
        } else {
            return {test.config.name, ""};
        }
        return {test.config.name, ""};
    }

    // void AddModule(std::string_view configuration, std::string_view filename);
    // void AddModule(Configuration& configuration, std::string_view filename);
    // void AddModuleSourcePair(std::string_view configuration, std::string_view filename);
    // void AddModuleSourcePair(Configuration& configuration, std::string_view filename);
    // void AddTest(std::string_view configuration, std::string_view filename, std::string_view content);
    // void AddTest(Configuration& configuration, std::string_view filename, std::string_view content);
    // void SaveToJSON(const fs::path& path) const;
}
