#pragma once

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <cstdarg>
#include <cstdio>

// Exact tungsten idStr storage layout (tungsten.exe.h type 12142). This is a
// deliberately small ABI facade; more authoritative text methods will be
// added as text/str.cpp is reconstructed.
class idStr {
public:
    idStr()
        : len(0)
        , data(baseBuffer)
        , allocedAndFlag(20) {
        baseBuffer[0] = '\0';
    }

    idStr(const char* text)
        : idStr() {
        Assign(text);
    }

    idStr(const idStr& other)
        : idStr() {
        Assign(other.c_str());
    }

    ~idStr() {
        if (data != baseBuffer && !IsStaticBuffer()) {
            std::free(data);
        }
    }

    idStr& operator=(const idStr& other) {
        if (this != &other) {
            Assign(other.c_str());
        }
        return *this;
    }

    idStr& operator=(const char* text) {
        Assign(text);
        return *this;
    }

    const char* c_str() const {
        return data;
    }

    operator const char*() const {
        return data;
    }

    int Length() const {
        return len;
    }

    void Clear() {
        len = 0;
        data[0] = '\0';
    }

    void TrimWhitespaceRecovered() {
        while (len > 0
            && std::isspace(static_cast<unsigned char>(data[len - 1])) != 0) {
            data[--len] = '\0';
        }
        int amount = 0;
        while (amount < len
            && std::isspace(static_cast<unsigned char>(data[amount])) != 0) {
            ++amount;
        }
        if (amount > 0) {
            std::memmove(data, data + amount,
                static_cast<std::size_t>(len - amount + 1));
            len -= amount;
        }
    }

    // Recovered from shared/idlib/text/str.cpp. Resource names are lowercase,
    // use forward slashes, and discard leading slashes except for UNC names.
    void MakeNameCanonical() {
        for (int index = 0; index < len; ++index) {
            if (data[index] >= 'A' && data[index] <= 'Z') {
                data[index] = static_cast<char>(data[index] + ('a' - 'A'));
            } else if (data[index] == '\\') {
                data[index] = '/';
            }
        }
        if (!(len >= 2 && data[0] == '/' && data[1] == '/')) {
            int leading = 0;
            while (leading < len && data[leading] == '/') {
                ++leading;
            }
            if (leading > 0) {
                std::memmove(data, data + leading,
                    static_cast<std::size_t>(len - leading + 1));
                len -= leading;
            }
        }
    }

    void Append(const char* text) {
        if (text == nullptr || *text == '\0') {
            return;
        }
        const int appendLength = static_cast<int>(std::strlen(text));
        if (!EnsureAlloced(len + appendLength + 1)) {
            return;
        }
        std::memcpy(data + len, text, static_cast<std::size_t>(appendLength + 1));
        len += appendLength;
    }

    void Append(const char value) {
        if (!EnsureAlloced(len + 2)) {
            return;
        }
        data[len++] = value;
        data[len] = '\0';
    }

    void Append(const idStr& text) {
        Append(text.c_str());
    }

    idStr& StripFileExtension() {
        for (int index = len - 1; index >= 0; --index) {
            const char value = data[index];
            if (value == '\\' || value == '/') {
                break;
            }
            if (value == '.') {
                data[index] = '\0';
                len = index;
                break;
            }
        }
        return *this;
    }

    idStr& SetFileExtension(const char* extension) {
        StripFileExtension();
        if (extension != nullptr && extension[0] != '.') {
            Append('.');
        }
        Append(extension);
        return *this;
    }

    void Format(const char* format, ...) {
        char buffer[4096];
        va_list arguments;
        va_start(arguments, format);
        const int count = _vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE,
            format != nullptr ? format : "", arguments);
        va_end(arguments);
        if (count >= 0) Assign(buffer);
        else Clear();
    }

    void ReplaceRecovered(const char* oldText, const char* newText) {
        if (oldText == nullptr || oldText[0] == '\0') {
            return;
        }
        const char* const replacement = newText == nullptr ? "" : newText;
        const int oldLength = static_cast<int>(std::strlen(oldText));
        idStr result;
        const char* cursor = data;
        for (;;) {
            const char* const match = std::strstr(cursor, oldText);
            if (match == nullptr) {
                result.Append(cursor);
                break;
            }
            const int prefixLength = static_cast<int>(match - cursor);
            for (int index = 0; index < prefixLength; ++index) {
                result.Append(cursor[index]);
            }
            result.Append(replacement);
            cursor = match + oldLength;
        }
        Assign(result.c_str());
    }

    static int Cmp(const char* left, const char* right) {
        const char* const safeLeft = left == nullptr ? "" : left;
        const char* const safeRight = right == nullptr ? "" : right;
        return std::strcmp(safeLeft, safeRight);
    }

    bool operator==(const idStr& other) const {
        return Cmp(c_str(), other.c_str()) == 0;
    }

    bool operator!=(const idStr& other) const {
        return !(*this == other);
    }

    bool operator<(const idStr& other) const {
        return Cmp(c_str(), other.c_str()) < 0;
    }

protected:
    int len;
    char* data;
    int allocedAndFlag;
    char baseBuffer[20];

    void UseStaticBufferRecovered(char* buffer, const int capacity) {
        if (data != baseBuffer && !IsStaticBuffer()) {
            std::free(data);
        }
        data = buffer;
        len = 0;
        allocedAndFlag = capacity | static_cast<int>(0x80000000u);
        if (data != nullptr && capacity > 0) {
            data[0] = '\0';
        }
    }

private:

    int GetAlloced() const {
        return allocedAndFlag & 0x7FFFFFFF;
    }

    bool IsStaticBuffer() const {
        return (static_cast<unsigned int>(allocedAndFlag) & 0x80000000u) != 0;
    }

    bool EnsureAlloced(const int amount) {
        if (amount <= GetAlloced()) {
            return true;
        }
        if (IsStaticBuffer()) {
            return false;
        }

        const int newAmount = (std::max)(amount, amount + amount / 2);
        char* const replacement = static_cast<char*>(
            std::malloc(static_cast<std::size_t>(newAmount))
        );
        if (replacement == nullptr) {
            return false;
        }
        std::memcpy(replacement, data, static_cast<std::size_t>(len + 1));
        if (data != baseBuffer) {
            std::free(data);
        }
        data = replacement;
        allocedAndFlag = newAmount;
        return true;
    }

    void Assign(const char* text) {
        const char* const source = text == nullptr ? "" : text;
        const int sourceLength = static_cast<int>(std::strlen(source));
        if (!EnsureAlloced(sourceLength + 1)) {
            return;
        }
        std::memmove(data, source, static_cast<std::size_t>(sourceLength + 1));
        len = sourceLength;
    }
};

static_assert(sizeof(idStr) == 32, "Recovered idStr ABI changed");
