#pragma once

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <type_traits>

namespace yekneb {

template<std::size_t N>
struct ConstantString
{
    std::array<char, N> m_utf8{};
    std::array<wchar_t, N + 2> m_wide{};
    std::size_t m_wide_len{0};

    [[noreturn]] static consteval void fail_invalid_utf8()
    {
        throw "Invalid UTF-8 in ConstantString";
    }

    static consteval bool is_continuation_byte(const unsigned char byte)
    {
        return (byte & 0xC0U) == 0x80U;
    }

    static consteval uint32_t min_code_point_for_length(const std::size_t len)
    {
        switch (len)
        {
        case 1:
            return 0x0U;
        case 2:
            return 0x80U;
        case 3:
            return 0x800U;
        case 4:
            return 0x10000U;
        default:
            fail_invalid_utf8();
        }
    }

    static consteval void validate_code_point(const uint32_t cp, const std::size_t len)
    {
        if (cp < min_code_point_for_length(len) || cp > 0x10FFFFU || (cp >= 0xD800U && cp <= 0xDFFFU))
        {
            fail_invalid_utf8();
        }
    }

    consteval ConstantString(const char (&str)[N])
    {
        std::copy_n(str, N, m_utf8.data());
        std::size_t w_idx{0};
        for (std::size_t i{0}; m_utf8[i] != '\0'; )
        {
            std::uint32_t cp{0};
            std::uint32_t c{static_cast<unsigned char>(m_utf8[i])};
            std::size_t len{0};
            if (c <= 0x7F)
            {
                cp = c;
                len = 1;
            }
            else if ((c & 0xE0) == 0xC0)
            {
                cp = c & 0x1F;
                len = 2;
            }
            else if ((c & 0xF0) == 0xE0)
            {
                cp = c & 0x0F;
                len = 3;
            }
            else if ((c & 0xF8) == 0xF0)
            {
                cp = c & 0x07;
                len = 4;
            }
            else
            {
                fail_invalid_utf8();
            }
            if (i + len > (N - 1))
            {
                fail_invalid_utf8();
            }
            for (std::size_t j{1}; j < len; ++j)
            {
                const auto continuation{static_cast<unsigned char>(str[i + static_cast<std::size_t>(j)])};
                if (!is_continuation_byte(continuation))
                {
                    fail_invalid_utf8();
                }
                cp = (cp << 6) | (continuation & 0x3F);
            }
            validate_code_point(cp, len);
            if constexpr (sizeof(wchar_t) == 2)
            {
                if (cp <= 0xFFFF)
                {
                    m_wide[w_idx++] = static_cast<wchar_t>(cp);
                }
                else
                {
                    m_wide[w_idx++] = static_cast<wchar_t>(0xD800 + ((cp - 0x10000) >> 10));
                    m_wide[w_idx++] = static_cast<wchar_t>(0xDC00 + ((cp - 0x10000) & 0x3FF));
                }
            }
            else
            {
                m_wide[w_idx++] = static_cast<wchar_t>(cp);
            }
            i += len;
        }
        if (w_idx >= N + 2)
        {
            fail_invalid_utf8();
        }
        m_wide[w_idx] = L'\0';
        m_wide_len = w_idx;
    }

    constexpr const char* get_utf8() const
    {
        return m_utf8.data();
    }

    constexpr const wchar_t* get_wide() const
    {
        return m_wide.data();
    }

    template<typename T>
    constexpr const T* get() const
    {
        if constexpr (std::is_same_v<T, char>)
        {
            return get_utf8();
        }
        else if constexpr (std::is_same_v<T, wchar_t>)
        {
            return get_wide();
        }
        else
        {
            static_assert(
                (std::is_same_v<T, char> || std::is_same_v<T, wchar_t>),
                "ConstantString::get only supports char and wchar_t");
            return nullptr;
        }
    }

    constexpr const char* c_str() const
    {
        return get<char>();
    }

    constexpr const wchar_t* wc_str() const
    {
        return get<wchar_t>();
    }
};

template<typename CharType, std::size_t N>
constexpr const CharType* GetAs(const yekneb::ConstantString<N>& value)
{
    return value.template get<CharType>();
}

} // namespace yekneb
