/*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 "Utf8.hpp"

#include "Tools/Log.hpp"

namespace
{
    /// Returns whether \p byte is a well-formed utf8 continuation byte
    bool continuation(char byte)
    {
        return (byte & 0xc0) == 0x80; //Continuation byte of shape [10......]
    }
}

namespace Utf8
{
    std::uint8_t to_utf_32(const std::string &str,
                           std::size_t i,
                           unsigned long &target)
    {
        if (i >= str.size())
            return 0; //Index to decode in str is out of bounds

        //Extract the amount of bytes that encode the character
        std::uint8_t bytesOffset = 0;

        if ((str[i] | 0x7f) == 0x7f)      //Leading byte of shape [0.......]
            bytesOffset = 1;
        else if ((str[i] & 0xe0) == 0xc0) //Leading byte of shape [110.....]
            bytesOffset = 2;
        else if ((str[i] & 0xf0) == 0xe0) //Leading byte of shape [1110....]
            bytesOffset = 3;
        else if ((str[i] & 0xf8) == 0xf0) //Leading byte of shape [11110...]
            bytesOffset = 4;

        if (bytesOffset == 0 || i + bytesOffset > str.size())
            return 0; //Unexpected leading byte or not enough continuation bytes

        //Extract the utf32 code point from the bytesOffset bytes
        if (bytesOffset == 1)
            target = static_cast<unsigned long>(str[i]);
        else if (bytesOffset == 2 && continuation(str[i+1]))
        {
            target = (static_cast<unsigned long>(str[i]   & 0x1f) << 6)  |
                      static_cast<unsigned long>(str[i+1] & 0x3f);
        }
        else if (bytesOffset == 3 && continuation(str[i+1])
                                  && continuation(str[i+2]))
        {
            target = (static_cast<unsigned long>(str[i]   & 0x0f) << 12) |
                     (static_cast<unsigned long>(str[i+1] & 0x3f) << 6)  |
                      static_cast<unsigned long>(str[i+2] & 0x3f);
        }
        else if (bytesOffset == 4 && continuation(str[i+1])
                                  && continuation(str[i+2])
                                  && continuation(str[i+3]))
        {
            target = (static_cast<unsigned long>(str[i]   & 0x07) << 18) |
                     (static_cast<unsigned long>(str[i+1] & 0x3f) << 12) |
                     (static_cast<unsigned long>(str[i+2] & 0x3f) << 6)  |
                      static_cast<unsigned long>(str[i+3] & 0x3f);
        }
        else //Malformed sequence of continuation bytes
            return 0;

        //Check for invalid code points (surrogate half or over bounds)
        if ((target >= 0xd800 && target <= 0xdfff) || target > 0x10ffff)
            return 0;

        return bytesOffset;
    }

    ////////////////////
    ///utf8_validator///
    ////////////////////
    bool utf8_validator::accepts(unsigned long utf32_char) const
    {
        return (utf32_char > 31 && utf32_char != 127);
    }

    /////////////////
    ///utf8_string///
    /////////////////
    utf8_string::utf8_string(const char *str) : utf8_string(std::string(str)) {}

    utf8_string::utf8_string(const std::string &str)
    {
        add_bytes(str);
    }

    utf8_string utf8_string::operator+(const utf8_string &suffix) const
    {
        utf8_string concatenated = (*this);
        concatenated.m_bytes += suffix.m_bytes;
        concatenated.m_utf8CharSize.insert(concatenated.m_utf8CharSize.end(),
                                           suffix.m_utf8CharSize.begin(),
                                           suffix.m_utf8CharSize.end());

        return concatenated;
    }

    bool utf8_string::operator==(const utf8_string &other) const
    {
        return m_bytes == other.m_bytes;
    }

    void utf8_string::add_bytes(const std::string &bytes,
                                const utf8_validator &validator)
    {
        unsigned long utf32Code = 0;
        std::uint8_t offset = 0;
        std::size_t position = 0;

        while ((offset = to_utf_32(bytes, position, utf32Code)) > 0)
        {
            //Assess whether to add the character, \0 is always rejected
            if (utf32Code != 0 && validator.accepts(utf32Code))
            {
                m_bytes += bytes.substr(position, offset);
                m_utf8CharSize.push_back(offset);
            }

            position += offset;
        }
    }

    void utf8_string::pop_back()
    {
        if (!m_utf8CharSize.empty() && m_bytes.size() >= m_utf8CharSize.back())
        {
            m_bytes.erase(m_bytes.size() - m_utf8CharSize.back());
            m_utf8CharSize.pop_back();
        }
    }

    void utf8_string::clear_after(std::size_t position)
    {
        while (utf8_string::length() > position)
            utf8_string::pop_back();
    }

    void utf8_string::clear()
    {
        m_bytes.clear();
        m_utf8CharSize.clear();
    }

    bool utf8_string::empty() const
    {
        return m_bytes.empty();
    }

    std::size_t utf8_string::length() const
    {
        return m_utf8CharSize.size();
    }

    std::vector<unsigned long> utf8_string::get_utf32() const
    {
        std::vector<unsigned long> utf32;
        utf32.reserve(m_utf8CharSize.size());

        std::size_t position = 0;
        for (std::uint8_t charSize : m_utf8CharSize)
        {
            unsigned long charAsUtf32 = 0;
            if (to_utf_32(m_bytes, position, charAsUtf32) != charSize)
            {
                Log::err("Inconsistent reconstruction of utf32 sequence");
                return {};
            }

            utf32.push_back(charAsUtf32);
            position += charSize;
        }

        return utf32;
    }

    const std::string &utf8_string::get() const
    {
        return m_bytes;
    }
}
