#include "../include/GlobalLib.hpp"
#include "../include/FileUtil.hpp"
#include "../backend/ninja/Ninja.hpp"
#include "../include/Conf.hpp"
#include "../include/curl.hpp"
#include "../include/Archive.hpp"

using namespace std;

namespace sibs
{
    Result<bool> GlobalLib::validatePackageExists(const FileString &globalLibRootDir, const std::string &name)
    {
        FileString packageDir = globalLibRootDir + TINYDIR_STRING("/");
#if OS_FAMILY == OS_FAMILY_POSIX
        packageDir += name;
#else
        packageDir += utf8To16(name);
#endif
        FileType packageDirFileType = getFileType(packageDir.c_str());
        switch(packageDirFileType)
        {
            case FileType::FILE_NOT_FOUND:
            {
                string errMsg = "Global lib dependency not found: ";
                errMsg += toUtf8(packageDir);
                return Result<bool>::Err(errMsg, DependencyError::DEPENDENCY_NOT_FOUND);
            }
            case FileType::REGULAR:
            {
                string errMsg = "Corrupt library directory. ";
                errMsg += toUtf8(packageDir);
                errMsg += " is a file, expected it to be a directory";
                return Result<bool>::Err(errMsg);
            }
            case FileType::DIRECTORY:
            {
                return Result<bool>::Ok(true);
            }
            default:
            {
                return Result<bool>::Err("Unexpected error!");
            }
        }
    }

    const _tinydir_char_t *sourceFileExtensions[] = { TINYDIR_STRING("c"),TINYDIR_STRING("cc"), TINYDIR_STRING("cpp"), TINYDIR_STRING("cxx") };
    bool isSourceFile(tinydir_file *file)
    {
        if(!file->is_reg)
            return false;

        for(const _tinydir_char_t *sourceFileExtension : sourceFileExtensions)
        {
            if(_tinydir_strcmp(sourceFileExtension, file->extension) == 0)
                return true;
        }

        return false;
    }

    bool isPathSubPathOf(const _tinydir_char_t *path, const FileString &subPathOf)
    {
        return _tinydir_strncmp(path, subPathOf.c_str(), subPathOf.size()) == 0;
    }

    Result<bool> GlobalLib::getLibsLinkerFlags(const SibsConfig &parentConfig, const FileString &globalLibRootDir, const std::string &name, const std::string &version, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback)
    {
        Result<bool> packageExistsResult = validatePackageExists(globalLibRootDir, name);
        if (packageExistsResult.isErr())
            return packageExistsResult;

#if OS_FAMILY == OS_FAMILY_POSIX
        FileString namePlatformNative = name;
        FileString versionPlatformNative = version;
#else
        FileString namePlatformNative = utf8To16(name);
        FileString versionPlatformNative = utf8To16(version);
#endif

        FileString packageDir = globalLibRootDir + TINYDIR_STRING("/");
        packageDir += namePlatformNative;

        // TODO: Instead of checking if version is exact match, check if package has same major version
        // and same or newer minor version
        FileString foundVersion;
        walkDir(packageDir.c_str(), [&foundVersion, &versionPlatformNative](tinydir_file *file)
        {
            if(file->is_dir)
            {
                //printf("version: %s\n", file->name);
                if(_tinydir_strcmp(versionPlatformNative.c_str(), file->name) == 0)
                    foundVersion = file->name;
            }
        });

        if(foundVersion.empty())
            return Result<bool>::Err("Global lib dependency found, but version doesn't match dependency version", DependencyError::DEPENDENCY_VERSION_NO_MATCH);

        packageDir += TINYDIR_STRING("/");
        packageDir += versionPlatformNative;

        FileString projectConfFilePath = packageDir;
        projectConfFilePath += TINYDIR_STRING("/project.conf");

        FileType projectConfFileType = getFileType(projectConfFilePath.c_str());
        switch(projectConfFileType)
        {
            case FileType::FILE_NOT_FOUND:
            {
                string errMsg = "Global lib dependency found: ";
                errMsg += toUtf8(packageDir);
                errMsg += ", but it's missing a project.conf file";
                return Result<bool>::Err(errMsg);
            }
            case FileType::DIRECTORY:
            {
                string errMsg = "Global lib dependency found: ";
                errMsg += toUtf8(packageDir);
                errMsg += ", but it's corrupt (Found directory instead of file)";
                return Result<bool>::Err(errMsg);
            }
        }

        SibsConfig sibsConfig(parentConfig.getCompiler(), packageDir, parentConfig.getOptimizationLevel());
        Result<bool> result = Config::readFromFile(projectConfFilePath.c_str(), sibsConfig);
        if (result.isErr())
            return result;

        if(sibsConfig.getPackageName().empty())
            return Result<bool>::Err("project.conf is missing required field package.name");

        if(sibsConfig.getPackageType() == PackageType::EXECUTABLE)
        {
            string errMsg = "The dependency ";
            errMsg += name;
            errMsg += " is an executable. Only libraries can be dependencies";
            return Result<bool>::Err(errMsg);
        }

        if (!containsPlatform(sibsConfig.getPlatforms(), SYSTEM_PLATFORM))
        {
            string errMsg = "The dependency ";
            errMsg += name;
            errMsg += " does not support your platform (";
            errMsg += asString(SYSTEM_PLATFORM);
            errMsg += ")";
            return Result<bool>::Err(errMsg);
        }

        backend::Ninja ninja;
        // TODO: Use same source file finder as in main.cpp
        FileWalkCallbackFunc collectSourceFiles = [&ninja, &sibsConfig, &collectSourceFiles](tinydir_file *file)
        {
            FileString pathNative = file->path;
#if OS_FAMILY == OS_FAMILY_WINDOWS
            replaceChar(pathNative, L'/', L'\\');
#endif
            if(file->is_reg)
            {
                if (isSourceFile(file))
                {
                    string fileNameNative = toUtf8(pathNative.c_str() + sibsConfig.getProjectPath().size() + 1);
                    ninja.addSourceFile(fileNameNative.c_str());
                }
                else
                {
                    //printf("Ignoring non-source file: %s\n", file->path + projectPath.size());
                }
            }
            else
            {
                // TODO: If compiling without "test" option, do not add test source dir to ninja. Ninja logic will then not build tests...
                // OR I believe there is no reason to run tests in dependencies. The main projects tests should cover that?
                // But you might want to know exactly which dependency is causing issue and which part of it...
                if (!sibsConfig.getTestPath().empty() && isPathSubPathOf(pathNative.c_str(), sibsConfig.getTestPath()))
                {
                    string filePathUtf8 = toUtf8(pathNative.c_str());
                    ninja.addTestSourceDir(filePathUtf8.c_str());
                }
                else if(!directoryToIgnore(pathNative, sibsConfig.getIgnoreDirs()))
                    walkDir(file->path, collectSourceFiles);
            }
        };
        walkDir(packageDir.c_str(), collectSourceFiles);

        FileString buildPath = packageDir + TINYDIR_STRING("/sibs-build/");
        switch (sibsConfig.getOptimizationLevel())
        {
            case OPT_LEV_DEBUG:
            {
                buildPath += TINYDIR_STRING("debug");
                // TODO: Check if this dependency is static or dynamic and decide which lib path to use from that
                for(const string &staticLib : sibsConfig.getDebugStaticLibs())
                {
                    string staticLibCmd = "\"";
                    staticLibCmd += staticLib;
                    staticLibCmd += "\"";
                    staticLinkerFlagCallbackFunc(staticLibCmd);
                }
                break;
            }
            case OPT_LEV_RELEASE:
            {
                buildPath += TINYDIR_STRING("release");
                // TODO: Check if this dependency is static or dynamic and decide which lib path to use from that
                for (const string &staticLib : sibsConfig.getReleaseStaticLibs())
                {
                    string staticLibCmd = "\"";
                    staticLibCmd += staticLib;
                    staticLibCmd += "\"";
                    staticLinkerFlagCallbackFunc(staticLibCmd);
                }
                break;
            }
        }

        if (!ninja.getSourceFiles().empty())
        {
            string libPath = toUtf8(buildPath);
            switch (sibsConfig.getCompiler())
            {
                case Compiler::GCC:
                {
                    libPath += "/lib";
                    libPath += name;
                    if (sibsConfig.getPackageType() == PackageType::STATIC)
                    {
                        libPath += ".a";
                        string libPathCmd = "'";
                        libPathCmd += libPath;
                        libPathCmd += "'";
                        staticLinkerFlagCallbackFunc(libPathCmd);
                    }
                    else
                    {
                        libPath += ".so";
                        string libPathCmd = "'";
                        libPathCmd += libPath;
                        libPathCmd += "'";
                        dynamicLinkerFlagCallbackFunc(libPathCmd);
                    }
                    break;
                }
                case Compiler::MSVC:
                {
                    libPath += "/";
                    libPath += name;
                    if (sibsConfig.getPackageType() == PackageType::STATIC)
                    {
                        libPath += ".lib";
                        string libPathCmd = "\"";
                        libPathCmd += libPath;
                        libPathCmd += "\"";
                        staticLinkerFlagCallbackFunc(libPathCmd);
                    }
                    else
                    {
                        libPath += ".lib";
                        string libPathCmd = "\"";
                        libPathCmd += libPath;
                        libPathCmd += "\"";
                        dynamicLinkerFlagCallbackFunc(libPathCmd);
                    }
                    break;
                }
            }
        }

        return ninja.build(sibsConfig, buildPath.c_str(), staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallbackFunc, globalIncludeDirCallback);
    }

    Result<bool> GlobalLib::downloadDependency(const Dependency &dependency)
    {
        string url = "https://github.com/DEC05EBA/";
        url += dependency.name;
        url += "/archive/";
        url += dependency.version;
        url += ".tar.gz";

        Result<FileString> libPathResult = getHomeDir();
        if (!libPathResult)
            return Result<bool>::Err(libPathResult);
        FileString libPath = libPathResult.unwrap();
        libPath += TINYDIR_STRING("/.sibs/lib/");
        libPath += toFileString(dependency.name);
        libPath += TINYDIR_STRING("/");
        libPath += toFileString(dependency.version);

        FileString libArchivedFilePath = libPathResult.unwrap();
        libArchivedFilePath += TINYDIR_STRING("/.sibs/archive/");
        libArchivedFilePath += toFileString(dependency.name);
        Result<bool> createArchiveDirResult = createDirectoryRecursive(libArchivedFilePath.c_str());
        if(createArchiveDirResult.isErr())
            return createArchiveDirResult;

        libArchivedFilePath += TINYDIR_STRING("/");
        libArchivedFilePath += toFileString(dependency.version);
        Result<bool> downloadResult = curl::downloadFile(url.c_str(), libArchivedFilePath.c_str());
        if(downloadResult.isErr())
            return downloadResult;

        // Create build path. This is done here because we dont want to create it if download fails
        Result<bool> createLibDirResult = createDirectoryRecursive(libPath.c_str());
        if(createLibDirResult.isErr())
            return createLibDirResult;

        return Archive::extract(libArchivedFilePath.c_str(), libPath.c_str());
    }
}