#include "l10n.hpp"

#include <unicode/errorcode.h>

#include <components/debug/debuglog.hpp>
#include <components/vfs/manager.hpp>

namespace sol
{
    template <>
    struct is_automagical<LuaUtil::L10nManager::Context> : std::false_type {};
}

namespace LuaUtil
{
    void L10nManager::init()
    {
        sol::usertype<Context> ctx = mLua->sol().new_usertype<Context>("L10nContext");
        ctx[sol::meta_function::call] = &Context::translate;
    }

    std::string L10nManager::translate(const std::string& contextName, const std::string& key)
    {
        Context& ctx = getContext(contextName).as<Context>();
        return ctx.translate(key, sol::nil);
    }

    void L10nManager::setPreferredLocales(const std::vector<std::string>& langs)
    {
        mPreferredLocales.clear();
        for (const auto &lang : langs)
            mPreferredLocales.push_back(icu::Locale(lang.c_str()));
        {
            Log msg(Debug::Info);
            msg << "Preferred locales:";
            for (const icu::Locale& l : mPreferredLocales)
                msg << " " << l.getName();
        }
        for (auto& [_, context] : mContexts)
            context.updateLang(this);
    }

    void L10nManager::Context::readLangData(L10nManager* manager, const icu::Locale& lang)
    {
        std::string path = "l10n/";
        path.append(mName);
        path.append("/");
        path.append(lang.getName());
        path.append(".yaml");
        if (!manager->mVFS->exists(path))
            return;

        mMessageBundles->load(*manager->mVFS->get(path), lang, path);
    }

    std::pair<std::vector<icu::Formattable>, std::vector<icu::UnicodeString>> getICUArgs(std::string_view messageId, const sol::table &table)
    {
        std::vector<icu::Formattable> args;
        std::vector<icu::UnicodeString> argNames;
        for (auto& [key, value] : table)
        {
            // Argument values
            if (value.is<std::string>())
                args.push_back(icu::Formattable(LuaUtil::cast<std::string>(value).c_str()));
            // Note: While we pass all numbers as doubles, they still seem to be handled appropriately.
            // Numbers can be forced to be integers using the argType number and argStyle integer
            //     E.g. {var, number, integer}
            else if (value.is<double>())
                args.push_back(icu::Formattable(LuaUtil::cast<double>(value)));
            else
            {
                Log(Debug::Error) << "Unrecognized argument type for key \"" << LuaUtil::cast<std::string>(key)
                                  << "\" when formatting message \"" << messageId << "\"";
            }

            // Argument names
            const auto str = LuaUtil::cast<std::string>(key);
            argNames.push_back(icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size())));
        }
        return std::make_pair(args, argNames);
    }

    std::string L10nManager::Context::translate(std::string_view key, const sol::object& data)
    {
        std::vector<icu::Formattable> args;
        std::vector<icu::UnicodeString> argNames;

        if (data.is<sol::table>()) {
            sol::table dataTable = data.as<sol::table>();
            auto argData = getICUArgs(key, dataTable);
            args = argData.first;
            argNames = argData.second;
        }

        return mMessageBundles->formatMessage(key, argNames, args);
    }

    void L10nManager::Context::updateLang(L10nManager* manager)
    {
        icu::Locale fallbackLocale = mMessageBundles->getFallbackLocale();
        mMessageBundles->setPreferredLocales(manager->mPreferredLocales);
        int localeCount = 0;
        bool fallbackLocaleInPreferred = false;
        for (const icu::Locale& loc: mMessageBundles->getPreferredLocales())
        {
            if (!mMessageBundles->isLoaded(loc))
                readLangData(manager, loc);
            if (mMessageBundles->isLoaded(loc))
            {
                localeCount++;
                Log(Debug::Verbose) << "Language file \"l10n/" << mName << "/" << loc.getName() << ".yaml\" is enabled";
                if (loc == fallbackLocale)
                    fallbackLocaleInPreferred = true;
            }
        }
        if (!mMessageBundles->isLoaded(fallbackLocale))
            readLangData(manager, fallbackLocale);
        if (mMessageBundles->isLoaded(fallbackLocale) && !fallbackLocaleInPreferred)
            Log(Debug::Verbose) << "Fallback language file \"l10n/" << mName << "/" << fallbackLocale.getName() << ".yaml\" is enabled";

        if (localeCount == 0)
        {
            Log(Debug::Warning) << "No language files for the preferred languages found in \"l10n/" << mName << "\"";
        }
    }

    sol::object L10nManager::getContext(const std::string& contextName, const std::string& fallbackLocaleName)
    {
        auto it = mContexts.find(contextName);
        if (it != mContexts.end())
            return sol::make_object(mLua->sol(), it->second);
        auto allowedChar = [](char c)
        {
            return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
                   (c >= '0' && c <= '9') || c == '_';
        };
        bool valid = !contextName.empty();
        for (char c : contextName)
            valid = valid && allowedChar(c);
        if (!valid)
            throw std::runtime_error(std::string("Invalid l10n context name: ") + contextName);
        icu::Locale fallbackLocale(fallbackLocaleName.c_str());
        Context ctx{contextName, std::make_shared<l10n::MessageBundles>(mPreferredLocales, fallbackLocale)};
        {
            Log msg(Debug::Verbose);
            msg << "Fallback locale: " << fallbackLocale.getName();
        }
        ctx.updateLang(this);
        mContexts.emplace(contextName, ctx);
        return sol::make_object(mLua->sol(), ctx);
    }

}
