#include "il2cpp-config.h"

#if IL2CPP_TARGET_POSIX && !IL2CPP_USE_PLATFORM_SPECIFIC_LIBRARYLOADER

#include <dlfcn.h>
#include <string>
#include <set>
#if IL2CPP_TARGET_LINUX
#include <unistd.h>
#include <gnu/lib-names.h>
#endif

#include "il2cpp-runtime-metadata.h"
#include "os/LibraryLoader.h"
#include "os/Path.h"
#include "utils/PathUtils.h"
#include "utils/StringUtils.h"
#include "utils/Environment.h"

namespace il2cpp
{
namespace os
{
    struct LibraryNamePrefixAndSuffix
    {
        LibraryNamePrefixAndSuffix(const char* prefix_, const char* suffix_)
        {
            prefix = std::string(prefix_);
            suffix = std::string(suffix_);
        }

        std::string prefix;
        std::string suffix;
    };

    static LibraryNamePrefixAndSuffix LibraryNamePrefixAndSuffixVariations[8] =
    {
        LibraryNamePrefixAndSuffix("", ".so"),
        LibraryNamePrefixAndSuffix("", ".dll"),
        LibraryNamePrefixAndSuffix("", ".dylib"),
        LibraryNamePrefixAndSuffix("", ".bundle"),
        LibraryNamePrefixAndSuffix("lib", ".so"),
        LibraryNamePrefixAndSuffix("lib", ".dll"),
        LibraryNamePrefixAndSuffix("lib", ".dylib"),
        LibraryNamePrefixAndSuffix("lib", ".bundle")
    };

    const HardcodedPInvokeDependencyLibrary* LibraryLoader::HardcodedPInvokeDependencies = NULL;
    const size_t LibraryLoader::HardcodedPInvokeDependenciesCount = 0;

    static Baselib_DynamicLibrary_Handle LoadLibraryWithName(const char* name, std::string& detailedError)
    {
#if IL2CPP_TARGET_IOS
        std::string dirName;
        if (utils::Environment::GetNumMainArgs() > 0)
        {
            std::string main = utils::StringUtils::Utf16ToUtf8(utils::Environment::GetMainArgs()[0]);
            dirName = utils::PathUtils::DirectoryName(main);
        }

        std::string libPath = utils::StringUtils::Printf("%s/%s", dirName.c_str(), name);
        auto errorState = Baselib_ErrorState_Create();
        auto handle = LibraryLoader::TryOpeningLibrary(libPath.c_str(), detailedError);
        if (handle != Baselib_DynamicLibrary_Handle_Invalid)
            return handle;

        // Fallback to just using the name. This might be a system dylib.
        return LibraryLoader::TryOpeningLibrary(name, detailedError);
#else
        return LibraryLoader::TryOpeningLibrary(name, detailedError);
#endif
    }

#if IL2CPP_TARGET_LINUX
    static Baselib_DynamicLibrary_Handle LoadLibraryRelativeToExecutableDirectory(const char* name, std::string& detailedError)
    {
        if (name == NULL || name[0] == '/')
            return Baselib_DynamicLibrary_Handle_Invalid;

        char exePath[PATH_MAX + 1];
        int len;

        if ((len = readlink("/proc/self/exe", exePath, sizeof(exePath))) == -1)
        {
            return Baselib_DynamicLibrary_Handle_Invalid;
        }
        exePath[len] = '\0'; // readlink does not terminate buffer
        while (len > 0 && exePath[len] != '/')
            len--;
        exePath[len] = '\0';
        std::string libPath = utils::StringUtils::Printf("%s/%s", exePath, name);

        return LibraryLoader::TryOpeningLibrary(libPath.c_str(), detailedError);
    }

#endif

    static Baselib_DynamicLibrary_Handle CheckLibraryVariations(const std::string& name, std::string& detailedError)
    {
        const int numberOfVariations = sizeof(LibraryNamePrefixAndSuffixVariations) / sizeof(LibraryNamePrefixAndSuffixVariations[0]);
        for (int i = 0; i < numberOfVariations; ++i)
        {
            std::string libraryName = LibraryNamePrefixAndSuffixVariations[i].prefix + name + LibraryNamePrefixAndSuffixVariations[i].suffix;
            auto handle = LoadLibraryWithName(libraryName.c_str(), detailedError);
            if (handle != Baselib_DynamicLibrary_Handle_Invalid)
                return handle;
#if IL2CPP_TARGET_LINUX
            // Linux does not search current directory by default
            handle = LoadLibraryRelativeToExecutableDirectory(libraryName.c_str(), detailedError);
            if (handle != Baselib_DynamicLibrary_Handle_Invalid)
                return handle;
#endif
        }

        return Baselib_DynamicLibrary_Handle_Invalid;
    }

    static Baselib_DynamicLibrary_Handle ProbeForLibraryImpl(const std::string& libraryName, std::string& detailedError)
    {
        auto handle = Baselib_DynamicLibrary_Handle_Invalid;

#if IL2CPP_TARGET_LINUX
        // Workaround the fact that on Linux, libc is actually named libc.so.6 instead of libc.so.
        // mscorlib P/Invokes into plain libc, so we need this for those P/Invokes to succeed
        if (strncasecmp(libraryName.c_str(), "libc", libraryName.size()) == 0)
            handle = LoadLibraryWithName(LIBC_SO, detailedError);
#endif

        if (handle == Baselib_DynamicLibrary_Handle_Invalid)
            handle = LoadLibraryWithName(libraryName.c_str(), detailedError);

        if (handle == Baselib_DynamicLibrary_Handle_Invalid)
            handle = CheckLibraryVariations(libraryName.c_str(), detailedError);

        if (handle == Baselib_DynamicLibrary_Handle_Invalid)
        {
            const size_t lengthWithoutDotDll = libraryName.size() - 4;
            if (strncmp(libraryName.c_str() + lengthWithoutDotDll, ".dll", 4) == 0)
            {
                char* nativeDynamicLibraryWithoutExtension = static_cast<char*>(alloca((lengthWithoutDotDll + 1) * sizeof(char)));
                memcpy(nativeDynamicLibraryWithoutExtension, libraryName.c_str(), lengthWithoutDotDll);
                nativeDynamicLibraryWithoutExtension[lengthWithoutDotDll] = 0;

                handle = CheckLibraryVariations(nativeDynamicLibraryWithoutExtension, detailedError);
            }
        }

        return handle;
    }

    Baselib_DynamicLibrary_Handle LibraryLoader::ProbeForLibrary(const Il2CppNativeChar* libraryName, const size_t libraryNameLength, std::string& detailedError)
    {
        std::string libraryNameStr = utils::StringUtils::NativeStringToUtf8(libraryName, libraryNameLength);

        // Probe the application directory first - this matches the expected P/Invoke behavior from Win32
        std::string applicationDir = os::Path::GetApplicationFolder();
        std::string libraryPath = utils::PathUtils::Combine(applicationDir, libraryNameStr);
        Baselib_DynamicLibrary_Handle handle = ProbeForLibraryImpl(libraryPath, detailedError);

        // Otherwise use the OS's search paths
        if (handle == Baselib_DynamicLibrary_Handle_Invalid)
            handle = ProbeForLibraryImpl(libraryNameStr, detailedError);

        return handle;
    }

    Baselib_DynamicLibrary_Handle LibraryLoader::OpenProgramHandle(Baselib_ErrorState& errorState, bool& /*needsClosing*/)
    {
        return Baselib_DynamicLibrary_OpenProgramHandle(&errorState);
    }
}
}

#endif
