#include <iostream>
#include <cstdlib>
#include <filesystem>
#include <mutex>
#include <Utilities/Utilities.hpp>
#include <Log/Log.hpp>
#include <UniqueID/UniqueID.hpp>

extern "C"
{
    #include "libtrashcan/trashcan.h"
}

static std::string dumpDir;

static thread_local std::mt19937 rng{std::random_device{}()};

EnvVars envVars = {false};

void formatStream(std::ostream &os, const char *fmt)
{
    os << fmt;
}

#ifdef _WIN32
#include <windows.h>

bool isOutputtingToConsole()
{
    DWORD temp;
    return GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &temp);
}

std::string inputPassword(std::string prompt)
{
    static const char BACKSPACE = 8;
    static const char RETURN = 13;

    std::string password;
    unsigned char ch = 0;

    std::cout << prompt;
    std::cout.flush();

    DWORD con_mode;
    DWORD dwRead;

    HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);

    GetConsoleMode(hIn, &con_mode);
    SetConsoleMode(hIn, con_mode & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT));

    while (ReadConsoleA(hIn, &ch, 1, &dwRead, NULL) && ch != RETURN)
    {
        if (ch == BACKSPACE)
        {
            if (password.length() != 0)
            {
                std::cout << "\b \b";
                std::cout.flush();
                password.resize(password.length() - 1);
            }
        }
        else
        {
            password += ch;
            std::cout << '*';
            std::cout.flush();
        }
    }

    std::cout << std::endl;
    return password;
}
#else
#include <termios.h>
#include <unistd.h>
#include <stdio.h>

bool isOutputtingToConsole()
{
    return isatty(1);
}

int getch()
{
    int ch;
    struct termios t_old, t_new;

    tcgetattr(STDIN_FILENO, &t_old);
    t_new = t_old;
    t_new.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &t_new);

    ch = getchar();

    tcsetattr(STDIN_FILENO, TCSANOW, &t_old);
    return ch;
}

std::string inputPassword(std::string prompt)
{
    static const char BACKSPACE = 127;
    static const char RETURN = 10;

    std::string password;
    unsigned char ch = 0;

    std::cout << prompt;
    std::cout.flush();

    while ((ch = getch()) != RETURN)
    {
        if (ch == BACKSPACE)
        {
            if (password.length() != 0)
            {
                std::cout << "\b \b";
                password.resize(password.length() - 1);
            }
        }
        else
        {
            password += ch;
            std::cout << '*';
        }
    }

    std::cout << std::endl;
    return password;
}
#endif

[[noreturn]] void crash(std::string message)
{
    static std::mutex crashMutex;
    std::unique_lock<std::mutex> crashLock(crashMutex);

    Log::error(formatString("[CRASH] $+", message));
    Log::flush();

#ifdef DEBUG_BUILD
    // In debug mode, we dereference a NULL pointer instead of just aborting. This is because we
    // have ASan enabled, and it will give us a helpful stack trace for crashes.
    *((volatile int*)0) = 0;
#endif

    std::abort();
}

std::ostream& operator<<(std::ostream &os, glm::vec3 vec)
{
    os << "(" << vec[0] << ", " << vec[1] << ", " << vec[2] << ")";
    return os;
}

float interpolate2D(float p00, float p10, float p01, float p11, float tx, float ty)
{
    return tx * ty * p11 + (1.0f - tx) * ty * p01 + tx * (1.0f - ty) * p10 + (1.0f - tx) * (1.0f - ty) * p00;
}

float interpolate2DSmooth(float p00, float p10, float p01, float p11, float tx, float ty)
{
    return interpolate2D(p00, p10, p01, p11, smoothstep(tx), smoothstep(ty));
}

float smoothstep(float t)
{
    float t3 = t * t * t;
    return 6.0f * t3 * t * t - 15.0f * t3 * t + 10.0f * t3;
}

std::string formatDataSize(size_t size)
{
    size *= 100;

    const char* unitNames[] = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"};
    size_t index = 0;

    while (size > 102400)
    {
        size /= 1024;
        index++;
    }

    double d = (double) size / 100.0;
    return formatString("$+ $+", d, unitNames[index]);
}

std::optional<std::string> moveToTrash(std::string path)
{
    int ret = trashcan_soft_delete(path.c_str());
    if (ret == 0)
    {
        return std::nullopt;
    }

    return trashcan_status_msg(ret);
}

std::string getDumpPath(std::string filename)
{
    if (dumpDir.size() == 0)
    {
        UniqueID id = UniqueID::generate();
        dumpDir = formatString("dumps/$+", id);

        Log::debug(formatString("Using dump directory: $+", dumpDir));
        std::filesystem::create_directories(dumpDir);
    }

    return formatString("$+/$+", dumpDir, filename);
}

void compressBytes(const std::vector<uint8_t> &bytes, std::vector<uint8_t> &output, int level)
{
    uLongf bufferSize = compressBound(bytes.size());
    output.resize(bufferSize);

    if (compress2(output.data(), &bufferSize, bytes.data(), bytes.size(), level) != Z_OK)
    {
        crash("compressBytes: compress2 failed!");
    }

    output.resize(bufferSize);
}

bool uncompressBytes(const std::vector<uint8_t> &bytes, std::vector<uint8_t> &output)
{
    uLongf uncompressedSize = bytes.size() * 4;
    int status;

    do
    {
        output.resize(uncompressedSize);
        status = uncompress(output.data(), &uncompressedSize, bytes.data(), bytes.size());

        if (status == Z_BUF_ERROR)
        {
            uncompressedSize *= 2;
        }
        else if (status != Z_OK)
        {
            return false;
        }
    } while (status == Z_BUF_ERROR);

    output.resize(uncompressedSize);
    return true;
}

uint64_t utf8read(const std::string& str, size_t& offset)
{
    uint64_t codepoint = 0;
    unsigned char lead = str[offset];

    if (lead < 0x80)
    {
        codepoint = lead;
        offset += 1;
    }
    else if ((lead >> 5) == 0x6)
    {
        if (offset + 1 >= str.size() || (str[offset + 1] & 0xC0) != 0x80)
        {
            offset++;
            return UTF8_INVALID_CHAR;
        }
        codepoint = ((lead & 0x1F) << 6) | (str[offset + 1] & 0x3F);
        offset += 2;
    }
    else if ((lead >> 4) == 0xE)
    {
        if (offset + 2 >= str.size() || 
            (str[offset + 1] & 0xC0) != 0x80 || 
            (str[offset + 2] & 0xC0) != 0x80)
        {
            offset++;
            return UTF8_INVALID_CHAR;
        }
        codepoint = ((lead & 0x0F) << 12) | 
            ((str[offset + 1] & 0x3F) << 6) | 
            (str[offset + 2] & 0x3F);
        offset += 3;
    }
    else if ((lead >> 3) == 0x1E)
    {
        if (offset + 3 >= str.size() || 
            (str[offset + 1] & 0xC0) != 0x80 || 
            (str[offset + 2] & 0xC0) != 0x80 || 
            (str[offset + 3] & 0xC0) != 0x80)
        {
            offset++;
            return UTF8_INVALID_CHAR;
        }
        codepoint = ((lead & 0x07) << 18) | 
            ((str[offset + 1] & 0x3F) << 12) | 
            ((str[offset + 2] & 0x3F) << 6) | 
            (str[offset + 3] & 0x3F);
        offset += 4;
    }
    else
    {
        offset++;
        return UTF8_INVALID_CHAR;
    }

    return codepoint;
}

static bool isStartOfUTF8Char(char c)
{
    return (c & 0xC0) != 0x80;
}

void utf8move(const std::string &str, size_t &offset, int delta)
{
    if (delta > 0)
    {
        for (int i = 0; i < delta; ++i)
        {
            if (offset >= str.size()) return;

            do
            {
                ++offset;
            }
            while (offset < str.size() && !isStartOfUTF8Char(str[offset]));
        }
    }
    else
    {
        for (int i = 0; i < -delta; ++i)
        {
            if (offset == 0) return;

            do
            {
                --offset;
            }
            while (offset > 0 && !isStartOfUTF8Char(str[offset]));
        }
    }
}

std::string trim(const std::string &str)
{
    size_t first = str.find_first_not_of(" \t\n\r");
    if (first == std::string::npos)
    {
        return "";  // String is all whitespace
    }

    size_t last = str.find_last_not_of(" \t\n\r");
    if (last == std::string::npos)
    {
        last = str.size() - 1;
    }

    return str.substr(first, last - first + 1);
}

std::string getWorldNameError(std::string worldName)
{
    if (worldName.size() == 0)
    {
        return "Please enter a world name.";
    }

    for (const char &c : worldName)
    {
        if (c == '/' || c == '\\')
        {
            return "Slashes are not allowed.";
        }
    }

    if (std::filesystem::exists(formatString("worlds/$+.voxelverse", worldName)))
    {
        return "Name already in use.";
    }

    return "";
}

float unormFromBits(uint32_t seed)
{
    union {
        uint32_t asInt;
        float asFloat;
    } u;

    // Set the integer part to the input
    u.asInt = seed;

    // We need to ensure the value is between 0 and 1
    // To do this, we need to manipulate the exponent bits in IEEE-754 format
    // The exponent for a value between 0 and 1 has to be between 0 and 127 (biased by 127)
    
    // Strip out the original exponent and set it to a small number
    // Keep the mantissa random by using the lower 23 bits of the seed
    u.asInt &= 0x007FFFFF;   // Keep the mantissa bits intact, zero the exponent and sign
    u.asInt |= 0x3F800000;   // Set the exponent bits to make it a value between 1 and 2
    
    // Now the float is between 1.0 and 2.0, so we subtract 1 to make it between 0.0 and 1.0
    return u.asFloat - 1.0f;
}

std::string formatTicks(uint64_t rem)
{
    rem /= 60;
    uint64_t seconds = rem % 60;
    rem /= 60;
    uint64_t minutes = rem % 60;
    rem /= 60;
    uint64_t hours = rem;

    return formatString(
        "$+h:$+$+$+m:$+$+$+s",
        hours,
        std::setw(2), std::setfill('0'), minutes,
        std::setw(2), std::setfill('0'), seconds
    );
}

float expWithControlPoints(float x, glm::vec2 controlA, glm::vec2 controlB)
{
    float b = std::log(controlB.y / controlA.y) / (controlB.x - controlA.x);
    float a = controlA.y / std::exp(b * controlA.x);

    return a * std::exp(b * x);
}

float randomUnit()
{
    static thread_local std::uniform_real_distribution<float> dist01(0.0f, 1.0f);
    return dist01(rng);
}

float randomSignedUnit()
{
    static thread_local std::uniform_real_distribution<float> dists1(-1.0f, 1.0f);
    return dists1(rng);
}