#include "common/io.h"
#include "fastfetch.h"
#include "common/strutil.h"
#include "common/time.h"

#include <fcntl.h>
#include <termios.h>
#include <dirent.h>
#include <errno.h>
#ifndef __APPLE__
    #include <poll.h>
#else
    #include <sys/select.h>
#endif

#if FF_HAVE_WORDEXP
    #include <wordexp.h>
#else
    #include <glob.h>
#endif

static void createSubfolders(const char* fileName) {
    FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();

    const char* token = nullptr;
    while ((token = strchr(fileName, '/')) != nullptr) {
        ffStrbufAppendNS(&path, (uint32_t) (token - fileName + 1), fileName);
        mkdir(path.chars, S_IRWXU | S_IRGRP | S_IROTH);
        fileName = token + 1;
    }
}

bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
    int openFlagsModes = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
    mode_t openFlagsRights = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;

    FF_AUTO_CLOSE_FD int fd = open(fileName, openFlagsModes, openFlagsRights);
    if (fd == -1) {
        if (errno == ENOENT) {
            createSubfolders(fileName);
            fd = open(fileName, openFlagsModes, openFlagsRights);
            if (fd == -1) {
                return false;
            }
        } else {
            return false;
        }
    }

    return write(fd, data, dataSize) > 0;
}

static inline void readWithLength(int fd, FFstrbuf* buffer, uint32_t length) {
    ffStrbufEnsureFixedLengthFree(buffer, length);
    ssize_t bytesRead = 0;
    while (
        length > 0 && (bytesRead = read(fd, buffer->chars + buffer->length, length)) > 0) {
        buffer->length += (uint32_t) bytesRead;
        length -= (uint32_t) bytesRead;
    }
}

static inline void readUntilEOF(int fd, FFstrbuf* buffer) {
    ffStrbufEnsureFree(buffer, 31);
    uint32_t available = ffStrbufGetFree(buffer);
    ssize_t bytesRead = 0;
    while (
        (bytesRead = read(fd, buffer->chars + buffer->length, available)) > 0) {
        buffer->length += (uint32_t) bytesRead;
        if ((uint32_t) bytesRead == available) {
            ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte.
        }
        available = ffStrbufGetFree(buffer);
    }
}

bool ffAppendFDBuffer(int fd, FFstrbuf* buffer) {
    struct stat fileInfo;
    if (fstat(fd, &fileInfo) != 0) {
        return false;
    }

    if (fileInfo.st_size > 0) {
        readWithLength(fd, buffer, (uint32_t) fileInfo.st_size);
    } else {
        readUntilEOF(fd, buffer);
    }

    buffer->chars[buffer->length] = '\0';

    return buffer->length > 0;
}

bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
    bool result = false;

#if FF_HAVE_WORDEXP

    wordexp_t exp;
    if (wordexp(in, &exp, 0) != 0) { // WARN: 0 = no safety flags; command substitution allowed
        return false;
    }

    if (exp.we_wordc >= 1) {
        result = true;
        ffStrbufSetS(out, exp.we_wordv[exp.we_wordc > 1 ? ffTimeGetNow() % exp.we_wordc : 0]);
    }

    wordfree(&exp);

#else

    glob_t gb;
    if (glob(in, GLOB_NOSORT
    #ifdef GLOB_TILDE
                | GLOB_TILDE
    #endif
    #ifdef GLOB_BRACE
                | GLOB_BRACE
    #endif
            ,
            nullptr,
            &gb) != 0)
        return false;

    if (gb.gl_pathc >= 1) {
        result = true;
        ffStrbufSetS(out, gb.gl_pathv[gb.gl_pathc > 1 ? ffTimeGetNow() % (unsigned) gb.gl_pathc : 0]);
    }

    globfree(&gb);

#endif

    return result;
}

static int ftty = -1;
static struct termios oldTerm;
void restoreTerm(void) {
    tcsetattr(ftty, TCSAFLUSH, &oldTerm);
}

const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
    if (ftty < 0) {
        ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
        if (ftty < 0) {
            return "open(\"/dev/tty\", O_RDWR | O_NOCTTY | O_CLOEXEC) failed";
        }

        if (tcgetattr(ftty, &oldTerm) == -1) {
            return "tcgetattr(STDIN_FILENO, &oldTerm) failed";
        }

        struct termios newTerm = oldTerm;
        newTerm.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
        if (tcsetattr(ftty, TCSAFLUSH, &newTerm) == -1) {
            return "tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm)";
        }
        atexit(restoreTerm);
    }

    ffWriteFDData(ftty, strlen(request), request);

// Give the terminal some time to respond
#ifndef __APPLE__
    if (poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS) <= 0) {
        return "poll(/dev/tty) timeout or failed";
    }
#else
    {
        // On macOS, poll(/dev/tty) always returns immediately
        // See also https://nathancraddock.com/blog/macos-dev-tty-polling/
        fd_set rd;
        FD_ZERO(&rd);
        FD_SET(ftty, &rd);
        if (select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }) <= 0) {
            return "select(/dev/tty) timeout or failed";
        }
    }
#endif

    char buffer[1024];
    size_t bytesRead = 0;

    va_list args;
    va_start(args, format);

    while (true) {
        ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1);

        if (nRead <= 0) {
            va_end(args);
            return "read(STDIN_FILENO, buffer, sizeof(buffer) - 1) failed";
        }

        bytesRead += (size_t) nRead;
        buffer[bytesRead] = '\0';

        va_list cargs;
        va_copy(cargs, args);
        int ret = vsscanf(buffer, format, cargs);
        va_end(cargs);

        if (ret <= 0) {
            va_end(args);
            return "vsscanf(buffer, format, args) failed";
        }
        if (ret >= nParams) {
            break;
        }
    }

    va_end(args);

    return nullptr;
}

bool ffSuppressIO(bool suppress) {
#ifndef NDEBUG
    if (instance.config.display.debugMode) {
        return false;
    }
#endif

    static bool init = false;
    static int origOut = -1;
    static int origErr = -1;
    static int nullFile = -1;

    if (!init) {
        if (!suppress) {
            return true;
        }

        origOut = dup(STDOUT_FILENO);
        origErr = dup(STDERR_FILENO);
        nullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
        init = true;
    }

    if (nullFile == -1) {
        return false;
    }

    fflush(stdout);
    fflush(stderr);

    dup2(suppress ? nullFile : origOut, STDOUT_FILENO);
    dup2(suppress ? nullFile : origErr, STDERR_FILENO);
    return true;
}

void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) {
    int dfd = open(folder->chars, O_RDONLY | O_CLOEXEC | O_DIRECTORY); // Ownership of dfd will be transformed to dir
    if (dfd < 0) {
        return;
    }

    FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd);
    if (dir == nullptr) {
        close(dfd);
        return; // Should not happen
    }

    uint32_t folderLength = folder->length;

    if (pretty && folderName != nullptr) {
        for (uint8_t i = 0; i < indentation - 1; i++) {
            fputs("  | ", stdout);
        }
        printf("%s/\n", folderName);
    }

    struct dirent* entry;

    while ((entry = readdir(dir)) != nullptr) {
        if (entry->d_name[0] == '.') { // skip hidden files
            continue;
        }

        bool isDir = false;
#if !defined(__sun) && !defined(__HAIKU__)
        if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK) {
            isDir = entry->d_type == DT_DIR;
        } else
#endif
        {
            struct stat stbuf;
            if (fstatat(dfd, entry->d_name, &stbuf, 0) < 0) {
                isDir = false;
            } else {
                isDir = S_ISDIR(stbuf.st_mode);
            }
        }
        if (isDir) {
            ffStrbufAppendS(folder, entry->d_name);
            ffStrbufAppendC(folder, '/');
            listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry->d_name, pretty);
            ffStrbufSubstrBefore(folder, folderLength);
            continue;
        }

        if (pretty) {
            for (uint8_t i = 0; i < indentation; i++) {
                fputs("  | ", stdout);
            }
        } else {
            fputs(folder->chars + baseLength, stdout);
        }

        puts(entry->d_name);
    }
}

void ffListFilesRecursively(const char* path, bool pretty) {
    FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path);
    ffStrbufEnsureEndsWithC(&folder, '/');
    listFilesRecursively(folder.length, &folder, 0, nullptr, pretty);
}

FFNativeFD ffGetNullFD(void) {
    static FFNativeFD hNullFile = -1;
    if (hNullFile != -1) {
        return hNullFile;
    }
    hNullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
    return hNullFile;
}

bool ffRemoveFile(const char* fileName) {
    return unlink(fileName) == 0;
}
