/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    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.h Utilities for international characters.
* @ingroup libcgencommon_files  */



#ifndef UTF8UTILS_H
#define UTF8UTILS_H

#include <string>
#include <limits>
#include <cassert>
#include <cstddef>

#ifndef _ASSERT
#define  _ASSERT(A) assert(A)
#elif !defined(_DEBUG) && !defined(NDEBUG)
#define NDEBUG
#endif

/** Test if the string is a valid utf8 or not.*/
bool IsValidUTF8(std::string_view utf8) noexcept;

/** Converts a series of 2-byte UTF16 characters into an UTF-8 string.
*  If length is not specified we search for a trailing (2-byte-long) zero,
*  else we use the length (in bytes). On odd length, we truncate last
*  character. */
std::string ConvertFromUTF16_to_UTF8(std::string_view utf16);

/** Converts a series of 2-byte UTF16 characters into an UTF-8 string.*/
inline std::string ConvertFromUTF16_to_UTF8(std::wstring_view utf16)
{ return ConvertFromUTF16_to_UTF8(std::string_view(reinterpret_cast<const char*>(utf16.data()), 2*utf16.length())); }

/** Converts a UTF-8 string into a series of UTF-16 characters.
*  If length is not specified we search for a trailing zero,
*  else we use the length (in bytes). On an error we ignore the resulting
*  characters. */
std::wstring ConvertFromUTF8_to_UTF16(std::string_view utf8, bool include_BOM=false);

/** Auto-detect if text is UTF8 or not. If not, convert as if it were UTF16.
* if no conversion is done we do NOT add a trailing zero, in case len is specified.
*  If length is not specified we search for a trailing (2-byte-long) zero,
*  else we use the length (in bytes). On odd length, we truncate last
*  character. The result is returned in 'text'. We allocate new memory for result,
*  and deallocate the received memory - or leave it if we need not convert.
*  Returns true if there was a conversion, false if text was already Utf8.
*  If 'force_as_unicode' is set, we always convert and return true.*/
bool ConvertToUTF8(std::string &text, bool force_as_unicode);

extern const unsigned char numTrailingBytesForUTF8[256];

/** How many bytes follow this byte in legal UTF8 for a single char?*/
inline unsigned char UTF8TrailingBytes(char c) noexcept
{
    return numTrailingBytesForUTF8[(unsigned char)c];
}

/** Returns how many characters an UTF-8 string (of a certain byte-length) contains.
 * Terminating partial characters, (e.g., a 3-byte, when we have only 2 left)
 * are counted as full.*/
inline size_t UTF8len(std::string_view text) noexcept
{
    size_t ret = 0;
    for (auto i=0U; i<text.length(); ret++)
        i += UTF8TrailingBytes(text[i])+1;
    return ret;
}

/** Returns how many characters an UTF-8 string (zero terminated) contains. 
 * Terminating partial characters, (e.g., a 3-byte, when we have only 2 left)
 * are counted as full.*/
inline size_t UTF8len(const char *text) noexcept
{
    if (text==nullptr) return 0;
    const char *s = text;
    while (*s)
        s += UTF8TrailingBytes(*s)+1;
    return s-text;
}

/** Find the first byte of character 'char_index'. In case of a too short string, we return -1.
 * In case the string terminates with a partial character (e.g., a 3-byte, when we have only 2 left),
 * and the requested character is exactly this, we silently return the byte position of its first byte.*/
inline ptrdiff_t GetUTF8ByteIndex(std::string_view utf8, size_t char_index) noexcept
{
    size_t i;
    for (i = 0; i<utf8.length() && char_index>0; char_index--) 
        i += UTF8TrailingBytes(utf8[i])+1;
    return char_index ? -1 /*too short string*/ : ptrdiff_t(i);
}

template <typename T> requires std::is_arithmetic_v<T>
constexpr T my_abs(T value) noexcept { return value < 0 ? -value : value; }

/** Returns +1 for positive, -1 for negative, 0 for zero numbers. */
template<typename T> inline int sign(T a) { return a > 0 ? +1 : a == 0 ? 0 : -1; }

 /** The position of a character inside an input file or a length of a piece of UTF8 text.
 * The indices count position in *characters* and *bytes* respectively. 
 * Since one UTF-8 character can be multiple bytes, the two may differ. 
 * Note that caret positions (cursor_pos) index the character the cursor is *before*. 
 * Note also that this class may also be used to denote a position _difference_ and 
 * in those cases the indices may be negative. */
struct CharByteIndex
{
    int char_index = 0;  ///<The (zero-based) character index of a character.
    int byte_index = 0;  ///<The (zero-based) byte index of a character's first byte
    [[nodiscard]] constexpr auto operator<=>(const CharByteIndex& o) const noexcept { auto ret = char_index <=> o.char_index; _ASSERT(ret == (byte_index <=> o.byte_index)); return ret; }
    [[nodiscard]] constexpr bool operator ==(const CharByteIndex& o) const noexcept { return (*this <=> o) == 0; }
    [[nodiscard]] constexpr bool operator <(const CharByteIndex& o) const noexcept { return (*this <=> o) < 0; }
    [[nodiscard]] constexpr bool operator <=(const CharByteIndex& o) const noexcept { return (*this <=> o) <= 0; }
    [[nodiscard]] constexpr bool operator >=(const CharByteIndex& o) const noexcept { return (*this <=> o) >= 0; }
    [[nodiscard]] constexpr bool operator >(const CharByteIndex& o) const noexcept { return (*this <=> o) > 0; }
    [[nodiscard]] constexpr bool is_null() const noexcept { _ASSERT(IsValidDiff()); return char_index==0; }
    [[nodiscard]] constexpr CharByteIndex operator +(CharByteIndex delta) const noexcept { return { char_index + delta.char_index, byte_index + delta.byte_index }; }
    [[nodiscard]] constexpr CharByteIndex operator -(CharByteIndex delta) const noexcept { return { char_index - delta.char_index, byte_index - delta.byte_index }; }
    constexpr CharByteIndex& operator +=(CharByteIndex delta) noexcept { char_index += delta.char_index; byte_index += delta.byte_index; return *this; }
    constexpr CharByteIndex& operator -=(CharByteIndex delta) noexcept { char_index -= delta.char_index; byte_index -= delta.byte_index; return *this; }
    [[nodiscard]] static CharByteIndex from(std::string_view s) noexcept { return { (int)UTF8len(s), (int)s.size() }; }
    [[nodiscard]] static CharByteIndex from(std::string_view s, int num_char) noexcept {
        if (int b = GetUTF8ByteIndex(s, num_char); 0 <= b) return { num_char, b };
        else return from(s);
    }
    [[nodiscard]] static constexpr CharByteIndex max() noexcept { return { std::numeric_limits<decltype(char_index)>::max(), std::numeric_limits<decltype(byte_index)>::max() }; }
    [[nodiscard]] static constexpr CharByteIndex invalid() noexcept { return { std::numeric_limits<decltype(char_index)>::min(), std::numeric_limits<decltype(byte_index)>::min() }; }
    /** Shifts both char and byte indices with the same number. (Assumes all characters are ASCII.)*/
    [[nodiscard]] constexpr CharByteIndex ShiftASCII(int offset) const noexcept { return CharByteIndex{ .char_index = char_index + offset, .byte_index = byte_index + offset }; }
    /** Tests if this is a nonnegative index and if the two values are sane to each other (1 char is 1-4 bytes) */
    [[nodiscard]] constexpr bool IsValid() const noexcept { return 0 <= char_index && 0 <= byte_index && char_index <= byte_index && byte_index <= char_index*4 && bool(char_index) == bool(byte_index); }
    /** Tests if this is a valid difference. indices may be negative (but has to have the same sign).*/
    [[nodiscard]] constexpr bool IsValidDiff() const noexcept { return my_abs(char_index) <= my_abs(byte_index) && my_abs(byte_index) <= my_abs(char_index)*4 && sign(char_index) == sign(byte_index); }
};


/** Find any one of the supplied ASCII characters & return char & byte index.
 * If not found, we return the total length in characters and bytes
 * (and you can easily check the byte length against text.length()).
 * On invalid UTF8 we may return a length *greater* than text.length()*/
template <typename ...CHARS> requires((std::is_same_v<CHARS, char> &&...))
inline CharByteIndex UTF8FindAnyOf(std::string_view text, CHARS... chars) {
    CharByteIndex ret;
    if (text.size()) 
        while (((text.front() != chars) && ...)) {
            const size_t len = UTF8TrailingBytes(text.front()) + 1;
            ret.char_index++;
            ret.byte_index += len;
            if (text.length() <= len) break;
            text.remove_prefix(len);
        }
    return ret;    
}


#endif
