#include <cstdio>

#include <mutex>
#include <memory>
#include <algorithm>

#include <QDebug>

#include <QDir>
#include <QFileInfo>
#include <QTemporaryFile>
#include <QCoreApplication>

#include <QLibrary>
#include <QElapsedTimer>

#ifdef Q_OS_UNIX
#include <sys/mman.h>
#endif

// static QByteArray buf;
extern QElapsedTimer elapsed_timer;
QElapsedTimer elapsed_timer;
static std::mutex blitmx;

class CharPointer {
public:
    CharPointer(char* buf, int size) : b(buf), s(size) {}
    CharPointer& operator=(const QByteArray &ba) {
        if (s > 0) {
            strncpy(b, ba.data(), s-1);
            b[s] = 0;
        } else {
            // if we haven't been told the length of b, just assume enough
            // because we didn't get it from emulator code
            strcpy(b, ba.data());
            b[ba.size()] = 0;
        }
        return *this;
    }
private:
    char* b;
    int s;
};

extern "C" {
#include <86box/86box.h>
#include <86box/device.h>
#include <86box/gameport.h>
#include <86box/plat_dynld.h>
#include <86box/config.h>
#include <86box/ui.h>

#include "../cpu/cpu.h"
#include <86box/plat.h>

volatile int cpu_thread_run = 1;
int mouse_capture = 0;
int fixed_size_x = 640;
int fixed_size_y = 480;
int rctrl_is_lalt = 0;
int	update_icons = 0;
int	kbd_req_capture = 0;
int hide_status_bar = 0;
uint32_t lang_id = 0x0409, lang_sys = 0x0409; // Multilangual UI variables, for now all set to LCID of en-US

plat_joystick_t	plat_joystick_state[MAX_PLAT_JOYSTICKS];
joystick_t	joystick_state[MAX_JOYSTICKS];

int stricmp(const char* s1, const char* s2)
{
    return QByteArray(s1).compare(s2, Qt::CaseInsensitive);
}

int strnicmp(const char *s1, const char *s2, size_t n)
{
    QByteArray b1(s1, std::min(strlen(s1), n));
    QByteArray b2(s2, std::min(strlen(s2), n));
    return b1.compare(b2, Qt::CaseInsensitive);
}

void
do_stop(void)
{
    QCoreApplication::quit();
}

void plat_get_exe_name(char *s, int size)
{
    CharPointer(s, size) = QCoreApplication::applicationFilePath().toUtf8();
}

uint32_t
plat_get_ticks(void)
{
    return elapsed_timer.elapsed();
}

uint64_t
plat_timer_read(void)
{
    return elapsed_timer.elapsed();
}

FILE *
plat_fopen(const char *path, const char *mode)
{
    /*
    QString filepath(path);
    if (filepath.isEmpty()) {
        return nullptr;
    }

    qWarning() << "plat_fopen" << filepath;
    bool ok = false;
    QFile file(filepath);
    auto mode_len = strlen(mode);
    for (size_t i = 0; i < mode_len; ++i) {
        switch (mode[i]) {
        case 'r':
            ok = file.open(QIODevice::ReadOnly);
            break;
        case 'w':
            ok = file.open(QIODevice::ReadWrite);
            break;
        case 'b':
        case 't':
            break;
        default:
            qWarning() << "Unhandled open mode" << mode[i];
        }
    }

    if (ok) {
        qDebug() << "filehandle" << file.handle();
        QFile returned;
        qDebug() << "\t" << returned.open(file.handle(), file.openMode(), QFileDevice::FileHandleFlag::DontCloseHandle);
        return fdopen(returned.handle(), mode);
    } else {
        return nullptr;
    }
    */
#ifdef Q_OS_WINDOWS
    wchar_t *pathw, *modew;
    int len;
    FILE *fp;

    if (acp_utf8)
        return fopen(path, mode);
    else {
        len = mbstoc16s(NULL, path, 0) + 1;
        pathw = malloc(sizeof(wchar_t) * len);
        mbstoc16s(pathw, path, len);

        len = mbstoc16s(NULL, mode, 0) + 1;
        modew = malloc(sizeof(wchar_t) * len);
        mbstoc16s(modew, mode, len);

        fp = _wfopen(pathw, modew);

        free(pathw);
        free(modew);

        return fp;
    }
#endif
#ifdef Q_OS_UNIX
    return fopen(path, mode);
#endif
}

FILE *
plat_fopen64(const char *path, const char *mode)
{
    return fopen(path, mode);
}

int
plat_dir_create(char *path)
{
    return QDir().mkdir(path) ? 0 : -1;
}

int
plat_dir_check(char *path)
{
    QFileInfo fi(path);
    return fi.isDir() ? 1 : 0;
}

int
plat_getcwd(char *bufp, int max)
{
    CharPointer(bufp, max) = QDir::currentPath().toUtf8();
    return 0;
}

void
plat_get_dirname(char *dest, const char *path)
{
    QFileInfo fi(path);
    CharPointer(dest, -1) = fi.dir().path().toUtf8();
}

char *
plat_get_extension(char *s)
{
    auto len = strlen(s);
    auto idx = QByteArray::fromRawData(s, len).lastIndexOf('.');
    if (idx >= 0) {
        return s+idx+1;
    }
    return s+len;
}

char *
plat_get_filename(char *s)
{
    auto idx = QByteArray::fromRawData(s, strlen(s)).lastIndexOf(QDir::separator().toLatin1());
    if (idx >= 0) {
        return s+idx+1;
    }
    return s;
}

int
plat_path_abs(char *path)
{
    QFileInfo fi(path);
    return fi.isAbsolute() ? 1 : 0;
}

void
plat_path_slash(char *path)
{
    auto len = strlen(path);
    auto separator = QDir::separator().toLatin1();
    if (path[len-1] != separator) {
        path[len] = separator;
        path[len+1] = 0;
    }
}

void
plat_append_filename(char *dest, const char *s1, const char *s2)
{
    strcpy(dest, s1);
    plat_path_slash(dest);
    strcat(dest, s2);
}

void
plat_tempfile(char *bufp, char *prefix, char *suffix)
{
    QString name;

    if (prefix != nullptr) {
        name.append(QString("%1-").arg(prefix));
    }

    name.append("XXXXXX");

    if (suffix != nullptr) {
        name.append(suffix);
    }
    QTemporaryFile temp(name);
    QByteArray buf(bufp);
    buf = temp.fileName().toUtf8();
}

void plat_remove(char* path)
{
    QFile(path).remove();
}

void *
plat_mmap(size_t size, uint8_t executable)
{
#if defined Q_OS_WINDOWS
    return VirtualAlloc(NULL, size, MEM_COMMIT, executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE);
#elif defined Q_OS_UNIX
#if defined Q_OS_DARWIN && defined MAP_JIT
    void *ret = mmap(0, size, PROT_READ | PROT_WRITE | (executable ? PROT_EXEC : 0), MAP_ANON | MAP_PRIVATE | (executable ? MAP_JIT : 0), 0, 0);
#else
    void *ret = mmap(0, size, PROT_READ | PROT_WRITE | (executable ? PROT_EXEC : 0), MAP_ANON | MAP_PRIVATE, 0, 0);
#endif
    auto retval = *reinterpret_cast<int*>(ret);
    return (retval < 0) ? nullptr : ret;
#endif
}

void
plat_munmap(void *ptr, size_t size)
{
    munmap(ptr, size);
}

void
plat_pause(int p)
{
    static wchar_t oldtitle[512];
    wchar_t title[512];

    dopause = p;
    if (p) {
        wcsncpy(oldtitle, ui_window_title(NULL), sizeof_w(oldtitle) - 1);
        wcscpy(title, oldtitle);
        wcscat(title, L" - PAUSED -");
        ui_window_title(title);
    } else {
        ui_window_title(oldtitle);
    }
}

// because we can't include nvr.h because it's got fields named new
extern int	nvr_save(void);

void
plat_power_off(void)
{
    confirm_exit = 0;
    nvr_save();
    config_save();

    /* Deduct a sufficiently large number of cycles that no instructions will
       run before the main thread is terminated */
    cycles -= 99999999;

    cpu_thread_run = 0;
}

void set_language(uint32_t id) {
    lang_id = id;
}

/* Sets up the program language before initialization. */
uint32_t plat_language_code(char* langcode) {
    /* or maybe not */
    return 0;
}

/* Converts back the language code to LCID */
void plat_language_code_r(uint32_t lcid, char* outbuf, int len) {
    /* or maybe not */
    return;
}

void* dynld_module(const char *name, dllimp_t *table)
{
    QString libraryName = name;
    QFileInfo fi(libraryName);
    QStringList removeSuffixes = {"dll", "dylib", "so"};
    if (removeSuffixes.contains(fi.suffix())) {
        libraryName = fi.completeBaseName();
    }

    auto lib = std::unique_ptr<QLibrary>(new QLibrary(libraryName));
    if (lib->load()) {
        for (auto imp = table; imp->name != nullptr; imp++)
        {
            auto ptr = lib->resolve(imp->name);
            if (ptr == nullptr) {
                return nullptr;
            }
            auto imp_ptr = reinterpret_cast<void**>(imp->func);
            *imp_ptr = reinterpret_cast<void*>(ptr);
        }
    } else {
        return nullptr;
    }

    return lib.release();
}

void dynld_close(void *handle)
{
    delete reinterpret_cast<QLibrary*>(handle);
}

void joystick_init(void) {}
void joystick_close(void) {}
void joystick_process(void) {}
void startblit()
{
    blitmx.lock();
}

void endblit()
{
    blitmx.unlock();
}

}
