/*
 *  The ManaPlus Client
 *  Copyright (C) 2008  The Legend of Mazzeroth Development Team
 *  Copyright (C) 2009  Aethyra Development Team
 *  Copyright (C) 2009  The Mana World Development Team
 *  Copyright (C) 2009-2010  The Mana Developers
 *  Copyright (C) 2011-2015  The ManaPlus Developers
 *
 *  This file is part of The ManaPlus Client.
 *
 *  This program 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 2 of the License, or
 *  any later version.
 *
 *  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "gui/theme.h"

#include "configuration.h"
#include "graphicsmanager.h"

#include "gui/skin.h"
#include "gui/themeinfo.h"

#include "resources/dyepalette.h"
#include "resources/image.h"
#include "resources/imagerect.h"
#include "resources/resourcemanager.h"

#include "utils/dtor.h"
#include "utils/files.h"
#include "utils/physfstools.h"

#include "debug.h"

static std::string defaultThemePath;

std::string Theme::mThemePath;
std::string Theme::mThemeName;
std::string Theme::mScreenDensity;

Theme *theme = nullptr;

// Set the theme path...
static void initDefaultThemePath()
{
    defaultThemePath = branding.getStringValue("guiThemePath");

    logger->log("defaultThemePath: " + defaultThemePath);
    if (!defaultThemePath.empty() && PhysFs::isDirectory(
        defaultThemePath.c_str()))
    {
        return;
    }
    else
    {
        defaultThemePath = "themes/";
    }
}

Theme::Theme() :
    Palette(static_cast<int>(ThemeColorId::THEME_COLORS_END) * THEME_PALETTES),
    mSkins(),
    mMinimumOpacity(-1.0F),
    mProgressColors(ProgressColors(static_cast<size_t>(
                    ProgressColorId::THEME_PROG_END)))
{
    initDefaultThemePath();

    config.addListener("guialpha", this);

    mColors[static_cast<size_t>(ThemeColorId::HIGHLIGHT)].ch = 'H';
    mColors[static_cast<size_t>(ThemeColorId::CHAT)].ch = 'C';
    mColors[static_cast<size_t>(ThemeColorId::GM)].ch = 'G';
    mColors[static_cast<size_t>(ThemeColorId::GLOBAL)].ch = 'g';
    mColors[static_cast<size_t>(ThemeColorId::PLAYER)].ch = 'Y';
    mColors[static_cast<size_t>(ThemeColorId::WHISPER_TAB)].ch = 'W';
    mColors[static_cast<size_t>(ThemeColorId::WHISPER_TAB_OFFLINE)].ch = 'w';
    mColors[static_cast<size_t>(ThemeColorId::IS)].ch = 'I';
    mColors[static_cast<size_t>(ThemeColorId::PARTY_CHAT_TAB)].ch = 'P';
    mColors[static_cast<size_t>(ThemeColorId::GUILD_CHAT_TAB)].ch = 'U';
    mColors[static_cast<size_t>(ThemeColorId::SERVER)].ch = 'S';
    mColors[static_cast<size_t>(ThemeColorId::LOGGER)].ch = 'L';
    mColors[static_cast<size_t>(ThemeColorId::HYPERLINK)].ch = '<';
    mColors[static_cast<size_t>(ThemeColorId::SELFNICK)].ch = 's';
    mColors[static_cast<size_t>(ThemeColorId::OLDCHAT)].ch = 'o';
    mColors[static_cast<size_t>(ThemeColorId::AWAYCHAT)].ch = 'a';
    mCharColors['H'] = static_cast<int>(ThemeColorId::HIGHLIGHT);
    mCharColors['C'] = static_cast<int>(ThemeColorId::CHAT);
    mCharColors['G'] = static_cast<int>(ThemeColorId::GM);
    mCharColors['g'] = static_cast<int>(ThemeColorId::GLOBAL);
    mCharColors['Y'] = static_cast<int>(ThemeColorId::PLAYER);
    mCharColors['W'] = static_cast<int>(ThemeColorId::WHISPER_TAB);
    mCharColors['w'] = static_cast<int>(ThemeColorId::WHISPER_TAB_OFFLINE);
    mCharColors['I'] = static_cast<int>(ThemeColorId::IS);
    mCharColors['P'] = static_cast<int>(ThemeColorId::PARTY_CHAT_TAB);
    mCharColors['U'] = static_cast<int>(ThemeColorId::GUILD_CHAT_TAB);
    mCharColors['S'] = static_cast<int>(ThemeColorId::SERVER);
    mCharColors['L'] = static_cast<int>(ThemeColorId::LOGGER);
    mCharColors['<'] = static_cast<int>(ThemeColorId::HYPERLINK);
    mCharColors['s'] = static_cast<int>(ThemeColorId::SELFNICK);
    mCharColors['o'] = static_cast<int>(ThemeColorId::OLDCHAT);
    mCharColors['a'] = static_cast<int>(ThemeColorId::AWAYCHAT);

    // here need use outlined colors
    mCharColors['H' | 0x80]
        = static_cast<int>(ThemeColorId::HIGHLIGHT_OUTLINE);
    mCharColors['C' | 0x80] = static_cast<int>(ThemeColorId::CHAT_OUTLINE);
    mCharColors['G' | 0x80] = static_cast<int>(ThemeColorId::GM_OUTLINE);
    mCharColors['g' | 0x80] = static_cast<int>(ThemeColorId::GLOBAL_OUTLINE);
    mCharColors['Y' | 0x80] = static_cast<int>(ThemeColorId::PLAYER_OUTLINE);
    mCharColors['W' | 0x80]
        = static_cast<int>(ThemeColorId::WHISPER_TAB_OUTLINE);
    mCharColors['w' | 0x80]
        = static_cast<int>(ThemeColorId::WHISPER_TAB_OFFLINE_OUTLINE);
    mCharColors['I' | 0x80] = static_cast<int>(ThemeColorId::IS_OUTLINE);
    mCharColors['P' | 0x80]
        = static_cast<int>(ThemeColorId::PARTY_CHAT_TAB_OUTLINE);
    mCharColors['U' | 0x80]
        = static_cast<int>(ThemeColorId::GUILD_CHAT_TAB_OUTLINE);
    mCharColors['S' | 0x80] = static_cast<int>(ThemeColorId::SERVER_OUTLINE);
    mCharColors['L' | 0x80] = static_cast<int>(ThemeColorId::LOGGER_OUTLINE);
    mCharColors['<' | 0x80]
        = static_cast<int>(ThemeColorId::HYPERLINK_OUTLINE);
    mCharColors['s' | 0x80] = static_cast<int>(ThemeColorId::SELFNICK_OUTLINE);
    mCharColors['o' | 0x80] = static_cast<int>(ThemeColorId::OLDCHAT_OUTLINE);
    mCharColors['a' | 0x80] = static_cast<int>(ThemeColorId::AWAYCHAT_OUTLINE);
}

Theme::~Theme()
{
    delete_all(mSkins);
    config.removeListener("guialpha", this);
    CHECKLISTENERS
    delete_all(mProgressColors);
}

Color Theme::getProgressColor(const ProgressColorIdT type,
                              const float progress)
{
    int color[3] = {0, 0, 0};

    if (theme)
    {
        const DyePalette *const dye
            = theme->mProgressColors[static_cast<size_t>(type)];

        if (dye)
        {
            dye->getColor(progress, color);
        }
        else
        {
            logger->log("color not found: "
                + toString(static_cast<int>(type)));
        }
    }

    return Color(color[0], color[1], color[2]);
}

Skin *Theme::load(const std::string &filename,
                  const std::string &filename2,
                  const bool full,
                  const std::string &restrict defaultPath)
{
    // Check if this skin was already loaded

    const SkinIterator skinIterator = mSkins.find(filename);
    if (mSkins.end() != skinIterator)
    {
        if (skinIterator->second)
            skinIterator->second->instances++;
        return skinIterator->second;
    }

    Skin *skin = nullptr;
    if (mScreenDensity.empty())
    {   // if no density detected
        skin = readSkin(filename, full);
        if (!skin && !filename2.empty() && filename2 != filename)
            skin = readSkin(filename2, full);
        if (!skin && filename2 != "window.xml")
            skin = readSkin("window.xml", full);
    }
    else
    {   // first use correct density images
        const std::string endStr("_" + mScreenDensity + ".xml");
        std::string name = filename;
        if (findCutLast(name, ".xml"))
            skin = readSkin(name + endStr, full);
        if (!skin)
            skin = readSkin(filename, full);
        if (!skin && !filename2.empty() && filename2 != filename)
        {
            name = filename2;
            if (findCutLast(name, ".xml"))
                skin = readSkin(name + endStr, full);
            if (!skin)
                skin = readSkin(filename2, full);
        }
        if (!skin && filename2 != "window.xml")
        {
            skin = readSkin("window" + endStr, full);
            if (!skin)
                skin = readSkin("window.xml", full);
        }
    }

    if (!skin)
    {
        // Try falling back on the defaultPath if this makes sense
        if (filename != defaultPath)
        {
            logger->log("Error loading skin '%s', falling back on default.",
                        filename.c_str());

            skin = readSkin(defaultPath, full);
        }

        if (!skin)
        {
            logger->log(strprintf("Error: Loading default skin '%s' failed. "
                                  "Make sure the skin file is valid.",
                                  defaultPath.c_str()));
        }
    }

    mSkins[filename] = skin;
    return skin;
}

void Theme::unload(Skin *const skin)
{
    if (!skin)
        return;
    skin->instances --;
    if (!skin->instances)
    {
        SkinIterator it = mSkins.begin();
        const SkinIterator it_end = mSkins.end();
        while (it != it_end)
        {
            if (it->second == skin)
            {
                mSkins.erase(it);
                break;
            }
            ++ it;
        }
        delete skin;
    }
}

void Theme::setMinimumOpacity(const float minimumOpacity)
{
    if (minimumOpacity > 1.0F)
        return;

    mMinimumOpacity = minimumOpacity;
    updateAlpha();
}

void Theme::updateAlpha()
{
    FOR_EACH (SkinIterator, iter, mSkins)
    {
        Skin *const skin = iter->second;
        if (skin)
            skin->updateAlpha(mMinimumOpacity);
    }
}

void Theme::optionChanged(const std::string &)
{
    updateAlpha();
}

struct SkinParameter final
{
    int index;
    std::string name;
};

static const SkinParameter skinParam[] =
{
    {0, "top-left-corner"},
    {0, "standart"},
    {0, "up"},
    {0, "hstart"},
    {0, "in"},
    {0, "normal"},
    {1, "top-edge"},
    {1, "highlighted"},
    {1, "down"},
    {1, "hmiddle"},
    {1, "in-highlighted"},
    {1, "checked"},
    {2, "top-right-corner"},
    {2, "pressed"},
    {2, "left"},
    {2, "hend"},
    {2, "out"},
    {2, "disabled"},
    {3, "left-edge"},
    {3, "disabled"},
    {3, "right"},
    {3, "hgrip"},
    {3, "out-highlighted"},
    {3, "disabled-checked"},
    {4, "bg-quad"},
    {4, "vstart"},
    {4, "normal-highlighted"},
    {5, "right-edge"},
    {5, "vmiddle"},
    {5, "checked-highlighted"},
    {6, "bottom-left-corner"},
    {6, "vend"},
    {7, "bottom-edge"},
    {7, "vgrip"},
    {8, "bottom-right-corner"},
};

static const SkinParameter imageParam[] =
{
    {0, "closeImage"},
    {1, "closeImageHighlighted"},
    {2, "stickyImageUp"},
    {3, "stickyImageDown"},
};

struct SkinHelper final
{
    SkinHelper() :
        partType(),
        xPos(),
        yPos(),
        width(),
        height(),
        rect(),
        node(),
        image()
    {
    }

    A_DELETE_COPY(SkinHelper)

    std::string partType;
    int xPos;
    int yPos;
    int width;
    int height;
    ImageRect *rect;
    XmlNodePtr *node;
    Image *image;

    bool loadList(const SkinParameter *const params,
                  const size_t size) A_NONNULL(2)
    {
        for (size_t f = 0; f < size; f ++)
        {
            const SkinParameter &param = params[f];
            if (partType == param.name)
            {
                rect->grid[param.index] = resourceManager->getSubImage(
                    image, xPos, yPos, width, height);
                return true;
            }
        }
        return false;
    }
};

Skin *Theme::readSkin(const std::string &filename, const bool full)
{
    if (filename.empty())
        return nullptr;

//    logger->log("Loading skin '%s'.", filename.c_str());

    XML::Document doc(resolveThemePath(filename),
        UseResman_true,
        SkipError_true);
    const XmlNodePtr rootNode = doc.rootNode();

    if (!rootNode || !xmlNameEqual(rootNode, "skinset"))
        return nullptr;

    const std::string skinSetImage = XML::getProperty(rootNode, "image", "");

    if (skinSetImage.empty())
    {
        logger->log1("Theme::readSkin(): Skinset does not define an image!");
        return nullptr;
    }

    Image *const dBorders = Theme::getImageFromTheme(skinSetImage);
    ImageRect *const border = new ImageRect;
    ImageRect *const images = new ImageRect;
    memset(border, 0, sizeof(ImageRect));
    memset(images, 0, sizeof(ImageRect));
    int padding = 3;
    int titlePadding = 4;
    int titlebarHeight = 0;
    int titlebarHeightRelative = 0;
    int closePadding = 3;
    int stickySpacing = 3;
    int stickyPadding = 3;
    int resizePadding = 2;
    StringIntMap *const mOptions = new StringIntMap;

    // iterate <widget>'s
    for_each_xml_child_node(widgetNode, rootNode)
    {
        if (!xmlNameEqual(widgetNode, "widget"))
            continue;

        const std::string widgetType =
                XML::getProperty(widgetNode, "type", "unknown");
        if (widgetType == "Window")
        {
            SkinHelper helper;
            const int globalXPos = XML::getProperty(widgetNode, "xpos", 0);
            const int globalYPos = XML::getProperty(widgetNode, "ypos", 0);
            for_each_xml_child_node(partNode, widgetNode)
            {
                if (xmlNameEqual(partNode, "part"))
                {
                    helper.partType = XML::getProperty(
                        partNode, "type", "unknown");
                    helper.xPos = XML::getProperty(
                        partNode, "xpos", 0) + globalXPos;
                    helper.yPos = XML::getProperty(
                        partNode, "ypos", 0) + globalYPos;
                    helper.width = XML::getProperty(partNode, "width", 1);
                    helper.height = XML::getProperty(partNode, "height", 1);
                    helper.image = dBorders;

                    helper.rect = border;
                    if (!helper.loadList(skinParam,
                        sizeof(skinParam) / sizeof(SkinParameter)))
                    {
                        helper.rect = images;
                        helper.loadList(imageParam,
                            sizeof(imageParam) / sizeof(SkinParameter));
                    }
                }
                else if (full && xmlNameEqual(partNode, "option"))
                {
                    const std::string name = XML::getProperty(
                        partNode, "name", "");
                    if (name == "padding")
                    {
                        padding = XML::getProperty(partNode, "value", 3);
                    }
                    else if (name == "titlePadding")
                    {
                        titlePadding = XML::getProperty(partNode, "value", 4);
                    }
                    else if (name == "closePadding")
                    {
                        closePadding = XML::getProperty(partNode, "value", 3);
                    }
                    else if (name == "stickySpacing")
                    {
                        stickySpacing = XML::getProperty(partNode, "value", 3);
                    }
                    else if (name == "stickyPadding")
                    {
                        stickyPadding = XML::getProperty(partNode, "value", 3);
                    }
                    else if (name == "titlebarHeight")
                    {
                        titlebarHeight = XML::getProperty(
                            partNode, "value", 0);
                    }
                    else if (name == "titlebarHeightRelative")
                    {
                        titlebarHeightRelative = XML::getProperty(
                            partNode, "value", 0);
                    }
                    else if (name == "resizePadding")
                    {
                        resizePadding = XML::getProperty(
                            partNode, "value", 2);
                    }
                    else
                    {
                        (*mOptions)[name] = XML::getProperty(
                            partNode, "value", 0);
                    }
                }
            }
        }
        else
        {
            logger->log("Theme::readSkin(): Unknown widget type '%s'",
                        widgetType.c_str());
        }
    }

    if (dBorders)
        dBorders->decRef();

    (*mOptions)["closePadding"] = closePadding;
    (*mOptions)["stickyPadding"] = stickyPadding;
    (*mOptions)["stickySpacing"] = stickySpacing;
    (*mOptions)["titlebarHeight"] = titlebarHeight;
    (*mOptions)["titlebarHeightRelative"] = titlebarHeightRelative;
    (*mOptions)["resizePadding"] = resizePadding;

    Skin *const skin = new Skin(border, images, filename, "", padding,
        titlePadding, mOptions);
    delete images;
    skin->updateAlpha(mMinimumOpacity);
    return skin;
}

bool Theme::tryThemePath(const std::string &themeName)
{
    if (!themeName.empty())
    {
        const std::string path = defaultThemePath + themeName;
        if (PhysFs::exists(path.c_str()))
        {
            mThemePath = path;
            mThemeName = themeName;
            if (theme)
                theme->loadColors("");
            return true;
        }
    }

    return false;
}

void Theme::fillSkinsList(StringVect &list)
{
    Files::getDirs(branding.getStringValue("guiThemePath"), list);
}

void Theme::fillFontsList(StringVect &list)
{
    PHYSFS_permitSymbolicLinks(1);
    Files::getFiles(branding.getStringValue("fontsPath"), list);
    PHYSFS_permitSymbolicLinks(0);
}

void Theme::fillSoundsList(StringVect &list)
{
    char **skins = PhysFs::enumerateFiles(
        branding.getStringValue("systemsounds").c_str());

    for (char **i = skins; *i; i++)
    {
        if (!PhysFs::isDirectory((
            branding.getStringValue("systemsounds") + *i).c_str()))
        {
            std::string str = *i;
            if (findCutLast(str, ".ogg"))
                list.push_back(str);
        }
    }

    PhysFs::freeList(skins);
}

void Theme::selectSkin()
{
    prepareThemePath();
    mScreenDensity = graphicsManager.getDensityString();
}

void Theme::prepareThemePath()
{
    initDefaultThemePath();

    mThemePath.clear();
    mThemeName.clear();

    // Try theme from settings
    if (tryThemePath(config.getStringValue("theme")))
        return;

    // Try theme from branding
    if (tryThemePath(branding.getStringValue("theme")))
        return;

    if (mThemePath.empty())
        mThemePath = "graphics/gui";

    theme->loadColors(mThemePath);

    logger->log("Selected Theme: " + mThemePath);
}

std::string Theme::resolveThemePath(const std::string &path)
{
    // Need to strip off any dye info for the existence tests
    const int pos = static_cast<int>(path.find('|'));
    std::string file;
    if (pos > 0)
        file = path.substr(0, pos);
    else
        file = path;

    // File with path
    if (file.find('/') != std::string::npos)
    {
        // Might be a valid path already
        if (PhysFs::exists(file.c_str()))
            return path;
    }

    // Try the theme
    file = getThemePath().append("/").append(file);

    if (PhysFs::exists(file.c_str()))
        return getThemePath().append("/").append(path);

    // Backup
    return branding.getStringValue("guiPath").append(path);
}

Image *Theme::getImageFromTheme(const std::string &path)
{
    return resourceManager->getImage(resolveThemePath(path));
}

ImageSet *Theme::getImageSetFromTheme(const std::string &path,
                                      const int w, const int h)
{
    return resourceManager->getImageSet(resolveThemePath(path), w, h);
}

static int readColorType(const std::string &type)
{
    static const std::string colors[static_cast<size_t>(
        ThemeColorId::THEME_COLORS_END)] =
    {
        "BROWSERBOX",
        "BROWSERBOX_OUTLINE",
        "SELFNICK",
        "SELFNICK_OUTLINE",
        "TEXT",
        "TEXT_OUTLINE",
        "CARET",
        "SHADOW",
        "OUTLINE",
        "BORDER",
        "PROGRESS_BAR",
        "PROGRESS_BAR_OUTLINE",
        "BUTTON",
        "BUTTON_OUTLINE",
        "BUTTON_DISABLED",
        "BUTTON_DISABLED_OUTLINE",
        "BUTTON_HIGHLIGHTED",
        "BUTTON_HIGHLIGHTED_OUTLINE",
        "BUTTON_PRESSED",
        "BUTTON_PRESSED_OUTLINE",
        "CHECKBOX",
        "CHECKBOX_OUTLINE",
        "DROPDOWN",
        "DROPDOWN_OUTLINE",
        "LABEL",
        "LABEL_OUTLINE",
        "LISTBOX",
        "LISTBOX_OUTLINE",
        "LISTBOX_SELECTED",
        "LISTBOX_SELECTED_OUTLINE",
        "RADIOBUTTON",
        "RADIOBUTTON_OUTLINE",
        "POPUP",
        "POPUP_OUTLINE",
        "TAB",
        "TAB_OUTLINE",
        "TAB_HIGHLIGHTED",
        "TAB_HIGHLIGHTED_OUTLINE",
        "TAB_SELECTED",
        "TAB_SELECTED_OUTLINE",
        "TEXTBOX",
        "TEXTFIELD",
        "TEXTFIELD_OUTLINE",
        "WINDOW",
        "WINDOW_OUTLINE",
        "BATTLE_CHAT_TAB",
        "BATTLE_CHAT_TAB_OUTLINE",
        "CHANNEL_CHAT_TAB",
        "CHANNEL_CHAT_TAB_OUTLINE",
        "PARTY_CHAT_TAB",
        "PARTY_CHAT_TAB_OUTLINE",
        "PARTY_SOCIAL_TAB",
        "PARTY_SOCIAL_TAB_OUTLINE",
        "GUILD_CHAT_TAB",
        "GUILD_CHAT_TAB_OUTLINE",
        "GUILD_SOCIAL_TAB",
        "GUILD_SOCIAL_TAB_OUTLINE",
        "GM_CHAT_TAB",
        "GM_CHAT_TAB_OUTLINE",
        "BATTLE_CHAT_TAB_HIGHLIGHTED",
        "BATTLE_CHAT_TAB_HIGHLIGHTED_OUTLINE",
        "CHANNEL_CHAT_TAB_HIGHLIGHTED",
        "CHANNEL_CHAT_TAB_HIGHLIGHTED_OUTLINE",
        "PARTY_CHAT_TAB_HIGHLIGHTED",
        "PARTY_CHAT_TAB_HIGHLIGHTED_OUTLINE",
        "PARTY_SOCIAL_TAB_HIGHLIGHTED",
        "PARTY_SOCIAL_TAB_HIGHLIGHTED_OUTLINE",
        "GUILD_CHAT_TAB_HIGHLIGHTED",
        "GUILD_CHAT_TAB_HIGHLIGHTED_OUTLINE",
        "GUILD_SOCIAL_TAB_HIGHLIGHTED",
        "GUILD_SOCIAL_TAB_HIGHLIGHTED_OUTLINE",
        "GM_CHAT_TAB_HIGHLIGHTED",
        "GM_CHAT_TAB_HIGHLIGHTED_OUTLINE",
        "BATTLE_CHAT_TAB_SELECTED",
        "BATTLE_CHAT_TAB_SELECTED_OUTLINE",
        "CHANNEL_CHAT_TAB_SELECTED",
        "CHANNEL_CHAT_TAB_SELECTED_OUTLINE",
        "PARTY_CHAT_TAB_SELECTED",
        "PARTY_CHAT_TAB_SELECTED_OUTLINE",
        "PARTY_SOCIAL_TAB_SELECTED",
        "PARTY_SOCIAL_TAB_SELECTED_OUTLINE",
        "GUILD_CHAT_TAB_SELECTED",
        "GUILD_CHAT_TAB_SELECTED_OUTLINE",
        "GUILD_SOCIAL_TAB_SELECTED",
        "GUILD_SOCIAL_TAB_SELECTED_OUTLINE",
        "GM_CHAT_TAB_SELECTED",
        "GM_CHAT_TAB_SELECTED_OUTLINE",
        "BACKGROUND",
        "BACKGROUND_GRAY",
        "SCROLLBAR_GRAY",
        "DROPDOWN_SHADOW",
        "HIGHLIGHT",
        "HIGHLIGHT_OUTLINE",
        "TAB_FLASH",
        "TAB_FLASH_OUTLINE",
        "TAB_PLAYER_FLASH",
        "TAB_PLAYER_FLASH_OUTLINE",
        "SHOP_WARNING",
        "ITEM_EQUIPPED",
        "ITEM_EQUIPPED_OUTLINE",
        "ITEM_NOT_EQUIPPED",
        "ITEM_NOT_EQUIPPED_OUTLINE",
        "CHAT",
        "CHAT_OUTLINE",
        "GM",
        "GM_OUTLINE",
        "GLOBAL",
        "GLOBAL_OUTLINE",
        "PLAYER",
        "PLAYER_OUTLINE",
        "WHISPER_TAB",
        "WHISPER_TAB_OUTLINE",
        "WHISPER_TAB_OFFLINE",
        "WHISPER_TAB_OFFLINE_OUTLINE",
        "WHISPER_TAB_HIGHLIGHTED",
        "WHISPER_TAB_HIGHLIGHTED_OUTLINE",
        "WHISPER_TAB_OFFLINE_HIGHLIGHTED",
        "WHISPER_TAB_OFFLINE_HIGHLIGHTED_OUTLINE",
        "WHISPER_TAB_SELECTED",
        "WHISPER_TAB_SELECTED_OUTLINE",
        "WHISPER_TAB_OFFLINE_SELECTED",
        "WHISPER_TAB_OFFLINE_SELECTED_OUTLINE",
        "IS",
        "IS_OUTLINE",
        "SERVER",
        "SERVER_OUTLINE",
        "LOGGER",
        "LOGGER_OUTLINE",
        "HYPERLINK",
        "HYPERLINK_OUTLINE",
        "UNKNOWN_ITEM",
        "UNKNOWN_ITEM_OUTLINE",
        "GENERIC",
        "GENERIC_OUTLINE",
        "HEAD",
        "HEAD_OUTLINE",
        "USABLE",
        "USABLE_OUTLINE",
        "TORSO",
        "TORSO_OUTLINE",
        "ONEHAND",
        "ONEHAND_OUTLINE",
        "LEGS",
        "LEGS_OUTLINE",
        "FEET",
        "FEET_OUTLINE",
        "TWOHAND",
        "TWOHAND_OUTLINE",
        "SHIELD",
        "SHIELD_OUTLINE",
        "RING",
        "RING_OUTLINE",
        "NECKLACE",
        "NECKLACE_OUTLINE",
        "ARMS",
        "ARMS_OUTLINE",
        "AMMO",
        "AMMO_OUTLINE",
        "SERVER_VERSION_NOT_SUPPORTED",
        "SERVER_VERSION_NOT_SUPPORTED_OUTLINE",
        "WARNING",
        "WARNING_OUTLINE",
        "CHARM",
        "CHARM_OUTLINE",
        "CARD",
        "CARD_OUTLINE",
        "PLAYER_ADVANCED",
        "PLAYER_ADVANCED_OUTLINE",
        "BUBBLE_NAME",
        "BUBBLE_NAME_OUTLINE",
        "BUBBLE_TEXT",
        "BUBBLE_TEXT_OUTLINE",
        "BLACK",
        "BLACK_OUTLINE",
        "RED",
        "RED_OUTLINE",
        "GREEN",
        "GREEN_OUTLINE",
        "BLUE",
        "BLUE_OUTLINE",
        "ORANGE",
        "ORANGE_OUTLINE",
        "YELLOW",
        "YELLOW_OUTLINE",
        "PINK",
        "PINK_OUTLINE",
        "PURPLE",
        "PURPLE_OUTLINE",
        "GRAY",
        "GRAY_OUTLINE",
        "BROWN",
        "BROWN_OUTLINE",
        "STATUSBAR_ON",
        "STATUSBAR_OFF",
        "TABLE_BACKGROUND",
        "SLOTS_BAR",
        "SLOTS_BAR_OUTLINE",
        "HP_BAR",
        "HP_BAR_OUTLINE",
        "MP_BAR",
        "MP_BAR_OUTLINE",
        "NO_MP_BAR",
        "NO_MP_BAR_OUTLINE",
        "XP_BAR",
        "XP_BAR_OUTLINE",
        "WEIGHT_BAR",
        "WEIGHT_BAR_OUTLINE",
        "MONEY_BAR",
        "MONEY_BAR_OUTLINE",
        "ARROWS_BAR",
        "ARROWS_BAR_OUTLINE",
        "STATUS_BAR",
        "STATUS_BAR_OUTLINE",
        "JOB_BAR",
        "JOB_BAR_OUTLINE",
        "OLDCHAT",
        "OLDCHAT_OUTLINE",
        "AWAYCHAT",
        "AWAYCHAT_OUTLINE",
        "SKILL_COOLDOWN",
        "TEXT_DISABLED",
        "TEXT_DISABLED_OUTLINE"
    };

    if (type.empty())
        return -1;

    for (int i = 0; i < static_cast<int>(ThemeColorId::THEME_COLORS_END); i++)
    {
        if (compareStrI(type, colors[i]) == 0)
            return i;
    }

    return -1;
}

static Color readColor(const std::string &description)
{
    const int size = static_cast<const int>(description.length());
    if (size < 7 || description[0] != '#')
    {
        logger->log("Error, invalid theme color palette: %s",
                    description.c_str());
        return Palette::BLACK;
    }

    unsigned int v = 0;
    for (int i = 1; i < 7; ++i)
    {
        signed const char c = description[i];
        int n;

        if ('0' <= c && c <= '9')
        {
            n = c - '0';
        }
        else if ('A' <= c && c <= 'F')
        {
            n = c - 'A' + 10;
        }
        else if ('a' <= c && c <= 'f')
        {
            n = c - 'a' + 10;
        }
        else
        {
            logger->log("Error, invalid theme color palette: %s",
                        description.c_str());
            return Palette::BLACK;
        }

        v = (v << 4) | n;
    }

    return Color(v);
}

static GradientTypeT readColorGradient(const std::string &grad)
{
    static const std::string grads[] =
    {
        "STATIC",
        "PULSE",
        "SPECTRUM",
        "RAINBOW"
    };

    if (grad.empty())
        return GradientType::STATIC;

    for (int i = 0; i < 4; i++)
    {
        if (compareStrI(grad, grads[i]))
            return static_cast<GradientTypeT>(i);
    }

    return GradientType::STATIC;
}

static int readProgressType(const std::string &type)
{
    static const std::string colors[static_cast<size_t>(
        ProgressColorId::THEME_PROG_END)] =
    {
        "HP",
        "HP_POISON",
        "MP",
        "NO_MP",
        "EXP",
        "INVY_SLOTS",
        "WEIGHT",
        "JOB",
        "UPDATE",
        "MONEY",
        "ARROWS",
        "STATUS"
    };

    if (type.empty())
        return -1;

    for (int i = 0; i < static_cast<int>(ProgressColorId::THEME_PROG_END); i++)
    {
        if (compareStrI(type, colors[i]) == 0)
            return i;
    }

    return -1;
}

void Theme::loadColors(std::string file)
{
    if (file == "")
        file = "colors.xml";
    else
        file.append("/colors.xml");

    XML::Document doc(resolveThemePath(file), UseResman_true, SkipError_false);
    const XmlNodePtrConst root = doc.rootNode();

    if (!root || !xmlNameEqual(root, "colors"))
    {
        logger->log("Error loading colors file: %s", file.c_str());
        return;
    }

    logger->log("Loading colors file: %s", file.c_str());

    for_each_xml_child_node(paletteNode, root)
    {
        if (xmlNameEqual(paletteNode, "progressbar"))
        {
            const int type = readProgressType(XML::getProperty(
                paletteNode, "id", ""));
            if (type < 0)
                continue;

            mProgressColors[type] = new DyePalette(XML::getProperty(
                paletteNode, "color", ""), 6);
        }
        else if (!xmlNameEqual(paletteNode, "palette"))
        {
            continue;
        }

        const int paletteId = XML::getProperty(paletteNode, "id", 1);
        if (paletteId < 0 || paletteId >= THEME_PALETTES)
            continue;

        for_each_xml_child_node(node, paletteNode)
        {
            if (xmlNameEqual(node, "color"))
            {
                const std::string id = XML::getProperty(node, "id", "");
                const int type = readColorType(id);
                if (type < 0)
                    continue;

                const std::string temp = XML::getProperty(node, "color", "");
                if (temp.empty())
                    continue;

                const Color color = readColor(temp);
                const GradientTypeT grad = readColorGradient(
                    XML::getProperty(node, "effect", ""));
                mColors[paletteId * static_cast<size_t>(
                    ThemeColorId::THEME_COLORS_END) + type].set(
                    type, color, grad, 10);

                if (!findLast(id, "_OUTLINE"))
                {
                    const int type2 = readColorType(id + "_OUTLINE");
                    if (type2 < 0)
                        continue;
                    const int idx = paletteId
                        * static_cast<int>(ThemeColorId::THEME_COLORS_END);
                    mColors[idx + type2] = mColors[idx + type];
                }
            }
        }
    }
}

#define loadGrid() \
    { \
        const ImageRect &rect = skin->getBorder(); \
        for (int f = start; f <= end; f ++) \
        { \
            if (rect.grid[f]) \
            { \
                image.grid[f] = rect.grid[f]; \
                image.grid[f]->incRef(); \
            } \
        } \
    }

void Theme::loadRect(ImageRect &image,
                     const std::string &name,
                     const std::string &name2,
                     const int start,
                     const int end)
{
    Skin *const skin = load(name, name2, false);
    if (skin)
    {
        loadGrid();
        unload(skin);
    }
}

Skin *Theme::loadSkinRect(ImageRect &image,
                          const std::string &name,
                          const std::string &name2,
                          const int start,
                          const int end)
{
    Skin *const skin = load(name, name2);
    if (skin)
        loadGrid();
    return skin;
}

void Theme::unloadRect(const ImageRect &rect,
                       const int start,
                       const int end)
{
    for (int f = start; f <= end; f ++)
    {
        if (rect.grid[f])
            rect.grid[f]->decRef();
    }
}

Image *Theme::getImageFromThemeXml(const std::string &name,
                                   const std::string &name2)
{
    if (!theme)
        return nullptr;

    Skin *const skin = theme->load(name, name2, false);
    if (skin)
    {
        const ImageRect &rect = skin->getBorder();
        if (rect.grid[0])
        {
            Image *const image = rect.grid[0];
            image->incRef();
            theme->unload(skin);
            return image;
        }
        theme->unload(skin);
    }
    return nullptr;
}

ImageSet *Theme::getImageSetFromThemeXml(const std::string &name,
                                         const std::string &name2,
                                         const int w, const int h)
{
    if (!theme)
        return nullptr;

    Skin *const skin = theme->load(name, name2, false);
    if (skin)
    {
        const ImageRect &rect = skin->getBorder();
        if (rect.grid[0])
        {
            Image *const image = rect.grid[0];
            const SDL_Rect &rect2 = image->mBounds;
            if (rect2.w && rect2.h)
            {
                ImageSet *const imageSet = resourceManager->getSubImageSet(
                    image, w, h);
                theme->unload(skin);
                return imageSet;
            }
        }
        theme->unload(skin);
    }
    return nullptr;
}

#define readValue(name) \
        info->name = reinterpret_cast<const char*>(\
            xmlNodeGetContent(infoNode))

#define readIntValue(name) \
        info->name = atoi(reinterpret_cast<const char*>(\
            xmlNodeGetContent(infoNode)))

#define readFloatValue(name) \
        info->name = static_cast<float>(atof(reinterpret_cast<const char*>(\
            xmlNodeGetContent(infoNode))))

ThemeInfo *Theme::loadInfo(const std::string &themeName)
{
    std::string path;
    if (themeName.empty())
    {
        path = "graphics/gui/info.xml";
    }
    else
    {
        path = std::string(defaultThemePath).append(
            themeName).append("/info.xml");
    }
    logger->log("loading: " + path);
    XML::Document doc(path, UseResman_true, SkipError_false);
    const XmlNodePtrConst rootNode = doc.rootNode();

    if (!rootNode || !xmlNameEqual(rootNode, "info"))
        return nullptr;

    ThemeInfo *const info = new ThemeInfo();

    const std::string fontSize2("fontSize_" + mScreenDensity);
    const std::string npcfontSize2("npcfontSize_" + mScreenDensity);
    for_each_xml_child_node(infoNode, rootNode)
    {
        if (xmlNameEqual(infoNode, "name"))
            readValue(name);
        else if (xmlNameEqual(infoNode, "copyright"))
            readValue(copyright);
        else if (xmlNameEqual(infoNode, "font"))
            readValue(font);
        else if (xmlNameEqual(infoNode, "boldFont"))
            readValue(boldFont);
        else if (xmlNameEqual(infoNode, "particleFont"))
            readValue(particleFont);
        else if (xmlNameEqual(infoNode, "helpFont"))
            readValue(helpFont);
        else if (xmlNameEqual(infoNode, "secureFont"))
            readValue(secureFont);
        else if (xmlNameEqual(infoNode, "npcFont"))
            readValue(npcFont);
        else if (xmlNameEqual(infoNode, "japanFont"))
            readValue(japanFont);
        else if (xmlNameEqual(infoNode, "chinaFont"))
            readValue(chinaFont);
        else if (xmlNameEqual(infoNode, "fontSize"))
            readIntValue(fontSize);
        else if (xmlNameEqual(infoNode, "npcfontSize"))
            readIntValue(npcfontSize);
        else if (xmlNameEqual(infoNode, "guialpha"))
            readFloatValue(guiAlpha);
        else if (xmlNameEqual(infoNode, fontSize2.c_str()))
            readIntValue(fontSize);
        else if (xmlNameEqual(infoNode, npcfontSize2.c_str()))
            readIntValue(npcfontSize);
    }
    return info;
}

ThemeColorIdT Theme::getIdByChar(const signed char c, bool &valid) const
{
    const CharColors::const_iterator it = mCharColors.find(c);
    if (it != mCharColors.end())
    {
        valid = true;
        return static_cast<ThemeColorIdT>((*it).second);
    }

    valid = false;
    return ThemeColorId::BROWSERBOX;
}