/**
 * @file llfloateremojipicker.cpp
 *
 * $LicenseInfo:firstyear=2003&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"

#include "llfloateremojipicker.h"

#include "llappviewer.h"
#include "llbutton.h"
#include "llcombobox.h"
#include "llemojidictionary.h"
#include "llemojihelper.h"
#include "llfloaterreg.h"
#include "llkeyboard.h"
#include "llscrollcontainer.h"
#include "llscrollingpanellist.h"
#include "llscrolllistctrl.h"
#include "llscrolllistitem.h"
#include "llsdserialize.h"
#include "lltextbox.h" 
#include "llviewerchat.h" 

namespace {
// The following variables and constants are used for storing the floater state
// between different lifecycles of the floater and different sissions of the viewer

// Floater constants
static const S32 ALL_EMOJIS_GROUP_INDEX = -2;
// https://www.compart.com/en/unicode/U+1F50D
static const S32 ALL_EMOJIS_IMAGE_INDEX = 0x1F50D;
static const S32 USED_EMOJIS_GROUP_INDEX = -1;
// https://www.compart.com/en/unicode/U+23F2
static const S32 USED_EMOJIS_IMAGE_INDEX = 0x23F2;
// https://www.compart.com/en/unicode/U+1F6D1
static const S32 EMPTY_LIST_IMAGE_INDEX = 0x1F6D1;
// The following categories should follow the required alphabetic order
static const std::string RECENTLY_USED_CATEGORY = "1 recently used";
static const std::string FREQUENTLY_USED_CATEGORY = "2 frequently used";

// Floater state related variables
static std::list<llwchar> sRecentlyUsed;
static std::list<std::pair<llwchar, U32>> sFrequentlyUsed;

// State file related values
static std::string sStateFileName;
static const std::string sKeyRecentlyUsed("RecentlyUsed");
static const std::string sKeyFrequentlyUsed("FrequentlyUsed");
}

class LLEmojiGridRow : public LLScrollingPanel
{
public:
    LLEmojiGridRow(const LLPanel::Params& panel_params,
        const LLScrollingPanelList::Params& list_params)
        : LLScrollingPanel(panel_params)
        , mList(new LLScrollingPanelList(list_params))
    {
        addChild(mList);
    }

    virtual void updatePanel(BOOL allow_modify) override {}

public:
    LLScrollingPanelList* mList;
};

class LLEmojiGridDivider : public LLScrollingPanel
{
public:
    LLEmojiGridDivider(const LLPanel::Params& panel_params, std::string text)
        : LLScrollingPanel(panel_params)
        , mText(utf8string_to_wstring(text))
    {
    }

    virtual void draw() override
    {
        LLScrollingPanel::draw();

        F32 x = 4; // padding-left
        F32 y = getRect().getHeight() / 2;
        LLFontGL::getFontSansSerif()->render(
            mText,                      // wstr
            0,                          // begin_offset
            x,                          // x
            y,                          // y
            LLColor4::white,            // color
            LLFontGL::LEFT,             // halign
            LLFontGL::VCENTER,          // valign
            LLFontGL::NORMAL,           // style
            LLFontGL::DROP_SHADOW_SOFT, // shadow
            mText.size());              // max_chars
    }

    virtual void updatePanel(BOOL allow_modify) override {}

private:
    const LLWString mText;
};

class LLEmojiGridIcon : public LLScrollingPanel
{
public:
    LLEmojiGridIcon(
        const LLPanel::Params& panel_params
        , const LLEmojiSearchResult& emoji)
        : LLScrollingPanel(panel_params)
        , mData(emoji)
        , mText(LLWString(1, emoji.Character))
    {
    }

    virtual void draw() override
    {
        LLScrollingPanel::draw();

        F32 x = getRect().getWidth() / 2;
        F32 y = getRect().getHeight() / 2;
        LLFontGL::getFontEmoji()->render(
            mText,                      // wstr
            0,                          // begin_offset
            x,                          // x
            y,                          // y
            LLColor4::white,            // color
            LLFontGL::HCENTER,          // halign
            LLFontGL::VCENTER,          // valign
            LLFontGL::NORMAL,           // style
            LLFontGL::DROP_SHADOW_SOFT, // shadow
            1);                         // max_chars
    }

    virtual void updatePanel(BOOL allow_modify) override {}

    const LLEmojiSearchResult& getData() const { return mData; }
    LLWString getText() const { return mText; }

private:
    const LLEmojiSearchResult mData;
    const LLWString mText;
};

class LLEmojiPreviewPanel : public LLPanel
{
public:
    LLEmojiPreviewPanel()
        : LLPanel()
    {
    }

    void setIcon(const LLEmojiGridIcon* icon)
    {
        if (icon)
        {
            setData(icon->getData().Character, icon->getData().String, icon->getData().Begin, icon->getData().End);
        }
        else
        {
            setData(0, LLStringUtil::null, 0, 0);
        }
    }

    void setData(llwchar emoji, std::string title, size_t begin, size_t end)
    {
        mWStr = LLWString(1, emoji);
        mEmoji = emoji;
        mTitle = title;
        mBegin = begin;
        mEnd = end;
    }

    virtual void draw() override
    {
        LLPanel::draw();

        S32 clientHeight = getRect().getHeight();
        S32 clientWidth = getRect().getWidth();
        S32 iconWidth = clientHeight;

        F32 centerX = 0.5f * iconWidth;
        F32 centerY = 0.5f * clientHeight;
        drawIcon(centerX, centerY - 1, iconWidth);

        static LLColor4 defaultColor(0.75f, 0.75f, 0.75f, 1.0f);
        static LLUIColor textColor = LLUIColorTable::instance().getColor("MenuItemEnabledColor", defaultColor);
        S32 max_pixels = clientWidth - iconWidth;
        drawName(iconWidth, centerY, max_pixels, textColor.get());
    }

protected:
    void drawIcon(F32 x, F32 y, S32 max_pixels)
    {
        LLFontGL::getFontEmojiHuge()->render(
            mWStr,                      // wstr
            0,                          // begin_offset
            x,                          // x
            y,                          // y
            LLColor4::white,            // color
            LLFontGL::HCENTER,          // halign
            LLFontGL::VCENTER,          // valign
            LLFontGL::NORMAL,           // style
            LLFontGL::DROP_SHADOW_SOFT, // shadow
            1,                          // max_chars
            max_pixels);                // max_pixels
    }

    void drawName(F32 x, F32 y, S32 max_pixels, const LLColor4& color)
    {
        F32 x0 = x;
        F32 x1 = max_pixels;
        LLFontGL* font = LLFontGL::getFontEmoji();
        if (mBegin)
        {
            std::string text = mTitle.substr(0, mBegin);
            font->renderUTF8(
                text,                       // text
                0,                          // begin_offset
                x0,                         // x
                y,                          // y
                color,                      // color
                LLFontGL::LEFT,             // halign
                LLFontGL::VCENTER,          // valign
                LLFontGL::NORMAL,           // style
                LLFontGL::DROP_SHADOW_SOFT, // shadow
                text.size(),                // max_chars
                x1);                        // max_pixels
            F32 dx = font->getWidthF32(text);
            x0 += dx;
            x1 -= dx;
        }
        if (x1 > 0 && mEnd > mBegin)
        {
            std::string text = mTitle.substr(mBegin, mEnd - mBegin);
            font->renderUTF8(
                text,                       // text
                0,                          // begin_offset
                x0,                         // x
                y,                          // y
                LLColor4::yellow6,          // color
                LLFontGL::LEFT,             // halign
                LLFontGL::VCENTER,          // valign
                LLFontGL::NORMAL,           // style
                LLFontGL::DROP_SHADOW_SOFT, // shadow
                text.size(),                // max_chars
                x1);                        // max_pixels
            F32 dx = font->getWidthF32(text);
            x0 += dx;
            x1 -= dx;
        }
        if (x1 > 0 && mEnd < mTitle.size())
        {
            std::string text = mEnd ? mTitle.substr(mEnd) : mTitle;
            font->renderUTF8(
                text,                       // text
                0,                          // begin_offset
                x0,                         // x
                y,                          // y
                color,                      // color
                LLFontGL::LEFT,             // halign
                LLFontGL::VCENTER,          // valign
                LLFontGL::NORMAL,           // style
                LLFontGL::DROP_SHADOW_SOFT, // shadow
                text.size(),                // max_chars
                x1);                        // max_pixels
        }
    }

private:
    llwchar mEmoji;
    LLWString mWStr;
    std::string mTitle;
    size_t mBegin;
    size_t mEnd;
};

LLFloaterEmojiPicker::LLFloaterEmojiPicker(const LLSD& key)
: super(key)
{
    // This floater should hover on top of our dependent (with the dependent having the focus)
    setFocusStealsFrontmost(FALSE);
    setBackgroundVisible(FALSE);
    setAutoFocus(FALSE);

    loadState();
}

BOOL LLFloaterEmojiPicker::postBuild()
{
    mGroups = getChild<LLPanel>("Groups");
    mBadge = getChild<LLPanel>("Badge");
    mEmojiScroll = getChild<LLScrollContainer>("EmojiGridContainer");
    mEmojiGrid = getChild<LLScrollingPanelList>("EmojiGrid");
    mDummy = getChild<LLTextBox>("Dummy");

    mPreview = new LLEmojiPreviewPanel();
    mPreview->setVisible(FALSE);
    addChild(mPreview);

    return LLFloater::postBuild();
}

void LLFloaterEmojiPicker::onOpen(const LLSD& key)
{
    mHint = key["hint"].asString();

    LLEmojiHelper::instance().setIsHideDisabled(mHint.empty());
    mFilterPattern = mHint;

    initialize();

    gFloaterView->adjustToFitScreen(this, FALSE);
}

void LLFloaterEmojiPicker::dirtyRect()
{
    super::dirtyRect();

    if (!mPreview)
        return;

    const S32 HPADDING = 4;
    const S32 VOFFSET = 12;
    LLRect rect(HPADDING, mDummy->getRect().mTop + 6, getRect().getWidth() - HPADDING, VOFFSET);
    if (mPreview->getRect() != rect)
    {
        mPreview->setRect(rect);
    }

    if (mEmojiScroll && mEmojiGrid)
    {
        S32 outer_width = mEmojiScroll->getRect().getWidth();
        S32 inner_width = mEmojiGrid->getRect().getWidth();
        if (outer_width != inner_width)
        {
            resizeGroupButtons();
            fillEmojis(true);
        }
    }
}

void LLFloaterEmojiPicker::initialize()
{
    S32 groupIndex = mSelectedGroupIndex && mSelectedGroupIndex <= mFilteredEmojiGroups.size() ?
        mFilteredEmojiGroups[mSelectedGroupIndex - 1] : ALL_EMOJIS_GROUP_INDEX;

    fillGroups();

    if (mFilteredEmojis.empty())
    {
        if (!mHint.empty())
        {
            hideFloater();
            return;
        }

        mGroups->setVisible(FALSE);
        mFocusedIconRow = -1;
        mFocusedIconCol = -1;
        mFocusedIcon = nullptr;
        mHoveredIcon = nullptr;
        mEmojiScroll->goToTop();
        mEmojiGrid->clearPanels();

        if (mFilterPattern.empty())
        {
            showPreview(false);
        }
        else
        {
            const std::string prompt("No emoji found for ");
            std::string title(prompt + '"' + mFilterPattern.substr(1) + '"');
            mPreview->setData(EMPTY_LIST_IMAGE_INDEX, title, prompt.size() + 1, title.size() - 1);
            showPreview(true);
        }
        return;
    }

    mGroups->setVisible(TRUE);
    mPreview->setIcon(nullptr);
    showPreview(true);

    mSelectedGroupIndex = groupIndex == ALL_EMOJIS_GROUP_INDEX ? 0 :
        (1 + std::distance(mFilteredEmojiGroups.begin(),
            std::find(mFilteredEmojiGroups.begin(), mFilteredEmojiGroups.end(), groupIndex))) %
        (1 + mFilteredEmojiGroups.size());

    mGroupButtons[mSelectedGroupIndex]->setToggleState(TRUE);
    mGroupButtons[mSelectedGroupIndex]->setUseFontColor(TRUE);

    fillEmojis();
}

void LLFloaterEmojiPicker::fillGroups()
{
    // Do not use deleteAllChildren() because mBadge shouldn't be removed
    for (LLButton* button : mGroupButtons)
    {
        mGroups->removeChild(button);
        delete button;
    }
    mFilteredEmojiGroups.clear();
    mFilteredEmojis.clear();
    mGroupButtons.clear();

    LLButton::Params params;
    params.font = LLFontGL::getFontEmoji();

    LLRect rect;
    rect.mTop = mGroups->getRect().getHeight();
    rect.mBottom = mBadge->getRect().getHeight();

    // Create button for "All categories"
    params.name = "All categories";
    createGroupButton(params, rect, ALL_EMOJIS_IMAGE_INDEX);

    // Create group and button for "Recently used" and/or "Frequently used"
    if (!sRecentlyUsed.empty() || !sFrequentlyUsed.empty())
    {
        std::map<std::string, std::vector<LLEmojiSearchResult>> cats;
        fillCategoryRecentlyUsed(cats);
        fillCategoryFrequentlyUsed(cats);

        if (!cats.empty())
        {
            mFilteredEmojiGroups.push_back(USED_EMOJIS_GROUP_INDEX);
            mFilteredEmojis.emplace_back(cats);
            params.name = "Used categories";
            createGroupButton(params, rect, USED_EMOJIS_IMAGE_INDEX);
        }
    }

    const std::vector<LLEmojiGroup>& groups = LLEmojiDictionary::instance().getGroups();

    // List all categories in the dictionary
    for (U32 i = 0; i < groups.size(); ++i)
    {
        std::map<std::string, std::vector<LLEmojiSearchResult>> cats;

        fillGroupEmojis(cats, i);

        if (!cats.empty())
        {
            mFilteredEmojiGroups.push_back(i);
            mFilteredEmojis.emplace_back(cats);
            params.name = "Group " + std::to_string(i);
            createGroupButton(params, rect, groups[i].Character);
        }
    }

    resizeGroupButtons();
}

void LLFloaterEmojiPicker::fillCategoryRecentlyUsed(std::map<std::string, std::vector<LLEmojiSearchResult>>& cats)
{
    if (sRecentlyUsed.empty())
        return;

    std::vector<LLEmojiSearchResult> emojis;

    // In case of empty mFilterPattern we'd use sRecentlyUsed directly
    if (!mFilterPattern.empty())
    {
        // List all emojis in "Recently used"
        const LLEmojiDictionary::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr();
        std::size_t begin, end;
        for (llwchar emoji : sRecentlyUsed)
        {
            auto e2d = emoji2descr.find(emoji);
            if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
            {
                const std::string shortcode(e2d->second->ShortCodes.front());
                if (LLEmojiDictionary::searchInShortCode(begin, end, shortcode, mFilterPattern))
                {
                    emojis.emplace_back(emoji, shortcode, begin, end);
                }
            }
        }
        if (emojis.empty())
            return;
    }

    cats.emplace(std::make_pair(RECENTLY_USED_CATEGORY, emojis));
}

void LLFloaterEmojiPicker::fillCategoryFrequentlyUsed(std::map<std::string, std::vector<LLEmojiSearchResult>>& cats)
{
    if (sFrequentlyUsed.empty())
        return;

    std::vector<LLEmojiSearchResult> emojis;

    // In case of empty mFilterPattern we'd use sFrequentlyUsed directly
    if (!mFilterPattern.empty())
    {
        // List all emojis in "Frequently used"
        const LLEmojiDictionary::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr();
        std::size_t begin, end;
        for (const auto& emoji : sFrequentlyUsed)
        {
            auto e2d = emoji2descr.find(emoji.first);
            if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
            {
                const std::string shortcode(e2d->second->ShortCodes.front());
                if (LLEmojiDictionary::searchInShortCode(begin, end, shortcode, mFilterPattern))
                {
                    emojis.emplace_back(emoji.first, shortcode, begin, end);
                }
            }
        }
        if (emojis.empty())
            return;
    }

    cats.emplace(std::make_pair(FREQUENTLY_USED_CATEGORY, emojis));
}

void LLFloaterEmojiPicker::fillGroupEmojis(std::map<std::string, std::vector<LLEmojiSearchResult>>& cats, U32 index)
{
    const std::vector<LLEmojiGroup>& groups = LLEmojiDictionary::instance().getGroups();
    const LLEmojiDictionary::cat2descrs_map_t& category2Descr = LLEmojiDictionary::instance().getCategory2Descrs();

    for (const std::string& category : groups[index].Categories)
    {
        const LLEmojiDictionary::cat2descrs_map_t::const_iterator& c2d = category2Descr.find(category);
        if (c2d == category2Descr.end())
            continue;

        std::vector<LLEmojiSearchResult> emojis;

        // In case of empty mFilterPattern we'd use category2Descr directly
        if (!mFilterPattern.empty())
        {
            // List all emojis in category
            std::size_t begin, end;
            for (const LLEmojiDescriptor* descr : c2d->second)
            {
                if (!descr->ShortCodes.empty())
                {
                    const std::string shortcode(descr->ShortCodes.front());
                    if (LLEmojiDictionary::searchInShortCode(begin, end, shortcode, mFilterPattern))
                    {
                        emojis.emplace_back(descr->Character, shortcode, begin, end);
                    }
                }
            }
            if (emojis.empty())
                continue;
        }

        cats.emplace(std::make_pair(category, emojis));
    }
}

void LLFloaterEmojiPicker::createGroupButton(LLButton::Params& params, const LLRect& rect, llwchar emoji)
{
    LLButton* button = LLUICtrlFactory::create<LLButton>(params);
    button->setClickedCallback([this](LLUICtrl* ctrl, const LLSD&) { onGroupButtonClick(ctrl); });
    button->setMouseEnterCallback([this](LLUICtrl* ctrl, const LLSD&) { onGroupButtonMouseEnter(ctrl); });
    button->setMouseLeaveCallback([this](LLUICtrl* ctrl, const LLSD&) { onGroupButtonMouseLeave(ctrl); });

    button->setRect(rect);
    button->setTabStop(FALSE);
    button->setLabel(LLUIString(LLWString(1, emoji)));
    button->setUseFontColor(FALSE);

    mGroupButtons.push_back(button);
    mGroups->addChild(button);
}

void LLFloaterEmojiPicker::resizeGroupButtons()
{
    U32 groupCount = (U32)mGroupButtons.size();
    if (!groupCount)
        return;

    S32 totalWidth = mGroups->getRect().getWidth();
    S32 badgeWidth = totalWidth / groupCount;
    S32 leftOffset = (totalWidth - badgeWidth * groupCount) / 2;

    for (U32 i = 0; i < groupCount; ++i)
    {
        LLRect rect = mGroupButtons[i]->getRect();
        rect.mLeft = leftOffset + badgeWidth * i;
        rect.mRight = rect.mLeft + badgeWidth;
        mGroupButtons[i]->setRect(rect);
    }

    LLRect rect = mBadge->getRect();
    rect.mLeft = leftOffset + badgeWidth * mSelectedGroupIndex;
    rect.mRight = rect.mLeft + badgeWidth;
    mBadge->setRect(rect);
}

void LLFloaterEmojiPicker::selectEmojiGroup(U32 index)
{
    if (index == mSelectedGroupIndex || index >= mGroupButtons.size())
        return;

    if (mSelectedGroupIndex < mGroupButtons.size())
    {
        mGroupButtons[mSelectedGroupIndex]->setUseFontColor(FALSE);
        mGroupButtons[mSelectedGroupIndex]->setToggleState(FALSE);
    }

    mSelectedGroupIndex = index;
    mGroupButtons[mSelectedGroupIndex]->setToggleState(TRUE);
    mGroupButtons[mSelectedGroupIndex]->setUseFontColor(TRUE);

    LLButton* button = mGroupButtons[mSelectedGroupIndex];
    LLRect rect = mBadge->getRect();
    rect.mLeft = button->getRect().mLeft;
    rect.mRight = button->getRect().mRight;
    mBadge->setRect(rect);

    fillEmojis();
}

void LLFloaterEmojiPicker::fillEmojis(bool fromResize)
{
    S32 scrollbar_size = mEmojiScroll->getSize();
    if (scrollbar_size < 0)
    {
        static LLUICachedControl<S32> scrollbar_size_control("UIScrollbarSize", 0);
        scrollbar_size = scrollbar_size_control;
    }

    const S32 scroll_width = mEmojiScroll->getRect().getWidth();
    const S32 client_width = scroll_width - scrollbar_size - mEmojiScroll->getBorderWidth() * 2;
    const S32 grid_padding = mEmojiGrid->getPadding();
    const S32 icon_spacing = mEmojiGrid->getSpacing();
    const S32 row_width = client_width - grid_padding * 2;
    const S32 icon_size = 28; // icon width and height
    const S32 max_icons = llmax(1, (row_width + icon_spacing) / (icon_size + icon_spacing));

    // Optimization: don't rearrange for different widths with the same maxIcons
    if (fromResize && (max_icons == mRecentMaxIcons))
        return;

    mRecentMaxIcons = max_icons;

    mFocusedIconRow = 0;
    mFocusedIconCol = 0;
    mFocusedIcon = nullptr;
    mHoveredIcon = nullptr;
    mEmojiScroll->goToTop();
    mEmojiGrid->clearPanels();
    mPreview->setIcon(nullptr);

    if (mEmojiGrid->getRect().getWidth() != client_width)
    {
        LLRect rect = mEmojiGrid->getRect();
        rect.mRight = rect.mLeft + client_width;
        mEmojiGrid->setRect(rect);
    }

    LLPanel::Params row_panel_params;
    row_panel_params.rect = LLRect(0, icon_size, row_width, 0);

    LLScrollingPanelList::Params row_list_params;
    row_list_params.rect = row_panel_params.rect;
    row_list_params.is_horizontal = TRUE;
    row_list_params.padding = 0;
    row_list_params.spacing = icon_spacing;

    LLPanel::Params icon_params;
    LLRect icon_rect(0, icon_size, icon_size, 0);

    static LLColor4 default_color(0.75f, 0.75f, 0.75f, 1.0f);
    LLColor4 bg_color = LLUIColorTable::instance().getColor("MenuItemHighlightBgColor", default_color);

    if (!mSelectedGroupIndex)
    {
        // List all groups
        for (const auto& group : mFilteredEmojis)
        {
            // List all categories in the group
            for (const auto& category : group)
            {
                // List all emojis in the category
                fillEmojisCategory(category.second, category.first, row_panel_params,
                    row_list_params, icon_params, icon_rect, max_icons, bg_color);
            }
        }
    }
    else
    {
        // List all categories in the selected group
        const auto& group = mFilteredEmojis[mSelectedGroupIndex - 1];
        for (const auto& category : group)
        {
            // List all emojis in the category
            fillEmojisCategory(category.second, category.first, row_panel_params,
                row_list_params, icon_params, icon_rect, max_icons, bg_color);
        }
    }

    if (mEmojiGrid->getPanelList().empty())
    {
        showPreview(false);
        mFocusedIconRow = -1;
        mFocusedIconCol = -1;
        if (!mHint.empty())
        {
            hideFloater();
        }
    }
    else
    {
        showPreview(true);
        mFocusedIconRow = 0;
        mFocusedIconCol = 0;
        moveFocusedIconNext();
    }
}

void LLFloaterEmojiPicker::fillEmojisCategory(const std::vector<LLEmojiSearchResult>& emojis,
    const std::string& category, const LLPanel::Params& row_panel_params, const LLUICtrl::Params& row_list_params,
    const LLPanel::Params& icon_params, const LLRect& icon_rect, S32 max_icons, const LLColor4& bg)
{
    // Place the category title
    std::string title =
        category == RECENTLY_USED_CATEGORY ? getString("title_for_recently_used") :
        category == FREQUENTLY_USED_CATEGORY ? getString("title_for_frequently_used") :
        isupper(category.front()) ? category : LLStringUtil::capitalize(category);
    LLEmojiGridDivider* div = new LLEmojiGridDivider(row_panel_params, title);
    mEmojiGrid->addPanel(div, true);

    int icon_index = 0;
    LLEmojiGridRow* row = nullptr;

    if (mFilterPattern.empty())
    {
        const LLEmojiDictionary::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr();
        LLEmojiSearchResult emoji { 0, "", 0, 0 };
        if (category == RECENTLY_USED_CATEGORY)
        {
            for (llwchar code : sRecentlyUsed)
            {
                const LLEmojiDictionary::emoji2descr_map_t::const_iterator& e2d = emoji2descr.find(code);
                if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
                {
                    emoji.Character = code;
                    emoji.String = e2d->second->ShortCodes.front();
                    createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
                        icon_rect, max_icons, bg, row, icon_index);
                }
            }
        }
        else if (category == FREQUENTLY_USED_CATEGORY)
        {
            for (const auto& code : sFrequentlyUsed)
            {
                const LLEmojiDictionary::emoji2descr_map_t::const_iterator& e2d = emoji2descr.find(code.first);
                if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
                {
                    emoji.Character = code.first;
                    emoji.String = e2d->second->ShortCodes.front();
                    createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
                        icon_rect, max_icons, bg, row, icon_index);
                }
            }
        }
        else
        {
            const LLEmojiDictionary::cat2descrs_map_t& category2Descr = LLEmojiDictionary::instance().getCategory2Descrs();
            const LLEmojiDictionary::cat2descrs_map_t::const_iterator& c2d = category2Descr.find(category);
            if (c2d != category2Descr.end())
            {
                for (const LLEmojiDescriptor* descr : c2d->second)
                {
                    emoji.Character = descr->Character;
                    emoji.String = descr->ShortCodes.front();
                    createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
                        icon_rect, max_icons, bg, row, icon_index);
                }
            }
        }
    }
    else
    {
        for (const LLEmojiSearchResult& emoji : emojis)
        {
            createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
                icon_rect, max_icons, bg, row, icon_index);
        }
    }
}

void LLFloaterEmojiPicker::createEmojiIcon(const LLEmojiSearchResult& emoji,
    const std::string& category, const LLPanel::Params& row_panel_params, const LLUICtrl::Params& row_list_params,
    const LLPanel::Params& icon_params, const LLRect& icon_rect, S32 max_icons, const LLColor4& bg,
    LLEmojiGridRow*& row, int& icon_index)
{
    // Place a new row each (max_icons) icons
    if (!(icon_index % max_icons))
    {
        row = new LLEmojiGridRow(row_panel_params, *(const LLScrollingPanelList::Params*)&row_list_params);
        mEmojiGrid->addPanel(row, true);
    }

    // Place a new icon to the current row
    LLEmojiGridIcon* icon = new LLEmojiGridIcon(icon_params, emoji);
    icon->setMouseEnterCallback([this](LLUICtrl* ctrl, const LLSD&) { onEmojiMouseEnter(ctrl); });
    icon->setMouseLeaveCallback([this](LLUICtrl* ctrl, const LLSD&) { onEmojiMouseLeave(ctrl); });
    icon->setMouseDownCallback([this](LLUICtrl* ctrl, S32, S32, MASK) { onEmojiMouseDown(ctrl); });
    icon->setMouseUpCallback([this](LLUICtrl* ctrl, S32, S32, MASK) { onEmojiMouseUp(ctrl); });
    icon->setBackgroundColor(bg);
    icon->setBackgroundOpaque(1);
    icon->setRect(icon_rect);
    row->mList->addPanel(icon, true);

    icon_index++;
}

void LLFloaterEmojiPicker::showPreview(bool show)
{
    //mPreview->setIcon(nullptr);
    mDummy->setVisible(show ? FALSE : TRUE);
    mPreview->setVisible(show ? TRUE : FALSE);
}

void LLFloaterEmojiPicker::onGroupButtonClick(LLUICtrl* ctrl)
{
    if (LLButton* button = dynamic_cast<LLButton*>(ctrl))
    {
        if (button == mGroupButtons[mSelectedGroupIndex] || button->getToggleState())
            return;

        auto it = std::find(mGroupButtons.begin(), mGroupButtons.end(), button);
        if (it == mGroupButtons.end())
            return;

        selectEmojiGroup(it - mGroupButtons.begin());
    }
}

void LLFloaterEmojiPicker::onGroupButtonMouseEnter(LLUICtrl* ctrl)
{
    if (LLButton* button = dynamic_cast<LLButton*>(ctrl))
    {
        button->setUseFontColor(TRUE);
    }
}

void LLFloaterEmojiPicker::onGroupButtonMouseLeave(LLUICtrl* ctrl)
{
    if (LLButton* button = dynamic_cast<LLButton*>(ctrl))
    {
        button->setUseFontColor(button->getToggleState());
    }
}

void LLFloaterEmojiPicker::onEmojiMouseEnter(LLUICtrl* ctrl)
{
    if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))
    {
        if (mFocusedIcon && mFocusedIcon != icon && mFocusedIcon->isBackgroundVisible())
        {
            unselectGridIcon(mFocusedIcon);
        }

        if (mHoveredIcon && mHoveredIcon != icon)
        {
            unselectGridIcon(mHoveredIcon);
        }

        selectGridIcon(icon);

        mHoveredIcon = icon;
    }
}

void LLFloaterEmojiPicker::onEmojiMouseLeave(LLUICtrl* ctrl)
{
    if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))
    {
        if (icon == mHoveredIcon)
        {
            if (icon != mFocusedIcon)
            {
                unselectGridIcon(icon);
            }
            mHoveredIcon = nullptr;
        }

        if (!mHoveredIcon && mFocusedIcon && !mFocusedIcon->isBackgroundVisible())
        {
            selectGridIcon(mFocusedIcon);
        }
    }
}

void LLFloaterEmojiPicker::onEmojiMouseDown(LLUICtrl* ctrl)
{
    if (getSoundFlags() & MOUSE_DOWN)
    {
        make_ui_sound("UISndClick");
    }
}

void LLFloaterEmojiPicker::onEmojiMouseUp(LLUICtrl* ctrl)
{
    if (getSoundFlags() & MOUSE_UP)
    {
        make_ui_sound("UISndClickRelease");
    }

    if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))
    {
        LLSD value(wstring_to_utf8str(icon->getText()));
        setValue(value);

        onCommit();

        if (!mHint.empty() || !(gKeyboard->currentMask(TRUE) & MASK_SHIFT))
        {
            hideFloater();
        }
    }
}

void LLFloaterEmojiPicker::selectFocusedIcon()
{
    if (mFocusedIcon && mFocusedIcon != mHoveredIcon)
    {
        unselectGridIcon(mFocusedIcon);
    }

    // Both mFocusedIconRow and mFocusedIconCol should be already verified
    LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(mEmojiGrid->getPanelList()[mFocusedIconRow]);
    mFocusedIcon = row ? dynamic_cast<LLEmojiGridIcon*>(row->mList->getPanelList()[mFocusedIconCol]) : nullptr;

    if (mFocusedIcon && !mHoveredIcon)
    {
        selectGridIcon(mFocusedIcon);
    }
}

bool LLFloaterEmojiPicker::moveFocusedIconUp()
{
    for (S32 i = mFocusedIconRow - 1; i >= 0; --i)
    {
        LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
        LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
        if (row && row->mList->getPanelList().size() > mFocusedIconCol)
        {
            mEmojiScroll->scrollToShowRect(row->getBoundingRect());
            mFocusedIconRow = i;
            selectFocusedIcon();
            return true;
        }
    }

    return false;
}

bool LLFloaterEmojiPicker::moveFocusedIconDown()
{
    S32 rowCount = mEmojiGrid->getPanelList().size();
    for (S32 i = mFocusedIconRow + 1; i < rowCount; ++i)
    {
        LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
        LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
        if (row && row->mList->getPanelList().size() > mFocusedIconCol)
        {
            mEmojiScroll->scrollToShowRect(row->getBoundingRect());
            mFocusedIconRow = i;
            selectFocusedIcon();
            return true;
        }
    }

    return false;
}

bool LLFloaterEmojiPicker::moveFocusedIconPrev()
{
    if (mHoveredIcon)
        return false;

    if (mFocusedIconCol > 0)
    {
        mFocusedIconCol--;
        selectFocusedIcon();
        return true;
    }

    for (S32 i = mFocusedIconRow - 1; i >= 0; --i)
    {
        LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
        LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
        if (row && row->mList->getPanelList().size())
        {
            mEmojiScroll->scrollToShowRect(row->getBoundingRect());
            mFocusedIconCol = row->mList->getPanelList().size() - 1;
            mFocusedIconRow = i;
            selectFocusedIcon();
            return true;
        }
    }

    return false;
}

bool LLFloaterEmojiPicker::moveFocusedIconNext()
{
    if (mHoveredIcon)
        return false;

    LLScrollingPanel* panel = mEmojiGrid->getPanelList()[mFocusedIconRow];
    LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
    S32 colCount = row ? row->mList->getPanelList().size() : 0;
    if (mFocusedIconCol < colCount - 1)
    {
        mFocusedIconCol++;
        selectFocusedIcon();
        return true;
    }

    S32 rowCount = mEmojiGrid->getPanelList().size();
    for (S32 i = mFocusedIconRow + 1; i < rowCount; ++i)
    {
        LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
        LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
        if (row && row->mList->getPanelList().size())
        {
            mEmojiScroll->scrollToShowRect(row->getBoundingRect());
            mFocusedIconCol = 0;
            mFocusedIconRow = i;
            selectFocusedIcon();
            return true;
        }
    }

    return false;
}

void LLFloaterEmojiPicker::selectGridIcon(LLEmojiGridIcon* icon)
{
    icon->setBackgroundVisible(TRUE);
    mPreview->setIcon(icon);
}

void LLFloaterEmojiPicker::unselectGridIcon(LLEmojiGridIcon* icon)
{
    icon->setBackgroundVisible(FALSE);
    mPreview->setIcon(nullptr);
}

// virtual
BOOL LLFloaterEmojiPicker::handleKey(KEY key, MASK mask, BOOL called_from_parent)
{
    if (mask == MASK_NONE)
    {
        switch (key)
        {
        case KEY_UP:
            moveFocusedIconUp();
            return TRUE;
        case KEY_DOWN:
            moveFocusedIconDown();
            return TRUE;
        case KEY_LEFT:
            moveFocusedIconPrev();
            return TRUE;
        case KEY_RIGHT:
            moveFocusedIconNext();
            return TRUE;
        case KEY_ESCAPE:
            hideFloater();
            return TRUE;
        }
    }

    if (mask == MASK_ALT)
    {
        switch (key)
        {
        case KEY_LEFT:
            selectEmojiGroup((mSelectedGroupIndex + mFilteredEmojis.size()) % mGroupButtons.size());
            return TRUE;
        case KEY_RIGHT:
            selectEmojiGroup((mSelectedGroupIndex + 1) % mGroupButtons.size());
            return TRUE;
        }
    }

    if (key == KEY_RETURN)
    {
        U64 time = totalTime();
        // <Shift+Return> comes twice for unknown reason
        if (mFocusedIcon && (time - mRecentReturnPressedMs > 100000)) // Min interval 0.1 sec.
        {
            onEmojiMouseDown(mFocusedIcon);
            onEmojiMouseUp(mFocusedIcon);
        }
        mRecentReturnPressedMs = time;
        return TRUE;
    }

    if (mHint.empty())
    {
        if (key >= 0x20 && key < 0x80)
        {
            if (!mEmojiGrid->getPanelList().empty())
            {
                if (mFilterPattern.empty())
                {
                    mFilterPattern = ":";
                }
                mFilterPattern += (char)key;
                initialize();
            }
            return TRUE;
        }
        else if (key == KEY_BACKSPACE)
        {
            if (!mFilterPattern.empty())
            {
                mFilterPattern.pop_back();
                if (mFilterPattern == ":")
                {
                    mFilterPattern.clear();
                }
                initialize();
            }
            return TRUE;
        }
    }

    return super::handleKey(key, mask, called_from_parent);
}

// virtual
void LLFloaterEmojiPicker::goneFromFront()
{
    hideFloater();
}

void LLFloaterEmojiPicker::hideFloater() const
{
    LLEmojiHelper::instance().hideHelper(nullptr, true);
}

// static
std::list<llwchar>& LLFloaterEmojiPicker::getRecentlyUsed()
{
    loadState();
    return sRecentlyUsed;
}

// static
void LLFloaterEmojiPicker::onEmojiUsed(llwchar emoji)
{
    // Update sRecentlyUsed
    auto itr = std::find(sRecentlyUsed.begin(), sRecentlyUsed.end(), emoji);
    if (itr == sRecentlyUsed.end())
    {
        sRecentlyUsed.push_front(emoji);
    }
    else if (itr != sRecentlyUsed.begin())
    {
        sRecentlyUsed.erase(itr);
        sRecentlyUsed.push_front(emoji);
    }

    // Increment and reorder sFrequentlyUsed
    auto itf = sFrequentlyUsed.begin();
    while (itf != sFrequentlyUsed.end())
    {
        if (itf->first == emoji)
        {
            itf->second++;
            while (itf != sFrequentlyUsed.begin())
            {
                auto prior = itf;
                prior--;
                if (prior->second > itf->second)
                    break;
                prior->swap(*itf);
                itf = prior;
            }
            break;
        }
        itf++;
    }
    // Append new if not found
    if (itf == sFrequentlyUsed.end())
    {
        // Insert before others with count == 1
        while (itf != sFrequentlyUsed.begin())
        {
            auto prior = itf;
            prior--;
            if (prior->second > 1)
                break;
            itf = prior;
        }
        sFrequentlyUsed.insert(itf, std::make_pair(emoji, 1));
    }
}

// static
void LLFloaterEmojiPicker::loadState()
{
    if (!sStateFileName.empty())
        return; // Already loaded

    sStateFileName = gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, "emoji_floater_state.xml");

    llifstream file;
    file.open(sStateFileName.c_str());
    if (!file.is_open())
    {
        LL_WARNS() << "Emoji floater state file is missing or inaccessible: " << sStateFileName << LL_ENDL;
        return;
    }

    LLSD state;
    LLSDSerialize::fromXML(state, file);
    if (state.isUndefined())
    {
        LL_WARNS() << "Emoji floater state file is missing or ill-formed: " << sStateFileName << LL_ENDL;
        return;
    }

    // Load and parse sRecentlyUsed
    std::string recentlyUsed = state[sKeyRecentlyUsed];
    std::vector<std::string> rtokens = LLStringUtil::getTokens(recentlyUsed, ",");
    int maxCountR = 20;
    for (const std::string& token : rtokens)
    {
        llwchar emoji = (llwchar)atoi(token.c_str());
        if (std::find(sRecentlyUsed.begin(), sRecentlyUsed.end(), emoji) == sRecentlyUsed.end())
        {
            sRecentlyUsed.push_back(emoji);
            if (!--maxCountR)
                break;
        }
    }

    // Load and parse sFrequentlyUsed
    std::string frequentlyUsed = state[sKeyFrequentlyUsed];
    std::vector<std::string> ftokens = LLStringUtil::getTokens(frequentlyUsed, ",");
    int maxCountF = 20;
    for (const std::string& token : ftokens)
    {
        std::vector<std::string> pair = LLStringUtil::getTokens(token, ":");
        if (pair.size() == 2)
        {
            llwchar emoji = (llwchar)atoi(pair[0].c_str());
            if (emoji)
            {
                U32 count = atoi(pair[1].c_str());
                auto it = std::find_if(sFrequentlyUsed.begin(), sFrequentlyUsed.end(),
                    [emoji](std::pair<llwchar, U32>& it) { return it.first == emoji; });
                if (it != sFrequentlyUsed.end())
                {
                    it->second += count;
                }
                else
                {
                    sFrequentlyUsed.push_back(std::make_pair(emoji, count));
                    if (!--maxCountF)
                        break;
                }
            }
        }
    }

    // Normalize by minimum
    if (!sFrequentlyUsed.empty())
    {
        U32 delta = sFrequentlyUsed.back().second - 1;
        for (auto& it : sFrequentlyUsed)
        {
            it.second = std::max((U32)0, it.second - delta);
        }
    }
}

// static
void LLFloaterEmojiPicker::saveState()
{
    if (sStateFileName.empty())
        return; // Not loaded

    if (LLAppViewer::instance()->isSecondInstance())
        return; // Not allowed

    LLSD state = LLSD::emptyMap();

    if (!sRecentlyUsed.empty())
    {
        U32 maxCount = 20;
        std::string recentlyUsed;
        for (llwchar emoji : sRecentlyUsed)
        {
            if (!recentlyUsed.empty())
                recentlyUsed += ",";
            char buffer[32];
            sprintf(buffer, "%u", (U32)emoji);
            recentlyUsed += buffer;
            if (!--maxCount)
                break;
        }
        state[sKeyRecentlyUsed] = recentlyUsed;
    }

    if (!sFrequentlyUsed.empty())
    {
        U32 maxCount = 20;
        std::string frequentlyUsed;
        for (auto& it : sFrequentlyUsed)
        {
            if (!frequentlyUsed.empty())
                frequentlyUsed += ",";
            char buffer[32];
            sprintf(buffer, "%u:%u", (U32)it.first, (U32)it.second);
            frequentlyUsed += buffer;
            if (!--maxCount)
                break;
        }
        state[sKeyFrequentlyUsed] = frequentlyUsed;
    }

    llofstream stream(sStateFileName.c_str());
    LLSDSerialize::toPrettyXML(state, stream);
}
