#define LOCALIZATIONMAINT_CPP
#include "Localization.h"
#include "LocalizationStorage.h"

#include <tinyxml2.h>

#include "Alloc.h"
#include "FileSystemUtils.h"
#include "Font.h"
#include "Graphics.h"
#include "Script.h"
#include "Vlogging.h"
#include "XMLUtils.h"


namespace loc
{

static void write_max_local(tinyxml2::XMLElement* pElem, uint8_t glyph_w, uint8_t glyph_h)
{
    const char* max;
    if ((max = pElem->Attribute("max")) != NULL)
    {
        unsigned short max_w, max_h, max_local_w, max_local_h;
        if (parse_max(max, &max_w, &max_h))
        {
            max_local_w = (max_w*8) / glyph_w;
            max_local_h = (max_h*10) / SDL_max(10, glyph_h);

            if (max_local_h == 0)
            {
                max_local_h = 1;
            }

            char buf[16];
            if (max_h == 1)
            {
                SDL_snprintf(buf, sizeof(buf), "%d", max_local_w);
            }
            else
            {
                SDL_snprintf(buf, sizeof(buf), "%d*%d", max_local_w, max_local_h);
            }
            pElem->SetAttribute("max_local", buf);
        }
    }
}

static void write_max_local_decl(tinyxml2::XMLDocument* doc, uint8_t glyph_w, uint8_t glyph_h)
{
    char buf[16];
    SDL_snprintf(buf, sizeof(buf), "%dx%d", glyph_w, glyph_h);
    doc->FirstChildElement()->SetAttribute("max_local_for", buf);
}

static void sync_lang_file(const std::string& langcode)
{
    /* Update translation files for the given language with new strings from templates.
     * This basically takes the (English) templates, fills in existing translations, and saves.
     * Any FILESYSTEM_saveTiXml2Document() writes to main lang dir */
    vlog_info("Syncing %s with templates...", langcode.c_str());

    lang = langcode;
    loadtext(false);

    uint8_t glyph_w = 8, glyph_h = 8;
    font::glyph_dimensions(PR_FONT_IDX(langmeta.font_idx, langmeta.rtl), &glyph_w, &glyph_h);
    bool max_local_needed = glyph_w != 8 || glyph_h != 8;

    tinyxml2::XMLDocument doc;
    tinyxml2::XMLHandle hDoc(&doc);
    tinyxml2::XMLElement* pElem;
    tinyxml2::XMLElement* subElem;

    if (load_lang_doc("meta", doc, "en"))
    {
        FOR_EACH_XML_ELEMENT(hDoc, pElem)
        {
            const char* pKey = pElem->Value();

            if (SDL_strcmp(pKey, "active") == 0)
                pElem->SetText((int) langmeta.active);
            else if (SDL_strcmp(pKey, "nativename") == 0)
                pElem->SetText(langmeta.nativename.c_str());
            else if (SDL_strcmp(pKey, "credit") == 0)
                pElem->SetText(langmeta.credit.c_str());
            else if (SDL_strcmp(pKey, "action_hint") == 0)
                pElem->SetText(langmeta.action_hint.c_str());
            else if (SDL_strcmp(pKey, "gamepad_hint") == 0)
                pElem->SetText(langmeta.gamepad_hint.c_str());
            else if (SDL_strcmp(pKey, "autowordwrap") == 0)
                pElem->SetText((int) langmeta.autowordwrap);
            else if (SDL_strcmp(pKey, "toupper") == 0)
                pElem->SetText((int) langmeta.toupper);
            else if (SDL_strcmp(pKey, "toupper_i_dot") == 0)
                pElem->SetText((int) langmeta.toupper_i_dot);
            else if (SDL_strcmp(pKey, "toupper_lower_escape_char") == 0)
                pElem->SetText((int) langmeta.toupper_lower_escape_char);
            else if (SDL_strcmp(pKey, "rtl") == 0)
                pElem->SetText((int) langmeta.rtl);
            else if (SDL_strcmp(pKey, "menu_select") == 0)
                pElem->SetText(langmeta.menu_select.c_str());
            else if (SDL_strcmp(pKey, "menu_select_tight") == 0)
                pElem->SetText(langmeta.menu_select_tight.c_str());
            else if (SDL_strcmp(pKey, "font") == 0)
                pElem->SetText(font::get_main_font_name(langmeta.font_idx));
        }

        /* This part exists because we want to preserve blank lines between the commented
         * options for clarity, so we have to take matters into our own hands. */
        for (
            tinyxml2::XMLNode* pNode = hDoc.FirstChildElement().FirstChild().ToNode();
            pNode != NULL;
            pNode = pNode->NextSibling()
        )
        {
            tinyxml2::XMLComment* pCom = pNode->ToComment();
            if (pCom != NULL)
            {
                tinyxml2::XMLNode* pPrevNode = pCom->PreviousSibling();
                if (pPrevNode != NULL)
                {
                    doc.FirstChildElement()->InsertAfterChild(pPrevNode, doc.NewText("\n\n    "));
                }
                doc.FirstChildElement()->InsertAfterChild(pCom, doc.NewText("\n    "));
            }
        }

        FILESYSTEM_saveTiXml2Document((langcode + "/meta.xml").c_str(), doc);
    }

    if (load_lang_doc("strings", doc, "en"))
    {
        if (max_local_needed)
        {
            write_max_local_decl(&doc, glyph_w, glyph_h);
        }

        FOR_EACH_XML_ELEMENT(hDoc, pElem)
        {
            EXPECT_ELEM(pElem, "string");

            const char* eng = pElem->Attribute("english");
            if (eng != NULL)
            {
                char textcase = pElem->UnsignedAttribute("case", 0);
                const char* tra;
                if (textcase == 0)
                {
                    tra = map_lookup_text(map_translation, eng, "");
                }
                else
                {
                    char* eng_prefixed = add_disambiguator(textcase, eng, NULL);
                    if (eng_prefixed == NULL)
                    {
                        /* Are we out of memory? Stop, don't blank our language files... */
                        return;
                    }
                    /* Note the fallback: if this string used to not be cased and now it is,
                     * simply fill in the old single variant we already had. */
                    tra = map_lookup_text(
                        map_translation,
                        eng_prefixed,
                        map_lookup_text(map_translation, eng, "")
                    );
                    VVV_free(eng_prefixed);
                }

                pElem->SetAttribute("translation", tra);
            }

            if (max_local_needed)
            {
                write_max_local(pElem, glyph_w, glyph_h);
            }
        }

        FILESYSTEM_saveTiXml2Document((langcode + "/strings.xml").c_str(), doc);
    }

    if (!load_lang_doc("numbers", doc, langcode))
    {
        /* If numbers.xml doesn't exist _at all_, then simply copy it from English,
         * so that you can make a new translation simply by making a folder and syncing. */

        if (load_lang_doc("numbers", doc, "en"))
        {
            FILESYSTEM_saveTiXml2Document((langcode + "/numbers.xml").c_str(), doc);
        }
    }

    if (load_lang_doc("strings_plural", doc, "en"))
    {
        /* Form 255 is technically invalid, but we have to account for it */
        bool form_id_used[256];
        SDL_zeroa(form_id_used);
        for (int num = 0; num < 200; num++)
        {
            form_id_used[number_plural_form[num]] = true;
        }

        if (max_local_needed)
        {
            write_max_local_decl(&doc, glyph_w, glyph_h);
        }

        FOR_EACH_XML_ELEMENT(hDoc, pElem)
        {
            EXPECT_ELEM(pElem, "string");

            if (max_local_needed)
            {
                write_max_local(pElem, glyph_w, glyph_h);
            }

            pElem->DeleteChildren();

            const char* eng_plural = pElem->Attribute("english_plural");

            for (int form_id = 0; form_id < 255; form_id++)
            {
                if (form_id_used[form_id] && eng_plural != NULL)
                {
                    subElem = doc.NewElement("translation");
                    pElem->LinkEndChild(subElem);

                    subElem->SetAttribute("form", form_id);

                    char* key = add_disambiguator(form_id+1, eng_plural, NULL);
                    if (key == NULL)
                    {
                        /* Out of memory or something, stop */
                        return;
                    }

                    subElem->SetAttribute("translation", map_lookup_text(map_translation_plural, key, ""));

                    VVV_free(key);
                }
            }
        }

        FILESYSTEM_saveTiXml2Document((langcode + "/strings_plural.xml").c_str(), doc);
    }

    if (load_lang_doc("cutscenes", doc, "en"))
    {
        FOR_EACH_XML_ELEMENT(hDoc, pElem)
        {
            EXPECT_ELEM(pElem, "cutscene");

            const char* cutscene_id = pElem->Attribute("id");
            if (cutscene_id == NULL)
            {
                continue;
            }

            hashmap* map = map_translation_cutscene;

            uintptr_t ptr_cutscene_map;
            bool found = hashmap_get(map, cutscene_id, SDL_strlen(cutscene_id), &ptr_cutscene_map);
            hashmap* cutscene_map = (hashmap*) ptr_cutscene_map;
            if (!found || cutscene_map == NULL)
            {
                continue;
            }

            FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
            {
                EXPECT_ELEM(subElem, "dialogue");

                const char* eng = subElem->Attribute("english");
                if (eng == NULL)
                {
                    continue;
                }

                size_t alloc_len;
                const std::string eng_unwrapped = font::string_unwordwrap(eng);
                char* eng_prefixed = add_disambiguator(subElem->UnsignedAttribute("case", 1), eng_unwrapped.c_str(), &alloc_len);
                if (eng_prefixed == NULL)
                {
                    /* Out of memory or something, stop */
                    return;
                }

                uintptr_t ptr_format;
                found = hashmap_get(cutscene_map, eng_prefixed, alloc_len-1, &ptr_format);
                const TextboxFormat* format = (TextboxFormat*) ptr_format;

                VVV_free(eng_prefixed);

                if (!found || format == NULL)
                {
                    continue;
                }

                subElem->DeleteAttribute("tt");
                subElem->DeleteAttribute("wraplimit");
                subElem->DeleteAttribute("centertext");
                subElem->DeleteAttribute("pad");
                subElem->DeleteAttribute("pad_left");
                subElem->DeleteAttribute("pad_right");
                subElem->DeleteAttribute("padtowidth");

                bool buttons = subElem->BoolAttribute("buttons", false);
                subElem->DeleteAttribute("buttons"); // we want this at the end...

                if (format->text != NULL)
                    subElem->SetAttribute("translation", format->text);
                if (format->tt)
                    subElem->SetAttribute("tt", 1);
                if (format->wraplimit_raw != 0)
                    subElem->SetAttribute("wraplimit", format->wraplimit_raw);
                if (format->centertext)
                    subElem->SetAttribute("centertext", 1);
                if (format->pad_left == format->pad_right && format->pad_left != 0)
                {
                    subElem->SetAttribute("pad", format->pad_left);
                }
                else
                {
                    if (format->pad_left != 0)
                        subElem->SetAttribute("pad_left", format->pad_left);
                    if (format->pad_right != 0)
                        subElem->SetAttribute("pad_right", format->pad_right);
                }
                if (format->padtowidth != 0)
                    subElem->SetAttribute("padtowidth", format->padtowidth);
                if (buttons)
                    subElem->SetAttribute("buttons", 1);
            }
        }

        FILESYSTEM_saveTiXml2Document((langcode + "/cutscenes.xml").c_str(), doc);
    }

    if (load_lang_doc("roomnames", doc, "en"))
    {
        FOR_EACH_XML_ELEMENT(hDoc, pElem)
        {
            EXPECT_ELEM(pElem, "roomname");

            pElem->SetAttribute("translation",
                get_roomname_translation(false, pElem->UnsignedAttribute("x"), pElem->UnsignedAttribute("y"))
            );
        }

        FILESYSTEM_saveTiXml2Document((langcode + "/roomnames.xml").c_str(), doc);
    }

    if (load_lang_doc("roomnames_special", doc, "en"))
    {
        FOR_EACH_XML_ELEMENT(hDoc, pElem)
        {
            EXPECT_ELEM(pElem, "roomname");

            const char* eng = pElem->Attribute("english");
            if (eng != NULL)
            {
                pElem->SetAttribute("translation", map_lookup_text(map_translation_roomnames_special, eng, ""));
            }
        }

        FILESYSTEM_saveTiXml2Document((langcode + "/roomnames_special.xml").c_str(), doc);
    }
}

bool sync_lang_files(void)
{
    /* Returns false if we can't set the lang write dir, true otherwise.
     * This could maybe be extended with better error reporting,
     * problem is getting across which files failed in which languages. */
    std::string oldlang = lang;
    if (!FILESYSTEM_setLangWriteDir())
    {
        vlog_error("Cannot set write dir to lang dir, not syncing language files");
        return false;
    }

    for (size_t i = 0; i < languagelist.size(); i++)
    {
        if (languagelist[i].code != "en")
            sync_lang_file(languagelist[i].code);
    }

    FILESYSTEM_restoreWriteDir();
    lang = oldlang;
    loadtext(false);

    return true;
}

bool save_roomname_to_file(const std::string& langcode, bool custom_level, int roomx, int roomy, const char* tra, const char* explanation)
{
    if (custom_level)
    {
        vlog_error("Saving custom level room names not implemented");
        return false;
    }

    if (!fix_room_coords(custom_level, &roomx, &roomy))
    {
        return false;
    }

    tinyxml2::XMLDocument doc;
    tinyxml2::XMLHandle hDoc(&doc);
    tinyxml2::XMLElement* pElem;

    if (!load_lang_doc("roomnames", doc, langcode))
    {
        return false;
    }

    bool found = false;
    FOR_EACH_XML_ELEMENT(hDoc, pElem)
    {
        EXPECT_ELEM(pElem, "roomname");

        int x = pElem->IntAttribute("x", -1);
        int y = pElem->IntAttribute("y", -1);

        if (x == roomx && y == roomy)
        {
            if (explanation != NULL)
            {
                pElem->SetAttribute("explanation", explanation);
            }
            if (tra != NULL)
            {
                pElem->SetAttribute("translation", tra);
            }
            found = true;
        }
    }

    if (!found)
    {
        vlog_error("Could not find room %d,%d in language file to replace!", roomx, roomy);
        return false;
    }

    if (!FILESYSTEM_setLangWriteDir())
    {
        vlog_error("Cannot set write dir to lang dir, so room name can't be saved");
        return false;
    }
    bool save_success = FILESYSTEM_saveTiXml2Document((langcode + "/roomnames.xml").c_str(), doc);
    FILESYSTEM_restoreWriteDir();
    if (!save_success)
    {
        vlog_error("Could not write roomnames document!");
        return false;
    }
    return store_roomname_translation(custom_level, roomx, roomy, tra, explanation);
}

bool save_roomname_explanation_to_files(bool custom_level, int roomx, int roomy, const char* explanation)
{
    bool success = true;
    for (size_t i = 0; i < languagelist.size(); i++)
    {
        if (!save_roomname_to_file(languagelist[i].code, custom_level, roomx, roomy, NULL, explanation))
        {
            success = false;
            vlog_warn("Could not save room name explanation to language %s", languagelist[i].code.c_str());
        }
    }

    return !languagelist.empty() && success;
}

void local_limits_check(void)
{
    text_overflows.clear();
    loadtext(true);
    limitscheck_current_overflow = 0;
}

void global_limits_check(void)
{
    text_overflows.clear();

    std::string oldlang = lang;

    textbook_clear(&textbook_main);
    textbook_set_protected(&textbook_main, true);

    for (size_t i = 0; i < languagelist.size(); i++)
    {
        if (languagelist[i].code != "en")
        {
            lang = languagelist[i].code;
            loadtext(true);
        }
    }

    lang = oldlang;
    loadtext(false);

    textbook_set_protected(&textbook_main, false);

    limitscheck_current_overflow = 0;
}

void populate_testable_script_ids(void)
{
    testable_script_ids.clear();

    tinyxml2::XMLDocument doc;
    tinyxml2::XMLHandle hDoc(&doc);
    tinyxml2::XMLElement* pElem;

    if (!load_lang_doc("cutscenes", doc))
    {
        return;
    }

    FOR_EACH_XML_ELEMENT(hDoc, pElem)
    {
        EXPECT_ELEM(pElem, "cutscene");

        const char* id = pElem->Attribute("id");
        if (id != NULL)
        {
            testable_script_ids.push_back(id);
        }
    }
}

bool populate_cutscene_test(const char* script_id)
{
    tinyxml2::XMLDocument doc;
    tinyxml2::XMLHandle hDoc(&doc);
    tinyxml2::XMLElement* pElem;

    if (!load_lang_doc("cutscenes", doc))
    {
        return false;
    }

    const char* original = get_level_original_lang(hDoc);

    FOR_EACH_XML_ELEMENT(hDoc, pElem)
    {
        EXPECT_ELEM(pElem, "cutscene");

        if (SDL_strcmp(pElem->Attribute("id"), script_id) != 0)
        {
            /* Not the correct cutscene */
            continue;
        }

        tinyxml2::XMLElement* subElem;
        FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
        {
            EXPECT_ELEM(subElem, "dialogue");

            const char* tra = subElem->Attribute("translation");
            const char* speaker = subElem->Attribute("speaker");
            const char* eng = subElem->Attribute(original);
            if (tra != NULL && tra[0] != '\0' && speaker != NULL && eng != NULL)
            {
                script.add_test_line(
                    speaker,
                    eng,
                    subElem->UnsignedAttribute("case", 1),
                    subElem->BoolAttribute("buttons", false)
                );
            }
        }

        return true;
    }

    return false;
}

} /* namespace loc */
