/*
    This file is part of Msc-generator.
    Copyright (C) 2026 Gábor Németh
    Distributed under GNU Affero General Public License.

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

    Msc-generator 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/
/** @file utf8utils.cpp Utilities for UTF
 * @ingroup libcgencommon_files  */

#include "utf8utils.h"
#include <cstdint>
#include <cstring>
#include <ranges>

/** Index into the table below with the first byte of a UTF-8 sequence to
 * get the number of trailing bytes that are supposed to follow it.
 * NB: 4 and 5-byte trails are not allowed anymore, and we replace such
 * code points. */
const unsigned char numTrailingBytesForUTF8[256] = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, /* invalid nowadays: */ 4, 4, 4, 4, 5, 5, 5, 5
};

namespace {

/// Test whether the string is valid UTF-8
bool legal(std::string_view utf8) {
    for (auto c = utf8.begin(); c != utf8.end(); )
        if (auto const len = UTF8TrailingBytes(*c) + 1;
                c + len <= utf8.end()) {
            switch (len) {
                case 1: if  (0b1000'0000 & *c) return false; break;
                case 2: if ((0b1110'0000 & *c) != 0b1100'0000 || (0b1100'0000 & *(c + 1)) != 0b1000'0000) return false; break;
                case 3: if ((0b1111'0000 & *c) != 0b1110'0000 || (0b1100'0000 & *(c + 1)) != 0b1000'0000 || (0b1100'0000 & *(c + 2)) != 0b1000'0000) return false; break;
                case 4: if ((0b1111'1000 & *c) != 0b1111'0000 || (0b1100'0000 & *(c + 1)) != 0b1000'0000 || (0b1100'0000 & *(c + 2)) != 0b1000'0000 || (0b1100'0000 & *(c + 3)) != 0b1000'0000) return false; break;
                default: return false;
            }
            c += len;
        } else
            return false;
    return true;
}

}

std::string ConvertFromUTF16_to_UTF8(std::string_view utf16) {
    auto chunks = utf16.substr(0, utf16.size() / 2 * 2) | std::views::chunk(2);
    auto range = chunks | std::views::transform([](auto c) {
        uint16_t x;
        std::memcpy(&x, std::to_address(c.begin()), sizeof x);
        return x;
    });
    auto const end = range.end();
    auto u16 = range.begin();
    bool const swap = u16 == end ? false
        : *u16 == 0xfeff ? u16++ == end
        : *u16 == 0xfffe ? u16++ != end
        : false;
    std::string ret;
    ret.reserve(4 * utf16.size() / 2);
    while (u16 != end) {
        std::uint32_t c = *u16++;
        if (swap)
            c = (c >> 8) | ((c & 0xff) << 8);
        if (c >= 0xd800 && c <= 0xdbff) { // lead surrogate
            if (u16 != end) {
                auto lo = *u16++;
                if (swap)
                    lo = (lo >> 8) | (lo << 8);
                if (lo >= 0xdc00 && lo <= 0xdfff) // trail OK
                    c = 0x10000 + ((c - 0xd800) << 10) + (lo - 0xdc00);
                else // trail invalid: replace with �
                    c = 0xfffd;
            } else
                c = 0xfffd;
        }
        if (c <= 0x7f)
            ret.push_back(c);
        else if (c <= 0x7ff) {
            ret.push_back(0b1100'0000 | (c >> 6));
            ret.push_back(0b1000'0000 | (c & 0b0011'1111));
        } else if (c <= 0xffff) {
            ret.push_back(0b1110'0000 | (c >> 12));
            ret.push_back(0b1000'0000 | ((c >> 6) & 0b0011'1111));
            ret.push_back(0b1000'0000 | (c & 0b0011'1111));
        } else {
            ret.push_back(0b1111'0000 | (c >> 18));
            ret.push_back(0b1000'0000 | ((c >> 12) & 0b0011'1111));
            ret.push_back(0b1000'0000 | ((c >> 6) & 0b0011'1111));
            ret.push_back(0b1000'0000 | (c & 0b0011'1111));
        }
    }
    return ret;
}

std::wstring ConvertFromUTF8_to_UTF16(std::string_view utf8, bool add_bom) {
    std::wstring ret;
    ret.reserve(1/*bom*/ + utf8.size());
    if (add_bom)
        ret.push_back(0xfeff);
    for (auto u8 = utf8.begin(); u8 != utf8.end(); )
        if (auto const bytes = UTF8TrailingBytes(*u8) + 1;
                u8 + bytes <= utf8.end()) {
            if (legal({u8, u8 + bytes})) {
                uint32_t c = 0;
                switch (bytes) {
                    case 1: c = *u8; break;
                    case 2: c = ((0b0001'1111 & *u8) <<  6) |  (0b0011'1111 & *(u8 + 1)); if (c < 0x80) /* overlong */ c = ~0; break;
                    case 3: c = ((0b0000'1111 & *u8) << 12) | ((0b0011'1111 & *(u8 + 1)) <<  6) |  (0b0011'1111 & *(u8 + 2)); if (c < 0x800) c = ~0; break;
                    case 4: c = ((0b0000'0111 & *u8) << 18) | ((0b0011'1111 & *(u8 + 1)) << 12) | ((0b0011'1111 & *(u8 + 2)) << 6) | (0b0011'1111 & *(u8 + 3)); if (c < 0x10000) c = ~0; break;
                    default: c = ~0;
                }
                if (c <= 0xffff)
                    ret.push_back(c < 0xd800 || c > 0xdfff ? c : 0xfffd); // replace surrogates
                else if (c <= 0x10ffff) {
                    c -= 0x10000;
                    ret.push_back(0xd800 + (c >> 10));
                    ret.push_back(0xdc00 + (c & 0b11'1111'1111));
                } else
                    ret.push_back(0xfffd);
            } else
                ret.push_back(0xfffd);
            u8 += bytes;
        } else
            break;
    return ret;
}

bool ConvertToUTF8(std::string &text, bool force_as_unicode) {
    if (force_as_unicode || !legal(text)) {
        text = ConvertFromUTF16_to_UTF8(text);
        return true;
    }
    return false;
}
