/*Copyright (C) 2010-2023 Duncan Deveaux

This file is part of Hikou no mizu.

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

Hikou no mizu is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

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

#include "TextInput.hpp"
#include "Graphics/Resources/Font.hpp"

#include "Tools/ScaledPixel.hpp"

namespace
{
    /// Allows only a given range of characters into a Utf8::utf8_string
    class RangeValidator : public Utf8::utf8_validator
    {
        public:
            RangeValidator(unsigned long char_begin, unsigned long char_end) :
            Utf8::utf8_validator(), m_begin(char_begin), m_end(char_end) {}

            bool accepts(unsigned long utf32_char) const override
            {
                return (utf32_char >= m_begin && utf32_char <= m_end);
            }

        private:
            unsigned long m_begin, m_end; ///< Allowed character range
    };

    class VisibleValidator : public Utf8::utf8_validator
    {
        public:
            bool accepts(unsigned long utf32_char) const override
            {
                return Utf8::utf8_validator::accepts(utf32_char) &&
                    utf32_char != 32; //Disallow space characters
            }
    };

    /// Augments a validator by further rejecting any character
    /// that is not available in a font
    class FontValidator : public Utf8::utf8_validator
    {
        public:
            explicit FontValidator(const Utf8::utf8_validator &validator,
                                   const Font &font) :
            Utf8::utf8_validator(), m_validator(validator), m_font(font) {}

            bool accepts(unsigned long utf32_char) const override
            {
                return (m_validator.accepts(utf32_char) &&
                        m_font.hasGlyph(utf32_char));
            }

        private:
            const Utf8::utf8_validator &m_validator;
            const Font &m_font;
    };
}

TextInput::TextInput(Mode mode, std::size_t lengthLimit, float minWidth,
                     const std::string &initialText,
                     const Utf8::utf8_string &previewText,
                     Font &font, float viewWidth) :
Widget(), m_mode(mode), m_lengthLimit(lengthLimit),
m_width(minWidth), m_minWidth(minWidth),
m_height(ScaledPixel{viewWidth}(50.f)),
m_xOffset(ScaledPixel{viewWidth}(10.f)),
m_lineSize(ScaledPixel{viewWidth}(2.f)),
m_borderSize(ScaledPixel{viewWidth}(3.f)),
m_blinkPeriod(1400.f),
m_cursorColor(77, 120, 178)
{
    m_background = Polygon::rectangle(m_width, m_height,
                                      Color(255, 255, 255), m_cursorColor);
    m_background.setBorderSize(m_borderSize);

    m_cursor = Polygon::rectangle(m_lineSize, m_height * .60f,
                                  m_cursorColor);
    m_cursor.setPosition(m_xOffset + m_lineSize, m_height * .2f);
    m_cursor.setBorderSize(0.f);

    m_previewText.setText(previewText);
    m_previewText.setColor(Color(150, 150, 150));
    m_previewText.setXPosition(m_xOffset);

    m_textField.setColor(Color(25, 25, 25));
    m_textField.setXPosition(m_xOffset);

    //Append initial text
    setTextFont(font);

    appendText(initialText, m_input);
    m_input.clear_after(m_lengthLimit);

    updateText();
}

void TextInput::draw()
{
    //Update matrix
    Drawable::pushMatrix();
    Drawable::updateMatrix();

    m_background.draw();

    if (m_input.empty() && m_composition.empty()) m_previewText.draw();
    else
    {
        if (!m_composition.empty())
            m_compositionLine.draw();

        m_textField.draw();
    }

    //Blink timer handling
    if (m_blinkTimer.getElapsed() > m_blinkPeriod)
        m_blinkTimer.reset();

    if (isFocused() && m_blinkTimer.getElapsed() < m_blinkPeriod / 2.f)
        m_cursor.draw();

    Drawable::popMatrix();
}

bool TextInput::mouseRelease(const Vector &mouseCoords)
{
    Vector localMouse = Widget::localCoords(mouseCoords);

    bool focused = Widget::contains(localMouse);
    setFocus(focused);

    if (focused)
        m_blinkTimer.reset();

    return focused;
}

bool TextInput::mouseClick(const Vector &mouseCoords)
{
    Vector localMouse = Widget::localCoords(mouseCoords);
    return Widget::contains(localMouse);
}

bool TextInput::mouseMove(const Vector &mouseCoords)
{
    Vector localMouse = Widget::localCoords(mouseCoords);
    return Widget::contains(localMouse);
}

void TextInput::setTextFont(Font &font)
{
    m_textField.setFont(font);
    updateTextPosition();

    m_previewText.setFont(font);
    m_previewText.setYPosition(m_height * .64f -
                               m_previewText.getOriginHeight());
}

bool TextInput::keyEvent(const SDL_Event &event)
{
    if (!isFocused())
        return false;

    if (event.type == SDL_KEYDOWN &&
        event.key.keysym.sym == SDLK_BACKSPACE)
    {
        m_input.pop_back();
        updateText();
        return true;
    }
    else if (event.type == SDL_TEXTEDITING)
    {
        m_composition.clear_after(static_cast<std::size_t>(event.edit.start));
        appendText(std::string(event.edit.text), m_composition);
        updateText();
        return true;
    }
    else if (event.type == SDL_TEXTINPUT && m_input.length() < m_lengthLimit)
    {
        appendText(std::string(event.text.text), m_input);
        m_input.clear_after(m_lengthLimit);

        m_composition.clear(); //Clear the composition immediately
        updateText();
        return true;
    }

    return false;
}

void TextInput::appendText(const std::string &str, Utf8::utf8_string &target)
{
    const Font *font = m_textField.getFont();
    if (font == nullptr)
        return;

    if (m_mode == TextInput::Mode::AlphaNumPunct)
        target.add_bytes(str, FontValidator(RangeValidator(33, 126), (*font)));
    else if (m_mode == TextInput::Mode::Digits)
        target.add_bytes(str, FontValidator(RangeValidator(48, 57), (*font)));
    else if (m_mode == TextInput::Mode::Visible)
        target.add_bytes(str, FontValidator(VisibleValidator(), (*font)));
}

void TextInput::updateText()
{
    //Update text field
    m_textField.setText(m_input + m_composition);
    updateTextPosition();
    m_blinkTimer.reset();

    //Update composition line
    BoundingBox compBox = m_textField.getBoundingBox(m_input.length(),
                            m_input.length() + m_composition.length());

    m_compositionLine = Polygon::rectangle(compBox.right - compBox.left,
                                           m_lineSize, m_cursorColor);

    m_compositionLine.setPosition(
        m_textField.getXPosition() + compBox.left,
        m_textField.getYPosition() - compBox.top + m_lineSize * 5.f);

    m_compositionLine.setBorderSize(0.f);
}

void TextInput::updateTextPosition()
{
    m_textField.setYPosition(m_height * .64f - m_textField.getOriginHeight());
    m_cursor.setXPosition(m_textField.getXPosition() + m_textField.getWidth() +
                          m_lineSize);

    //Extend the text input if the input is too long
    float textWidth = m_textField.getWidth();
    float resizeOffset = m_xOffset * 2.f;

    if ((textWidth + resizeOffset >= m_width ||
         textWidth <= m_width - resizeOffset))
    {
        m_width = textWidth + resizeOffset;
        if (m_width < m_minWidth)
            m_width = m_minWidth;

        m_background = Polygon::rectangle(m_width, m_height,
                                          Color(255, 255, 255), m_cursorColor);
        m_background.setBorderSize(m_borderSize);
    }
}

const Utf8::utf8_string &TextInput::getInput() const
{
    return m_input;
}

float TextInput::getWidth() const
{
    return m_width;
}

float TextInput::getHeight() const
{
    return m_height;
}
