#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstdarg>
#include <cwchar>
#include <bit>

#include "wstring.hh"

#if USE_ICONV
#include <iconv.h>

static const char* midset = std::endian::native == std::endian::little ? "UCS-4LE" : "UCS-4BE";

#else
/* not iconv */
#include <clocale>
#include <cstdlib>
#include <string>
#include <cwchar>
#include <stdexcept>
#include <cuchar>
#include <climits>
#include <cerrno>
static class EnsureLocale
{
public:
    EnsureLocale()
    {
        // 1. Try loading locale from environment variables
        if (std::setlocale(LC_ALL, "") == nullptr)
        {
            // 2. Fallback attempts if env locale fails or isn't generated
            if (std::setlocale(LC_ALL, "C.UTF-8") == nullptr &&
                std::setlocale(LC_ALL, "en_US.UTF-8") == nullptr &&
                std::setlocale(LC_ALL, ".UTF-8") == nullptr) // Windows MSVC fallback
            {
                // failed
            }
        }
        // Synchronize C++ locale facets with C locale
        std::locale::global(std::locale(""));
    }
} EnsureLocale;

#endif

wstringOut::wstringOut()
#if USE_ICONV
 : converter(), tester(), charset(midset)
#endif
{
#if USE_ICONV
    converter = iconv_open(charset.c_str(), midset);

    if(converter == (iconv_t)-1) std::perror("iconv_open");
    tester = iconv_open(charset.c_str(), midset);
    if(tester == (iconv_t)-1) std::perror("iconv_open");
#endif
}

#if USE_ICONV
wstringOut::wstringOut(const char *setname) : converter(), tester(), charset(midset)
{
    converter = iconv_open(charset.c_str(), midset);
    tester = iconv_open(charset.c_str(), midset);
    SetSet(setname);
}
void wstringOut::SetSet(const char *setname)
{
    iconv_close(converter);
    iconv_close(tester);
    charset = setname;
    converter = iconv_open(setname, midset);
    if(converter == (iconv_t)(-1))
    {
        std::perror("wstringOut::SetSet::iconv_open");
        std::exit(1);
    }
    tester = iconv_open(setname, midset);
}
#endif

wstringOut::~wstringOut()
{
#if USE_ICONV
    iconv_close(converter);
    iconv_close(tester);
#endif
}

const std::string wstringOut::puts(const std::u32string &s) const
{
#if USE_ICONV
    std::string result;
    if (s.empty()) return result;

    // Cast the const char32_t* to a char* for iconv. 
    // iconv requires a char**, so we make a local copy of the pointer.
    char* in_ptr = reinterpret_cast<char*>(const_cast<char32_t*>(s.data()));
    std::size_t in_left = s.size() * sizeof(char32_t);

    char out_chunk[1024];

    while (in_left > 0)
    {
        char* out_ptr = out_chunk;
        std::size_t out_left = sizeof(out_chunk);

        std::size_t res = iconv(converter, &in_ptr, &in_left, &out_ptr, &out_left);

        std::size_t bytes_produced = sizeof(out_chunk) - out_left;
        result.append(out_chunk, bytes_produced);

        if (res == static_cast<size_t>(-1))
        {
            if (errno == E2BIG)
            {
                // Output buffer full, cycle again
                continue;
            }
            else
            {
                // EILSEQ: The target encoding cannot represent this character.
                // EINVAL: Should not happen on UCS-4 fixed-width chars unless mathematically misaligned.
                // We skip one full char32_t to recover.
                if (in_left >= sizeof(char32_t))
                {
                    in_ptr += sizeof(char32_t);
                    in_left -= sizeof(char32_t);
                }
                else
                {
                    break;
                }
            }
        }
    }
    return result;
#else
    std::string result;
    if (s.empty()) return result;

    char buf[MB_LEN_MAX];
    for (char32_t c32 : s)
    {
        std::size_t res = std::c32rtomb(buf, c32, &state);
        if (res == static_cast<std::size_t>(-1))
        {
            // Character cannot be represented in the current system encoding.
            // Reset state to recover gracefully.
            state = std::mbstate_t{};
        }
        else
        {
            result.append(buf, res);
        }
    }
    return result;
#endif
}

bool wstringOut::isok(char32_t p) const
{
#if USE_ICONV
    char* in_ptr = reinterpret_cast<char*>(&p);
    std::size_t in_left = sizeof(char32_t);

    // Provide enough space for one maximum-length multibyte character
    char out_chunk[32];
    char* out_ptr = out_chunk;
    std::size_t out_left = sizeof(out_chunk);

    // Test conversion
    std::size_t res = iconv(tester, &in_ptr, &in_left, &out_ptr, &out_left);

    // Reset the tester's shift state. 
    // If testing a character shifted the state (e.g., ISO-2022 encodings),
    // this resets it so subsequent calls to isok() aren't broken.
    iconv(tester, nullptr, nullptr, nullptr, nullptr);

    // If iconv returns (size_t)-1, the character could not be converted.
    return (res != static_cast<std::size_t>(-1));
#else
    char buf[MB_LEN_MAX];
    // Copy the shift state so testing doesn't alter conversion state for puts()
    std::mbstate_t test_state = state;

    std::size_t res = std::c32rtomb(buf, p, &test_state);
    return (res != static_cast<std::size_t>(-1));
#endif
}

wstringIn::wstringIn()
#if USE_ICONV
   : converter(iconv_open(midset, midset)), charset(midset)
#endif
{
#if !USE_ICONV
    std::mbtowc(NULL, NULL, 0);
#endif
}

#if USE_ICONV
wstringIn::wstringIn(const char *setname)
   : converter(iconv_open(midset, midset)), charset(midset)
{
    SetSet(setname);
}
void wstringIn::SetSet(const char *setname)
{
    iconv_close(converter);
    charset = setname;
    converter = iconv_open(midset, setname);
    if(converter == (iconv_t)(-1))
    {
        std::perror("wstringIn::SetSet::iconv_open");
        std::exit(1);
    }
}
#endif

wstringIn::~wstringIn()
{
#if USE_ICONV
    iconv_close(converter);
#endif
}

const std::u32string wstringIn::puts(const std::string &s) const
{
#if USE_ICONV
    // Prepend any leftover bytes from a previous incomplete sequence
    std::string input = std::move(leftover) + s;
    leftover.clear();

    std::u32string result;
    if (input.empty()) return result;

    // iconv modifies the pointer, but not the data itself. 
    // We use .data() which returns a mutable char* since C++17.
    char* in_ptr   = input.data();
    size_t in_left = input.size();

    // Buffer for output. 256 char32_t is an arbitrary chunk size.
    char32_t out_chunk[256];
    while (in_left > 0)
    {
        char* out_ptr = reinterpret_cast<char*>(out_chunk);
        std::size_t out_left = sizeof(out_chunk);

        std::size_t res = iconv(converter, &in_ptr, &in_left, &out_ptr, &out_left);

        // Calculate how many characters were actually written to out_chunk
        std::size_t chars_produced = (sizeof(out_chunk) - out_left) / sizeof(char32_t);
        result.append(out_chunk, chars_produced);

        if (res == static_cast<size_t>(-1))
        {
            if (errno == E2BIG)
            {
                // The output buffer is full. Loop again to keep converting.
                continue;
            }
            else if (errno == EINVAL)
            {
                // Incomplete multibyte sequence at the end of the input.
                // Save it for the next call to puts().
                leftover.assign(in_ptr, in_left);
                break;
            }
            else
            {
                // EILSEQ (invalid sequence) or other errors.
                // Skip the bad byte to recover and prevent an infinite loop.
                if (in_left > 0)
                {
                    in_ptr++;
                    in_left--;
                }
            }
        }
    }
    return result;
#else
    const std::string& input = s;

    std::u32string result;
    if (input.empty()) return result;

    const char* in_ptr = input.data();
    std::size_t in_left = input.size();

    while (in_left > 0)
    {
        char32_t c32;
        // Convert multibyte byte sequence to char32_t using thread-safe state
        std::size_t res = std::mbrtoc32(&c32, in_ptr, in_left, &state);
        if (res == 0)
        {
            // Null byte processed
            result.push_back(U'\0');
            in_ptr += 1;
            in_left -= 1;
        }
        else if (res <= in_left)
        {
            // Successfully converted 1..in_left bytes into a char32_t
            result.push_back(c32);
            in_ptr += res;
            in_left -= res;
        }
        else if (res == static_cast<std::size_t>(-3))
        {
            // Consumed 0 input bytes, but produced a character (stateful shifts)
            result.push_back(c32);
        }
        else if (res == static_cast<std::size_t>(-2))
        {
            // Incomplete multibyte sequence at the end of input chunk.
            // 'state' has saved the partial byte information. 
            // We stop processing 's' and wait for the remaining bytes in the next puts() call.
            break;
        }
        else
        {
            // -1 (EILSEQ): Invalid multibyte sequence.
            unsigned char byte = *in_ptr;
            std::fprintf(stderr, "Ignoring %02X (%c)\n", byte, byte);
            // Skip 1 byte to recover and reset internal state.
            in_ptr += 1;
            in_left -= 1;
            state = std::mbstate_t{};
        }
    }

    return result;
#endif
}

const std::u32string AscToWstr(const std::string &s)
{
    std::u32string result;
    result.reserve(s.size());
    for(unsigned a=0; a<s.size(); ++a)
        result += AscToWchar(s[a]);
    return result;
}

const std::string WstrToAsc(const std::u32string &s)
{
    std::string result;
    result.reserve(s.size());
    for(unsigned a=0; a<s.size(); ++a)
        result += WcharToAsc(s[a]);
    return result;
}

char WcharToAsc(char32_t c)
{
    // jis ascii
    if(c >= 0xFF10 && c <= 0xFF19) return c - 0xFF10 + '0';
    if(c >= 0xFF21 && c <= 0xFF26) return c - 0xFF21 + 'A';
    if(c >= 0xFF41 && c <= 0xFF46) return c - 0xFF41 + 'a';
    // default
    return static_cast<char> (c);
}

long atoi(const char32_t *p, int base)
{
    long long ret=0;
    bool      sign=false;
    while(*p == '-') { sign=!sign; ++p; }
    for(; *p; ++p)
    {
        char c = WcharToAsc(*p);

        int p = Whex(c);
        if(p == -1)break;
        ret = ret*base + p;
    }
    if(sign) ret=-ret;
    return ret;
}

int Whex(char32_t p)
{
    char c = WcharToAsc(p);
    if(c >= '0' && c <= '9') return (c-'0');
    if(c >= 'A' && c <= 'Z') return (c+10-'A');
    if(c >= 'a' && c <= 'z') return (c+10-'a');
    return -1;
}
