#ifdef _WIN32

#include <windows.h>
#include <stdio.h>
#include <io.h>
#include <stdarg.h>
#include "print.h"

#ifndef FOREGROUND_MASK
#define FOREGROUND_MASK (FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY)
#endif
#ifndef BACKGROUND_MASK
#define BACKGROUND_MASK (BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_INTENSITY)
#endif

#define FG_RGB (FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN)
#define BG_RGB (BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_GREEN)

// BUFSIZ is guarenteed to be "at least 256 bytes" which might
// not be enough for us. Use an arbitrary but reasonably big
// buffer. win32 colored output will be truncated to this length.
#define BUF_SIZE (16 * 1024)

// max consecutive ansi sequence values beyond which we're aborting
// e.g. this is 3 values: \e[0;1;33m
#define MAX_VALUES 8

static int g_use_ansi = 0;
void windows_use_ansi(int use_ansi) {
    g_use_ansi = use_ansi;
}

int fprintf_w32(FILE *fp, const char *format, ...) {
    va_list args;
    char buf[BUF_SIZE] = { 0 }, *ptr = buf;
    static WORD attr_reset;
    static BOOL attr_initialized = FALSE;
    HANDLE stdo = INVALID_HANDLE_VALUE;
    WORD attr;
    DWORD written, csize;
    CONSOLE_CURSOR_INFO cci;
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    COORD coord;

    // if we don't output to screen (tty) e.g. for pager/pipe or
    // if for other reason we can't access the screen info, of if
    // the user just prefers ansi, do plain passthrough.
    BOOL passthrough =
        g_use_ansi ||
        !isatty(fileno(fp)) ||
        INVALID_HANDLE_VALUE == (stdo = (HANDLE)_get_osfhandle(fileno(fp))) ||
        !GetConsoleScreenBufferInfo(stdo, &csbi);

    if (passthrough) {
        int rv;
        va_start(args, format);
        rv = vfprintf(fp, format, args);
        va_end(args);
        return rv;
    }

    va_start(args, format);
    // truncates to (null terminated) BUF_SIZE if too long.
    // if too long - vsnprintf will fill count chars without
    // terminating null. buf is zeroed, so make sure we don't fill it.
    vsnprintf(buf, BUF_SIZE - 1, format, args);
    va_end(args);

    attr = csbi.wAttributes;
    if (!attr_initialized) {
        // reset is defined to have all (non color) attributes off
        attr_reset = attr & (FG_RGB | BG_RGB);
        attr_initialized = TRUE;
    }

    while (*ptr) {
        if (*ptr == '\033') {
            unsigned char c;
            int i, n = 0, m = '\0', v[MAX_VALUES], w, h;
            for (i = 0; i < MAX_VALUES; i++)
                v[i] = -1;
            ptr++;
        retry:
            if ((c = *ptr++) == 0)
                break;
            if (isdigit(c)) {
                if (v[n] == -1)
                    v[n] = c - '0';
                else
                    v[n] = v[n] * 10 + c - '0';
                goto retry;
            }
            if (c == '[') {
                goto retry;
            }
            if (c == ';') {
                if (++n == MAX_VALUES)
                    break;
                goto retry;
            }
            if (c == '>' || c == '?') {
                m = c;
                goto retry;
            }

            switch (c) {
                // n is the last occupied index, so we have n+1 values
                case 'h':
                    if (m == '?') {
                        for (i = 0; i <= n; i++) {
                            switch (v[i]) {
                                case 3:
                                    GetConsoleScreenBufferInfo(stdo, &csbi);
                                    w = csbi.dwSize.X;
                                    h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
                                    csize = w * (h + 1);
                                    coord.X = 0;
                                    coord.Y = csbi.srWindow.Top;
                                    FillConsoleOutputCharacter(stdo, ' ', csize, coord, &written);
                                    FillConsoleOutputAttribute(stdo, csbi.wAttributes, csize, coord, &written);
                                    SetConsoleCursorPosition(stdo, csbi.dwCursorPosition);
                                    csbi.dwSize.X = 132;
                                    SetConsoleScreenBufferSize(stdo, csbi.dwSize);
                                    csbi.srWindow.Right = csbi.srWindow.Left + 131;
                                    SetConsoleWindowInfo(stdo, TRUE, &csbi.srWindow);
                                    break;
                                case 5:
                                    attr =
                                        ((attr & FOREGROUND_MASK) << 4) |
                                        ((attr & BACKGROUND_MASK) >> 4);
                                    SetConsoleTextAttribute(stdo, attr);
                                    break;
                                case 9:
                                    break;
                                case 25:
                                    GetConsoleCursorInfo(stdo, &cci);
                                    cci.bVisible = TRUE;
                                    SetConsoleCursorInfo(stdo, &cci);
                                    break;
                                case 47:
                                    coord.X = 0;
                                    coord.Y = 0;
                                    SetConsoleCursorPosition(stdo, coord);
                                    break;
                                default:
                                    break;
                            }
                        }
                    } else if (m == '>' && v[0] == 5) {
                        GetConsoleCursorInfo(stdo, &cci);
                        cci.bVisible = FALSE;
                        SetConsoleCursorInfo(stdo, &cci);
                    }
                    break;
                case 'l':
                    if (m == '?') {
                        for (i = 0; i <= n; i++) {
                            switch (v[i]) {
                                case 3:
                                    GetConsoleScreenBufferInfo(stdo, &csbi);
                                    w = csbi.dwSize.X;
                                    h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
                                    csize = w * (h + 1);
                                    coord.X = 0;
                                    coord.Y = csbi.srWindow.Top;
                                    FillConsoleOutputCharacter(stdo, ' ', csize, coord, &written);
                                    FillConsoleOutputAttribute(stdo, csbi.wAttributes, csize, coord, &written);
                                    SetConsoleCursorPosition(stdo, csbi.dwCursorPosition);
                                    csbi.srWindow.Right = csbi.srWindow.Left + 79;
                                    SetConsoleWindowInfo(stdo, TRUE, &csbi.srWindow);
                                    csbi.dwSize.X = 80;
                                    SetConsoleScreenBufferSize(stdo, csbi.dwSize);
                                    break;
                                case 5:
                                    attr =
                                        ((attr & FOREGROUND_MASK) << 4) |
                                        ((attr & BACKGROUND_MASK) >> 4);
                                    SetConsoleTextAttribute(stdo, attr);
                                    break;
                                case 25:
                                    GetConsoleCursorInfo(stdo, &cci);
                                    cci.bVisible = FALSE;
                                    SetConsoleCursorInfo(stdo, &cci);
                                    break;
                                default:
                                    break;
                            }
                        }
                    } else if (m == '>' && v[0] == 5) {
                        GetConsoleCursorInfo(stdo, &cci);
                        cci.bVisible = TRUE;
                        SetConsoleCursorInfo(stdo, &cci);
                    }
                    break;
                case 'm':
                    for (i = 0; i <= n; i++) {
                        if (v[i] == -1 || v[i] == 0)
                            attr = attr_reset;
                        else if (v[i] == 1)
                            attr |= FOREGROUND_INTENSITY;
                        else if (v[i] == 4)
                            attr |= FOREGROUND_INTENSITY;
                        else if (v[i] == 5) // blink is typically applied as bg intensity
                            attr |= BACKGROUND_INTENSITY;
                        else if (v[i] == 7)
                            attr =
                                ((attr & FOREGROUND_MASK) << 4) |
                                ((attr & BACKGROUND_MASK) >> 4);
                        else if (v[i] == 10)
                            ; // symbol on
                        else if (v[i] == 11)
                            ; // symbol off
                        else if (v[i] == 22)
                            attr &= ~FOREGROUND_INTENSITY;
                        else if (v[i] == 24)
                            attr &= ~FOREGROUND_INTENSITY;
                        else if (v[i] == 25)
                            attr &= ~BACKGROUND_INTENSITY;
                        else if (v[i] == 27)
                            attr =
                                ((attr & FOREGROUND_MASK) << 4) |
                                ((attr & BACKGROUND_MASK) >> 4);
                        else if (v[i] >= 30 && v[i] <= 37) {
                            attr &= ~FG_RGB; // doesn't affect attributes
                            if ((v[i] - 30) & 1)
                                attr |= FOREGROUND_RED;
                            if ((v[i] - 30) & 2)
                                attr |= FOREGROUND_GREEN;
                            if ((v[i] - 30) & 4)
                                attr |= FOREGROUND_BLUE;
                        } else if (v[i] == 39) // reset fg color and attributes
                            attr = (attr & ~FOREGROUND_MASK) | (attr_reset & FG_RGB);
                        else if (v[i] >= 40 && v[i] <= 47) {
                            attr &= ~BG_RGB;
                            if ((v[i] - 40) & 1)
                                attr |= BACKGROUND_RED;
                            if ((v[i] - 40) & 2)
                                attr |= BACKGROUND_GREEN;
                            if ((v[i] - 40) & 4)
                                attr |= BACKGROUND_BLUE;
                        } else if (v[i] == 49) // reset bg color
                            attr = (attr & ~BACKGROUND_MASK) | (attr_reset & BG_RGB);
                    }
                    SetConsoleTextAttribute(stdo, attr);
                    break;
                case 'K':
                    GetConsoleScreenBufferInfo(stdo, &csbi);
                    coord = csbi.dwCursorPosition;
                    switch (v[0]) {
                        default:
                        case 0:
                            csize = csbi.dwSize.X - coord.X;
                            break;
                        case 1:
                            csize = coord.X;
                            coord.X = 0;
                            break;
                        case 2:
                            csize = csbi.dwSize.X;
                            coord.X = 0;
                            break;
                    }
                    FillConsoleOutputCharacter(stdo, ' ', csize, coord, &written);
                    FillConsoleOutputAttribute(stdo, csbi.wAttributes, csize, coord, &written);
                    SetConsoleCursorPosition(stdo, csbi.dwCursorPosition);
                    break;
                case 'J':
                    GetConsoleScreenBufferInfo(stdo, &csbi);
                    w = csbi.dwSize.X;
                    h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
                    coord = csbi.dwCursorPosition;
                    switch (v[0]) {
                        default:
                        case 0:
                            csize = w * (h - coord.Y) - coord.X;
                            coord.X = 0;
                            break;
                        case 1:
                            csize = w * coord.Y + coord.X;
                            coord.X = 0;
                            coord.Y = csbi.srWindow.Top;
                            break;
                        case 2:
                            csize = w * (h + 1);
                            coord.X = 0;
                            coord.Y = csbi.srWindow.Top;
                            break;
                    }
                    FillConsoleOutputCharacter(stdo, ' ', csize, coord, &written);
                    FillConsoleOutputAttribute(stdo, csbi.wAttributes, csize, coord, &written);
                    SetConsoleCursorPosition(stdo, csbi.dwCursorPosition);
                    break;
                case 'H':
                    GetConsoleScreenBufferInfo(stdo, &csbi);
                    coord = csbi.dwCursorPosition;
                    if (v[0] != -1) {
                        if (v[1] != -1) {
                            coord.Y = csbi.srWindow.Top + v[0] - 1;
                            coord.X = v[1] - 1;
                        } else
                            coord.X = v[0] - 1;
                    } else {
                        coord.X = 0;
                        coord.Y = csbi.srWindow.Top;
                    }
                    if (coord.X < csbi.srWindow.Left)
                        coord.X = csbi.srWindow.Left;
                    else if (coord.X > csbi.srWindow.Right)
                        coord.X = csbi.srWindow.Right;
                    if (coord.Y < csbi.srWindow.Top)
                        coord.Y = csbi.srWindow.Top;
                    else if (coord.Y > csbi.srWindow.Bottom)
                        coord.Y = csbi.srWindow.Bottom;
                    SetConsoleCursorPosition(stdo, coord);
                    break;
                default:
                    break;
            }
        } else {
            putchar(*ptr);
            ptr++;
        }
    }
    return ptr - buf;
}

#endif /* _WIN32 */
