/*
 * Copyright (c) KylinSoft  Co., Ltd. 2024. All rights reserved.
 *
 * kaiming-builder is licensed under the GPL v2.0+.
 * 
 * See the LICENSE file for more details.
 */

#include "KMBuildFlow.h"
#include "KMBuilderBuild.h"
#include "KMBuilderContext.h"
#include "KMBuilderUtils.h"
#include "KMBuilderCache.h"
#include "common/KMProcessUtils.h"
#include "common/KMException.h"
#include "common/KMInfoJson.h"
#include "common/KMProcessUtils.h"
#include "common/KMLogger.h"

namespace fs = std::filesystem;

REGISTER_SUBCOMMAND_DYNCREATE(build, KMBuildFlow)

KMBuildFlow::KMBuildFlow()
{
}

KMBuildFlow::~KMBuildFlow()
{}

int KMBuildFlow::dispose(int argc, char **argv)
{
    init(argc, argv);

    return run();
}

void KMBuildFlow::init(int argc, char **argv)
{
    // 参数解析
    builderContext.parseCommandLine(argc, argv);

    // 加载清单配置
    builderContext.loadConfig();

    KMBuilderCache::instance().open(builderContext.getCacheDir());
}

int KMBuildFlow::run()
{
    setCcache(builderContext.isCcache(), builderContext.getCcacheDir());

    if (!builderContext.isFinishOnly() && !builderContext.isExportOnly())
    {
        std::map<std::shared_ptr<KMBuilderModule>, std::string> moudle_str_map;
        downloadFile(moudle_str_map);

        buildInit();

        build(moudle_str_map);

        cleanup();

        if (builderContext.isBuildOnly())
        {
            return EXIT_SUCCESS;
        }
    }

    if (!builderContext.isExportOnly())
    {
        buildFinish();
    }

    if (builderContext.isExport() || builderContext.isExportAll())
    {
        buildExport();
    }

    return EXIT_SUCCESS;
}

/**
 * @brief: 设置ccache目录
 * @param: 是否启用
 * @param: ccache缓存目录
 */
bool KMBuildFlow::setCcache(bool enable, const std::string &ccacheDir)
{
    if (enable)
    {
        std::error_code ec;
        std::string binDir = KMBuilderUtils::buildFilename(ccacheDir, "bin");
        if (!KMBuilderUtils::mkpath(binDir, ec))
        {
            throw KMException(ec.message());
        }

        std::vector<string> compilers = { "cc", "c++", "gcc", "g++" };
        for (auto &compiler : compilers)
        {
            std::string symlinkPath = KMBuilderUtils::buildFilename(binDir, compiler);
            if (fs::exists(symlinkPath, ec) && !fs::remove(symlinkPath, ec))
            {
                throw KMException(ec.message());
            }
            fs::create_symlink("/usr/bin/ccache", symlinkPath, ec);
        }
    }
    else
    {
        std::error_code ec;
        std::string disabledDir = ccacheDir + "/disabled";
        if (!KMBuilderUtils::mkpath(disabledDir, ec))
        {
            throw KMException(ec.message());
        }

        std::string ccacheConfiPath = disabledDir + "/ccache.conf";
        std::ofstream file(ccacheConfiPath, ios::out);
        file << "disable = true\n";
        file.close();
    }

    return true;
}

/**
 * @brief : 编译初始化
 * @return : true-成功; false-失败
 */
bool KMBuildFlow::buildInit()
{
    KMTrace("KMBuildFlow::buildInit invoke begin");

    if (!builderContext.isForceClean())
    {
        if (KMBuilderCache::instance().lookup("init"))
        {
            KMInfo("Skiping init stage");
            return true;
        }
    }

    // 清理appDir，重新初始化
    KMBuilderUtils::rmRfAll(builderContext.getAppDir());

    std::vector<std::string> args;
    initArgs(args);

    if (!KMProcessUtils::spawn("kaiming", args, false))
    {
        throw KMException("kaiming build-init failed.");
    }
    KMBuilderCache::instance().open(builderContext.getCacheDir());
    KMBuilderCache::instance().recordStage("init");

    KMTrace("KMBuildFlow::buildInit invoke end");
    return true;
}

/**
 * @brief : 下载
 * @return : true-成功; false-失败
 */
bool KMBuildFlow::downloadFile(std::map<std::shared_ptr<KMBuilderModule>, std::string> &moudle_str_map)
{
    KMTrace("KMBuildFlow::downloadFile invoke begin");

    std::shared_ptr<KMBuilderManifest> manifest = builderContext.manifestConfig();
    expandModules(manifest->m_components);
    KMInfo("Modules : ");
    for (auto module : m_expandModules)
    {
        std::cout << std::any_cast<std::string>(module->getProperty(KMBuilderModule::Module_Name)) << std::endl;
    }
    
    KMInfo("Starting download");
    std::string stopAt = builderContext.getStopat();
    for (auto module : m_expandModules)
    {
        std::string name = std::any_cast<std::string>(module->getProperty(KMBuilderModule::Module_Name));
        if (stopAt == name)
        {
            return true;
        }

        if (!builderContext.isForceClean())
        {
            if (KMBuilderCache::instance().lookup("build-component-" + name))
            {
                KMInfo("Cache hit, skipping download : " + name);
                continue;
            }
        }

        if (!createSubdir(builderContext.getBuildDir(), name, moudle_str_map[module]))
        {
            throw KMException("Failed to create build dir for component : " + name);
        }

        if (!download(module, moudle_str_map[module]))
        {
            throw KMException("Failed to download sources for component : " + name);
        }
    }

    KMTrace("KMBuildFlow::downloadFile invoke end");
    return true;
}

/**
 * @brief : 编译
 * @return : true-成功; false-失败
 */
bool KMBuildFlow::build(std::map<std::shared_ptr<KMBuilderModule>, std::string> &moudle_str_map)
{
    KMTrace("KMBuildFlow::build invoke begin");

    // 记录构建前初始化后的文件列表
    m_lastFiles = KMBuilderUtils::getDirFiles(KMBuilderUtils::buildFilename(builderContext.getAppDir(), "files"));
    m_moduleToFiles["init-stage"] = m_lastFiles;
    
    KMInfo("Starting build");
    std::string stopAt = builderContext.getStopat();
    for (auto module : m_expandModules)
    {
        std::string name = std::any_cast<std::string>(module->getProperty(KMBuilderModule::Module_Name));
        if (stopAt == name)
        {
            return true;
        }

        if (!isShouldBuild(module))
        {
            continue;
        }

        if (!builderContext.isForceClean())
        {
            if (KMBuilderCache::instance().lookup("build-component-" + name))
            {
                KMInfo("Cache hit, skipping component : " + name);
                continue;
            }
        }

        KMBuilderBuild build;
        build.m_subdir = moudle_str_map[module];
        if (!build.doBuild(module))
        {
            throw KMException("Failed to build component : " + name);
        }

        // 记录当前模块构建新增的文件
        std::vector<std::string> files = KMBuilderUtils::getDirFiles(KMBuilderUtils::buildFilename(builderContext.getAppDir(), "files"));
        std::vector<std::string> incFiles;
        for (auto& file : files)
        {
            if (!KMBuilderUtils::contains(m_lastFiles, file))
            {
                incFiles.push_back(file);
            }
        }
        m_lastFiles = files;
        m_moduleToFiles[name] = incFiles;

        KMBuilderCache::instance().recordStage("build-component-" + name, incFiles);
    }

    KMTrace("KMBuildFlow::build invoke end");
    return true;
}

/**
 * @brief : 初始化参数
 * @return : true-成功; false-失败
 */
bool KMBuildFlow::initArgs(std::vector<std::string> &args)
{
    /* 
       用法：kaiming build-init [OPTIONS] DIRECTORY APPID
       样例：kaiming build-init --base=dev.openkylin.Base/channel/23.0.0 --runtime=dev.openkylin.Runtime/channel/23.0.0 build top.openkylin.demo
     */
    // args.push_back("kaiming");
    args.push_back("build-init");

    std::shared_ptr<KMBuilderManifest> manifest = builderContext.manifestConfig();
    args.push_back("--arch=" + builderContext.getArch());
    args.push_back("--kind=" + manifest->m_package.m_kind);
    args.push_back("--channel=" + manifest->m_channel);
    args.push_back("--base=" + manifest->m_base);
    args.push_back("--version=" + manifest->m_package.m_version);
    args.push_back("--cacheDir=" + builderContext.getCacheDir());

    if (!manifest->m_depends.empty())
    {
        for (auto depend : manifest->m_depends)
        {
            args.push_back("--depend=" + depend);
        }
    }

    if (!manifest->m_runtime.empty())
    {
        args.push_back("--runtime=" + manifest->m_runtime);
    }

    if (manifest->m_annotations.kylin_privilege)
    {
        args.push_back("--kylin-privilege");
    }

    if (manifest->m_annotations.kylin_sysapp)
    {
        args.push_back("--kylin-sysapp");
    }

    args.push_back(builderContext.getAppDir());
    args.push_back(manifest->m_package.m_id);

    return true;
}

/**
 * @brief : 展开所有的builderModules
 * @param modules : 所有的builderModules
 * @param expandModules : 展开后的builderModules
 * @return : true-成功; false-失败
 */
void KMBuildFlow::expandModules(const std::vector<std::shared_ptr<KMBuilderModule>> &modules)
{
    for (auto module : modules)
    {
        if (module->getData()->m_disabled)
        {
            continue;
        }

        std::vector<std::shared_ptr<KMBuilderModule>> inmodules = module->builderModules();
        if (!inmodules.empty())
        {
            expandModules(inmodules);
        }

        m_expandModules.push_back(module);
    }
}

/**
 * @brief : 是否需要构建
 * @param module : builderModule模块
 * @return : true-需要; false-不需要
 */
bool KMBuildFlow::isShouldBuild(const std::shared_ptr<KMBuilderModule> &module)
{
    if (module->buildSystem() == KMBuilderModule::BuildSystem::BS_Unknow)
    {
        return false;
    }

    if (!module->buildSources().empty())
    {
        return true;
    }

    // 即使没有源代码，也允许构建simple类型，这通常很有用
    if (module->buildSystem() == KMBuilderModule::BuildSystem::BS_Simple)
    {
        return true;
    }

    return false;
}

/**
 * @brief : 构建流程中的cleanup环节
 */ 
void KMBuildFlow::cleanup()
{
    KMTrace("KMBuildFlow::cleanup invoke begin");

    std::string appFiles = KMBuilderUtils::buildFilename(builderContext.getAppDir(), "files");
    std::string dependFiles = KMBuilderUtils::buildFilename(builderContext.getAppDir(), "depends");
    for (auto& module : m_expandModules)
    {
        if (module->cleanup().empty())
        {
            continue ;
        }

        if (m_moduleToFiles.end() == m_moduleToFiles.find(module->getData()->m_name))
        {
            continue ;
        }

        const std::vector<std::string> &files = m_moduleToFiles.at(module->getData()->m_name);
        std::vector<std::string> toRemoves;
        KMBuildFlow::collectCleanupForPath(files, appFiles, module->cleanup(), toRemoves);

        for (auto& remove : toRemoves)
        {
            KMBuilderUtils::rmRfAll(remove);
        }
    }

    std::shared_ptr<KMBuilderManifest> manifest = builderContext.manifestConfig();
    if (!manifest->m_cleanups.empty())
    {
        std::vector<std::string> toRemoves;
        KMBuildFlow::collectCleanupForPath(appFiles, appFiles, manifest->m_cleanups, toRemoves);
        KMBuildFlow::collectCleanupForPath(dependFiles, dependFiles, manifest->m_cleanups, toRemoves);
        for (auto& remove : toRemoves)
        {
            KMBuilderUtils::rmRfAll(remove);
        }
    }

    if (manifest->m_annotations.kylin_sysapp)
    {
        // 删除构建结果中的因挂载而生成到构建结果中的文件，需要在删除空目录之前
        std::vector<std::string> defaultCleanups = {
            "/etc/default/locale",
            "/etc/timezone",
            "/etc/passwd",
            "/etc/group",
            "/etc/pkcs11/pkcs11.conf",
            "/etc/shadow",
            "/etc/gshadow",
            "/etc/machine-id",
            "/etc/localtime",
            "/usr/share/zoneinfo",
            "/etc/resolv.conf",
            "/etc/host.conf",
            "/etc/hosts",
            "/etc/gai.conf",
            "/usr/bin/xdg-open"
        };

        for (auto& cleanup : defaultCleanups)
        {
            std::string remove = KMBuilderUtils::buildFilename(appFiles, cleanup);
            KMBuilderUtils::rmRfAll(remove);
        }

        // 删除构建结果中的空文件夹，这些空文件夹应该都是overlay叠加挂载时创建upperdir时创建的（目的是使得目录可写）
        std::error_code ec;
        if (!KMBuilderUtils::removeEmptyDirectory(appFiles, ec))
        {
            throw KMException(ec.message());
        }
    }

    KMTrace("KMBuildFlow::cleanup invoke end");
}

/**
 * @brief : 收集指定路径下匹配规则的文件/目录列表
 * @param : [in] path, 指定路径
 * @param : [in] prefix, 前缀，匹配比较时，如果match是相对路径，需要去掉文件/目录的前缀后比较
 * @param : [in] matchs, 匹配规则
 * @param : [out] toRemoves，待删除的路径
 */ 
void KMBuildFlow::collectCleanupForPath(const std::string& path, const std::string& prefix, const std::vector<std::string>& matchs, std::vector<std::string>& toRemoves)
{
    fs::path basePath(path);
    if (!fs::exists(path) || matchs.empty())
    {
        return ;
    }

    for (auto const &dirEntry : fs::directory_iterator{ basePath })
    {
        fs::path p = dirEntry.path();
        std::string filename = p.filename();
        if (filename == "." || filename == "..")
        {
            continue ;
        }

        std::string subPath = p.string();
        std::string unprefixedPath = subPath.substr(prefix.length());

        bool isMatch = false;
        for (auto& match : matchs)
        {
            const char *rest =  nullptr;
            if (match.at(0) == '/')
            {
                rest = KMBuilderUtils::pathMatchPrefix(match.c_str(), unprefixedPath.c_str());
            }
            else
            {
                rest = KMBuilderUtils::pathMatchPrefix(match.c_str(), filename.c_str());
            }

            if (rest != nullptr)
            {
                isMatch = true;
                break ;
            }
        }

        if (isMatch)
        {
            toRemoves.push_back(subPath);
            continue ;
        }

        if (fs::is_directory(p))
        {
            KMBuildFlow::collectCleanupForPath(subPath, prefix, matchs, toRemoves);
        }
    }
}

void KMBuildFlow::collectCleanupForPath(const std::vector<std::string>& files, const std::string& prefix, const std::vector<std::string>& matchs, std::vector<std::string> &toRemoves)
{
    std::string filename;
    for (auto& file : files)
    {
        std::string unprefixedPath = file.substr(prefix.length());

        bool isMatch = false;
        for (auto& match : matchs)
        {
            const char *rest =  nullptr;
            if (match.at(0) == '/')
            {
                rest = KMBuilderUtils::pathMatchPrefix(match.c_str(), unprefixedPath.c_str());
            }
            else
            {
                filename = fs::path(file).filename();
                rest = KMBuilderUtils::pathMatchPrefix(match.c_str(), filename.c_str());
            }

            if (rest != nullptr)
            {
                isMatch = true;
                break ;
            }
        }

        if (isMatch)
        {
            toRemoves.push_back(file);
        }
    }
}

/**
 * @brief : 填充info.json的部分信息
 */
void KMBuildFlow::fillInfoJson()
{
    // build-init时已经生成了部分info.json信息，此处填充其它信息
    KMInfoJson metadata;
    metadata.loadFile(KMBuilderUtils::buildFilename(builderContext.getAppDir(), "info.json"));

    std::shared_ptr<KMBuilderManifest> manifest = builderContext.manifestConfig();
    metadata.commands = manifest->m_commands;
    metadata.modules = manifest->m_modules;
    metadata.name = manifest->m_package.m_name;
    metadata.names = manifest->m_package.m_names;
    metadata.description = manifest->m_package.m_description;
    metadata.descriptions = manifest->m_package.m_descriptions;
    metadata.version = manifest->m_package.m_version;
    metadata.permission = manifest->m_permission;
    metadata.envs = manifest->m_envs;
    metadata.license = manifest->m_license;
    metadata.homepage = manifest->m_homepage;
    metadata.maintainers = manifest->maintainers;
    metadata.annotations = manifest->m_annotations;

    metadata.saveFile(KMBuilderUtils::buildFilename(builderContext.getAppDir(), "info.json"));
    if (metadata.kind == PACKGE_KIND_APP && manifest->m_modules.size() > 1) 
    {
        metadata.saveModulesFile(KMBuilderUtils::buildFilename(builderContext.getAppDir(), "modules.json"));
    }
}

/**
 * @brief : 构建流程中的finish环节
 */
void KMBuildFlow::buildFinish()
{
    KMTrace("KMBuildFlow::buildFinish invoke begin");

    std::shared_ptr<KMBuilderManifest> manifest = builderContext.manifestConfig();
    KMInfo("Finishing " + manifest->m_package.m_id);

    if (!builderContext.isForceClean())
    {
        if (KMBuilderCache::instance().lookup("build-finish"))
        {
            KMInfo("Skiping build-finish");
            return;
        }
    }

    fillInfoJson();

    std::vector<std::string> args;
    args.push_back("build-finish");
    args.push_back(builderContext.getAppDir());

    if (!KMProcessUtils::spawn("kaiming", args, false))
    {
        throw KMException("kaiming build-finish failed.");
    }

    KMBuilderCache::instance().recordStage("build-finish");

    KMTrace("KMBuildFlow::buildFinish invoke end");
}

/**
 * @brief : 构建流程中的export环节
 */ 
void KMBuildFlow::buildExport()
{
    KMTrace("KMBuildFlow::buildExport invoke begin");

    std::vector<std::string> args;
    args.push_back("build-export");
    args.push_back("--dest-dir=" + builderContext.getRunDir());
    args.push_back("--bundle-filesystem=" + builderContext.getBundleFilesystem());
    if (builderContext.isExportAll())
    {
        args.push_back("--all");
    }
    args.push_back(builderContext.getAppDir());

    if (!KMProcessUtils::spawn("kaiming", args, false))
    {
        throw KMException("Failed to export.");
    }

    KMTrace("KMBuildFlow::buildExport invoke end");
}

