/*Copyright (C) 2010-2023 Duncan Deveaux

This file is part of Hikou no mizu.

Hikou no mizu is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Hikou no mizu 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with Hikou no mizu.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "I18n.hpp"

#include "Tools/Log.hpp"
#include "Tools/Format.hpp"
#include "Tools/Utf8.hpp"
#include "Tools/BuildValues.hpp" ///< Generated at build time
#include <pugixml.hpp>
#include <algorithm>
#include <array>
#include <map>
#include <set>
#include <stdexcept>
#include <cassert>

/// Platform-specific user language detection code
#if defined(__HAIKU__)
    #include <Locale.h>
#elif defined(_WIN32)
    #include <winnls.h>
#elif defined(__APPLE__)
    #include <CoreFoundation/CoreFoundation.h>
#elif defined(__unix__)
    #include <cstdlib>
#endif

namespace
{
    std::array<std::string, I18n::EntrySize> makeLabels()
    {
        std::array<std::string, I18n::EntrySize> labels;
        labels[I18n::MenuPlay]                 = "menu_play";
        labels[I18n::MenuVersus]               = "menu_versus";
        labels[I18n::MenuMultiplayer]          = "menu_multiplayer";
        labels[I18n::MenuOptions]              = "menu_options";
        labels[I18n::MenuExit]                 = "menu_exit";
        labels[I18n::Menu2Players]             = "menu_2players";
        labels[I18n::Menu3Players]             = "menu_3players";
        labels[I18n::Menu4Players]             = "menu_4players";
        labels[I18n::Menu5Players]             = "menu_5players";
        labels[I18n::Menu6Players]             = "menu_6players";
        labels[I18n::MenuSingleRound]          = "menu_singleround";
        labels[I18n::Menu2Rounds]              = "menu_2rounds";
        labels[I18n::Menu3Rounds]              = "menu_3rounds";
        labels[I18n::Menu4Rounds]              = "menu_4rounds";
        labels[I18n::Menu5Rounds]              = "menu_5rounds";
        labels[I18n::HeaderBack]               = "header_back";
        labels[I18n::CharacterSelectionTitle]  = "character_selection_title";
        labels[I18n::CharacterSelectionSubtitle] =
            "character_selection_subtitle";
        labels[I18n::CharacterSelectionNoName] = "character_selection_no_name";
        labels[I18n::CharacterSelectionAIName] =
            "character_selection_ai_name:{name}";
        labels[I18n::ArenaSelectionTitle]      = "arena_selection_title";
        labels[I18n::ArenaSelectionSubtitle]   = "arena_selection_subtitle";
        labels[I18n::ArenaSelectionValidate]   = "arena_selection_validate";
        labels[I18n::ClientLobbyTitle]         = "client_lobby_title";
        labels[I18n::ClientLobbySubtitle]      = "client_lobby_subtitle";
        labels[I18n::ClientLobbyAddress]       = "client_lobby_address";
        labels[I18n::ClientLobbyPort]          = "client_lobby_port";
        labels[I18n::ClientLobbyUsername]      = "client_lobby_username";
        labels[I18n::ClientLobbyConnect]       = "client_lobby_connect";
        labels[I18n::ClientLobbyConnecting]    = "client_lobby_connecting";
        labels[I18n::ClientLobbyUnresolvable]  = "client_lobby_unresolvable";
        labels[I18n::DialogOK]                 = "dialog_ok";
        labels[I18n::DialogCancel]             = "dialog_cancel";
        labels[I18n::KickedConnectionLost]     = "kicked_connection_lost";
        labels[I18n::KickedVersionMismatch]    = "kicked_version_mismatch";
        labels[I18n::KickedServerIsFull]       = "kicked_server_is_full";
        labels[I18n::KickedServerNoCharacter]  = "kicked_server_no_character";
        labels[I18n::KickedInactivity]         = "kicked_inactivity";
        labels[I18n::KickedIllegalCommand]     = "kicked_illegal_command";
        labels[I18n::KickedUnintelligibleByServer] =
            "kicked_unintelligible_by_server";
        labels[I18n::KickedUnintelligibleByClient] =
            "kicked_unintelligible_by_client";
        labels[I18n::KickedConnectionTimeout]  = "kicked_connection_timeout";
        labels[I18n::KickedClientNoArena]      = "kicked_client_no_arena";
        labels[I18n::KickedClientNoCharacter]  = "kicked_client_no_character";
        labels[I18n::KickedClientDecision]     = "kicked_client_decision";
        labels[I18n::KickedENetError]          = "kicked_enet_error";
        labels[I18n::KickedUnknownReason]      = "kicked_unknown_reason";
        labels[I18n::OptionsTitle]             = "options_title";
        labels[I18n::OptionsSubtitle]          = "options_subtitle";
        labels[I18n::OptionsGeneral]           = "options_general";
        labels[I18n::OptionsControls]          = "options_controls";
        labels[I18n::OptionsDisplay]           = "options_display";
        labels[I18n::OptionsRefreshRate]       = "options_refresh_rate";
        labels[I18n::OptionsAudio]             = "options_audio";
        labels[I18n::OptionsLocale]            = "options_locale";
        labels[I18n::OptionsMiscellaneous]     = "options_miscellaneous";
        labels[I18n::OptionsDisplayMode]       = "options_display_mode";
        labels[I18n::OptionsDisplayModeFullscreenBorderless] =
            "options_display_mode_fullscreen_borderless";
        labels[I18n::OptionsDisplayModeFullscreenNative] =
            "options_display_mode_fullscreen_native";
        labels[I18n::OptionsDisplayModeWindowed] =
            "options_display_mode_windowed";
        labels[I18n::OptionsDisplayResolution] = "options_display_resolution";
        labels[I18n::OptionsDisplayRevertToDefault]
            = "options_display_revert_to_default";
        labels[I18n::OptionsDisplayApply] = "options_display_apply";
        labels[I18n::OptionsDisplayAppliedAtNextRestart] =
            "options_display_applied_at_next_restart";
        labels[I18n::OptionsRefreshRateVerticalSync]
            = "options_refresh_rate_vertical_sync";
        labels[I18n::OptionsRefreshRateFramesPerSecond]
            = "options_refresh_rate_frames_per_second";
        labels[I18n::OptionsAudioMaster]       = "options_audio_master";
        labels[I18n::OptionsAudioMusic]        = "options_audio_music";
        labels[I18n::OptionsAudioAmbient]      = "options_audio_ambient";
        labels[I18n::OptionsAudioSoundEffects] = "options_audio_sound_effects";
        labels[I18n::OptionsLocaleLanguage]    = "options_locale_language";
        labels[I18n::OptionsDynamicCamera]     = "options_dynamic_camera";
        labels[I18n::OptionsShakingCamera]     = "options_shaking_camera";
        labels[I18n::OptionsShowFrameRate]     = "options_show_frame_rate";
        labels[I18n::OptionsShowHitboxes]      = "options_show_hitboxes";
        labels[I18n::OptionsControlsBinderID] =
            "options_controls_binder_id:{binder_id}";
        labels[I18n::OptionsControlsActiveDevice] =
            "options_controls_active_device";
        labels[I18n::OptionsControlsResetJoystick] =
            "options_controls_reset_joystick";
        labels[I18n::OptionsControlsKeyboard]  = "options_controls_keyboard";
        labels[I18n::OptionsControlsPunch]     = "options_controls_punch";
        labels[I18n::OptionsControlsKick]      = "options_controls_kick";
        labels[I18n::OptionsControlsThrow]     = "options_controls_throw";
        labels[I18n::OptionsControlsJump]      = "options_controls_jump";
        labels[I18n::OptionsControlsMoveLeft]  = "options_controls_move_left";
        labels[I18n::OptionsControlsMoveRight] = "options_controls_move_right";
        labels[I18n::OptionsControlsCrouch]    = "options_controls_crouch";
        labels[I18n::KeyJoyButton]             = "key_joy_button:{button_id}";
        labels[I18n::KeyJoyAxisNegative] =
            "key_joy_axis_negative:{axis_id}";
        labels[I18n::KeyJoyAxisPositive] =
            "key_joy_axis_positive:{axis_id}";
        labels[I18n::KeyJoyHatUp]              = "key_joy_hat_up:{hat_id}";
        labels[I18n::KeyJoyHatDown]            = "key_joy_hat_down:{hat_id}";
        labels[I18n::KeyJoyHatLeft]            = "key_joy_hat_left:{hat_id}";
        labels[I18n::KeyJoyHatRight]           = "key_joy_hat_right:{hat_id}";
        labels[I18n::KeyArrowUp]               = "key_arrow_up";
        labels[I18n::KeyArrowDown]             = "key_arrow_down";
        labels[I18n::KeyArrowLeft]             = "key_arrow_left";
        labels[I18n::KeyArrowRight]            = "key_arrow_right";
        labels[I18n::KeySpacebar]              = "key_spacebar";
        labels[I18n::KeyBackspace]             = "key_backspace";
        labels[I18n::KeyReturn]                = "key_return";
        labels[I18n::FpsCounterValue]          = "fps_counter_value:{fps}";
        labels[I18n::RoundAnnouncerReady]      = "round_announcer_ready";
        labels[I18n::RoundAnnouncerNumberedRound] =
            "round_announcer_numbered_round:{round_id}";
        labels[I18n::RoundAnnouncerFinalRound] = "round_announcer_final_round";
        labels[I18n::RoundAnnouncerFight]      = "round_announcer_fight";
        labels[I18n::RoundResultDrawGame]      = "round_result_draw_game";
        labels[I18n::RoundResultNetworkedWinner] =
            "round_result_networked_winner:{username}";
        labels[I18n::RoundResultLocalWinner] =
            "round_result_local_winner:{name},{player_id}";
        labels[I18n::LocalGamePauseTitle]      = "local_game_pause_title";
        labels[I18n::LocalGamePauseRestart]    = "local_game_pause_restart";
        labels[I18n::LocalGameEndReplay]       = "local_game_end_replay";
        labels[I18n::NetworkedGamePauseTitle]  = "networked_game_pause_title";
        labels[I18n::NetworkedGamePauseDisconnect] =
            "networked_game_pause_disconnect";
        labels[I18n::PauseResume]              = "pause_resume";
        labels[I18n::PauseToMainMenu]          = "pause_to_main_menu";
        labels[I18n::PeerInfoCharacter]        = "peer_info_character";
        labels[I18n::PeerInfoUsername]         = "peer_info_username";
        labels[I18n::PeerInfoPing]             = "peer_info_ping";
        labels[I18n::PeerInfoSpectators]       = "peer_info_spectators";
        labels[I18n::ServerEventGameScheduled] = "server_event_game_scheduled";
        labels[I18n::ServerEventMatchmakingStack] =
            "server_event_matchmaking_stack";
        labels[I18n::ServerEventMatchmakingRandom] =
            "server_event_matchmaking_random";
        labels[I18n::ServerEventMatchmakingChallenger] =
            "server_event_matchmaking_challenger";
        labels[I18n::ServerEventPeerJoined] = "server_event_peer_joined:{peer}";
        labels[I18n::ServerEventPeerLeft]   = "server_event_peer_left:{peer}";
        labels[I18n::ServerEventPeerKickedForInactivity] =
            "server_event_peer_kicked_for_inactivity:{peer}";
        labels[I18n::ServerEventRulesVersus] =
            "server_event_rules_versus:{player_count},{rounds_to_win}";
        labels[I18n::ServerEventRulesDefault] =
            "server_event_rules_default:{player_count},{rounds_to_win}";

        return labels;
    }

    const std::array<std::string, I18n::EntrySize> labels = makeLabels();

    /// Make a label->index map to efficiently find the index of a label
    std::map<std::string, std::size_t> makeLabelsMap()
    {
        std::map<std::string, std::size_t> labelsToIndices;
        for (std::size_t i = 0; i < labels.size(); i++)
            labelsToIndices.emplace(labels[i], i);

        return labelsToIndices;
    }

    const std::map<std::string, std::size_t> labelsToIndices = makeLabelsMap();

    /// Returns the index of the entry of label \p label
    /// Returns I18n::EntrySize if \p label is not a valid entry label
    std::size_t getIndexForLabel(const std::string &label)
    {
        const std::map<std::string, std::size_t>::const_iterator it =
            labelsToIndices.find(label);

        return (it != labelsToIndices.end()) ? it->second : I18n::EntrySize;
    }

    /// Wrapper containing the set of possible translations for each entry
    /// A single entry can feature an arbitrary number of translation
    /// variations based on the specific placeholder inputs that were provided
    class Translation
    {
        /// Associates a translation context
        ///  (list of placeholder names and their specific values)
        /// with a translation string
        ///  (containing the formatting placeholders)
        using ContextualTranslation =
            std::pair<I18n::TranslationContext, std::string>;

        public:
            Translation() = default;

            Translation(const std::string &translation) :
            m_defaultTranslation(translation) {}

            /// Add a translation for a specific non-empty context,
            /// if not already existing
            bool addForContext(const I18n::TranslationContext &context,
                               const std::string &translation)
            {
                if (context.empty())
                    return false;

                for (const ContextualTranslation &translation :
                     m_contextualTranslations)
                {
                    if (translation.first == context)
                        return false;
                }

                m_contextualTranslations.emplace_back(context, translation);
                return true;
            }

            /// Returns the translation for a given context
            /// Returns the default translation as a fallback
            const std::string &get(
                const I18n::TranslationContext &context) const
            {
                if (context.empty()) //Shortcut if no context was provided
                    return m_defaultTranslation;

                //Return the translation for the closest known context
                std::size_t matchesCount = 0, rulesCount = 0;
                const std::string *translation = &m_defaultTranslation;

                for (const ContextualTranslation &option :
                     m_contextualTranslations)
                {
                    const std::size_t matches = option.first.matches(context);
                    const std::size_t rules = option.first.size();

                    //Found more matches or a more precise match
                    //(less unmatched rules)
                    if (matches > matchesCount ||
                        (matches == matchesCount && rules < rulesCount))
                    {
                        translation = &(option.second);
                        matchesCount = matches;
                        rulesCount = rules;

                        //Found an optimal match, return immediately
                        if (matchesCount == context.size())
                            return (*translation);
                    }
                }

                return (*translation);
            }

        private:
            std::string m_defaultTranslation;
            std::vector<ContextualTranslation> m_contextualTranslations;
    };

    /// Returns the described translation context (set of placeholder
    /// name and specific associated values) that was encoded as attributes
    /// of an xml node
    I18n::TranslationContext xmlEntryContext(const pugi::xml_node &node)
    {
        I18n::TranslationContext context;
        for (pugi::xml_attribute attribute = node.first_attribute();
             attribute;
             attribute = attribute.next_attribute())
        {
            if (std::string(attribute.name()) != "key")
                context.addNamedValue(attribute.name(), attribute.value());
        }

        return context;
    }

    /// The default entries are the key labels
    std::array<Translation, I18n::EntrySize> makeDefaultEntries()
    {
        std::array<Translation, I18n::EntrySize> defaultEntries;
        for (std::size_t i = 0; i < defaultEntries.size(); i++)
            defaultEntries[i] = labels[i];

        return defaultEntries;
    }

    std::array<Translation, I18n::EntrySize> entries = makeDefaultEntries();

    /// Load the supported locales from locale/locale.xml
    std::vector<I18n::Locale> makeLocales()
    {
        pugi::xml_document xmlFile;
        const pugi::xml_parse_result result =
            xmlFile.load_file(BuildValues::locale("locale").c_str());

        if (!result)
        {
            Log::err(Format::format("Could not load the locale list: {}",
                                    result.description()));
            return {};
        }

        std::vector<I18n::Locale> locales;
        pugi::xml_node localeNode;
        for (localeNode = xmlFile.child("main").child("locale");
             localeNode;
             localeNode = localeNode.next_sibling("locale"))
        {
            std::vector<std::string> coveredCodes;
            pugi::xml_node coversNode;
            for (coversNode = localeNode.child("covers");
                 coversNode;
                 coversNode = coversNode.next_sibling("covers"))
            {
                coveredCodes.emplace_back(coversNode.attribute("code").value());
            }

            locales.push_back({localeNode.attribute("code").value(),
                               localeNode.attribute("name").value(),
                               coveredCodes});
        }

        return locales;
    }

    std::vector<I18n::Locale> knownLocales = makeLocales();
}

std::string I18n::findLocale(const std::string &localeCode)
{
    for (const I18n::Locale &localeData : knownLocales)
    {
        if (localeData.code == localeCode)
            return localeData.code;
        else if (std::find(localeData.covers.begin(),
                           localeData.covers.end(),
                           localeCode) != localeData.covers.end())
        {
            Log::info(Format::format("Using locale '{}' to cover language '{}'",
                                     localeData.code, localeCode));
            return localeData.code;
        }
    }

    Log::err(Format::format("Locale '{}' is not supported, "
                            "falling back to 'en'", localeCode));
    return "en";
}

bool I18n::loadEntries(const std::string &localeName)
{
    pugi::xml_document xmlFile;
    const pugi::xml_parse_result result =
        xmlFile.load_file(BuildValues::locale(localeName).c_str());

    if (!result)
    {
        Log::err(Format::format("Could not load the locale '{}': {}",
                                localeName, result.description()));
        return false;
    }

    //Reset entries to default
    entries = makeDefaultEntries();

    //Keep track of loaded entries for log statistics
    std::set<std::size_t> loaded;

    pugi::xml_node entryNode;
    for (entryNode = xmlFile.child("locale").child("entry");
         entryNode;
         entryNode = entryNode.next_sibling("entry"))
    {
        const std::string key = entryNode.attribute("key").value();
        const std::size_t ix = getIndexForLabel(key);

        if (ix >= entries.size())
        {
            Log::err(Format::format("Ignored unknown locale entry '{}'", key));
            continue;
        }

        const TranslationContext context = xmlEntryContext(entryNode);
        const std::string translation =
            Utf8::utf8_string(entryNode.child_value()).get();

        if (loaded.insert(ix).second) //First insertion of ix
        {
            entries[ix] = translation;
            if (!context.empty())
                Log::err(Format::format("Ignored contextual "
                         "parameters for local entry '{}'\nThe default "
                         "translation must be provided first", key));
        }
        else if (!context.empty())
        {
            if (!entries[ix].addForContext(context, translation))
                Log::err(Format::format("Ignored contextual duplicate "
                                        "for locale entry '{}'", key));
        }
        else //Default translation already known
            Log::err(Format::format("Ignoring duplicate for locale "
                                    "entry '{}'", key));
    }

    Log::info(Format::format("Loaded {} entries from locale '{}' "
                             "({} missing)", loaded.size(), localeName,
                             entries.size() - loaded.size()));
    return true;
}

std::string I18n::translate(std::size_t index,
                            const TranslationContext &context)
{
    assert(index < entries.size());
    return entries[index].get(context);
}


const std::vector<I18n::Locale> &I18n::getLocales()
{
    return knownLocales;
}

#ifdef __unix__
namespace
{
    /// For unix systems, attempts to read the user language from the
    /// environment variable \p envName into \p dstLang
    bool getLangFromEnv(const char *envName, std::string &dstLang)
    {
        if (const char *language = std::getenv(envName))
        {
            const std::string localeName(language);
            if (localeName.size() >= 2)
            {
                dstLang = localeName.substr(0, 2);
                return true;
            }
            else
                Log::err(Format::format("Ignoring {}={}, locale code is too "
                                        "short", envName, localeName));
        }

        return false;
    }
}
#endif

std::string I18n::getUserLocale()
{
    #if defined(__HAIKU__)
        BLocale locale;
        BLanguage language;
        if (locale.GetLanguage(&language) == B_OK)
            return language.Code();
        else
            Log::err("Cannot read user locale");

    #elif defined(_WIN32)
        wchar_t buffer[LOCALE_NAME_MAX_LENGTH];
        if (GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH) > 0)
        {
            //The language code is probably ascii, right?
            const std::wstring wlocale(buffer);
            const std::string localeName(wlocale.begin(), wlocale.end());

            if (localeName.size() >= 2)
                return localeName.substr(0, 2);
            else
                Log::err(Format::format("Detected user locale '{}' is too "
                                        "short", localeName));
        }
        else
            Log::err("Cannot read user locale");

    #elif defined(__APPLE__)
        const CFLocaleRef locale = CFLocaleCopyCurrent();
        if (locale != nullptr)
        {
            const CFStringRef localeRef = static_cast<CFStringRef>(
                CFLocaleGetValue(locale, kCFLocaleLanguageCode));
            CFRelease(locale);

            if (localeRef != nullptr)
            {
                const CFIndex localeLength =
                    CFStringGetMaximumSizeForEncoding(
                        CFStringGetLength(localeRef),
                        kCFStringEncodingUTF8) + 1;

                char *buffer = new char[localeLength];
                if (CFStringGetCString(localeRef, buffer, localeLength,
                                       kCFStringEncodingUTF8))
                {
                    std::string localeName(buffer);
                    delete[] buffer;
                    return localeName;
                }
                else
                    delete[] buffer;
            }
        }

        Log::err("Cannot read user locale");

    #elif defined(__unix__)
        std::string language;
        if (getLangFromEnv("LANGUAGE", language)    ||
            getLangFromEnv("LC_ALL", language)      ||
            getLangFromEnv("LC_MESSAGES", language) ||
            getLangFromEnv("LANG", language))
            return language;

        Log::err("Cannot read user locale");
    #endif

    return "en"; //Default language code if unsuccessful
}
