/** @file
    Terminal control utility functions.

    Copyright (C) 2018 Christian Zuckschwerdt

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
*/

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#ifndef _WIN32
#include <unistd.h>
#include <sys/ioctl.h>
#endif

#include "term_ctl.h"

#ifdef _WIN32
#include <io.h>
#include <limits.h>
#include <windows.h>
#include <lm.h>

#ifndef STDOUT_FILENO
#define STDOUT_FILENO   1
#endif

#ifndef STDERR_FILENO
#define STDERR_FILENO   2
#endif

#ifndef UNUSED
#define UNUSED(x) (void)(x)
#endif

typedef struct console {
    CONSOLE_SCREEN_BUFFER_INFO info;
    BOOL                       redirected;
    BOOL                       ansi;
    HANDLE                     hnd;
    FILE                      *file;
    WORD                       fg, bg;
} console_t;

static WORD _term_get_win_color(BOOL fore, term_color_t color)
{
    // applies intensity only on foreground,
    // i.e. background will fallback to dim
    switch (color) {
       case TERM_COLOR_RESET: /* Cannot occur; just to suppress a warning */
            break;
       case TERM_COLOR_BLACK:
            return (0);
       case TERM_COLOR_BLUE:
            return (1);
       case TERM_COLOR_GREEN:
            return (2);
       case TERM_COLOR_CYAN:
            return (3);
       case TERM_COLOR_RED:
            return (4);
       case TERM_COLOR_MAGENTA:
            return (5);
       case TERM_COLOR_YELLOW:
            return (6);
       case TERM_COLOR_WHITE:
            return (7);
       case TERM_COLOR_BRIGHT_BLACK:
            return (fore ? 0 + FOREGROUND_INTENSITY : 0);
       case TERM_COLOR_BRIGHT_BLUE:
            return (fore ? 1 + FOREGROUND_INTENSITY : 1);
       case TERM_COLOR_BRIGHT_GREEN:
            return (fore ? 2 + FOREGROUND_INTENSITY : 2);
       case TERM_COLOR_BRIGHT_CYAN:
            return (fore ? 3 + FOREGROUND_INTENSITY : 3);
       case TERM_COLOR_BRIGHT_RED:
            return (fore ? 4 + FOREGROUND_INTENSITY : 4);
       case TERM_COLOR_BRIGHT_MAGENTA:
            return (fore ? 5 + FOREGROUND_INTENSITY : 5);
       case TERM_COLOR_BRIGHT_YELLOW:
            return (fore ? 6 + FOREGROUND_INTENSITY : 6);
       case TERM_COLOR_BRIGHT_WHITE:
            return (fore ? 7 + FOREGROUND_INTENSITY : 7);
    }
    fprintf(stderr,"FATAL: No mapping for TERM_COLOR_x=%d (fore: %d)\n", color, fore);
    return (0);
}

static void _term_set_color(console_t *console, BOOL fore, term_color_t color)
{
    WORD win_color;

    if (!console->file)
        return;

    if (color == TERM_COLOR_RESET) {
        console->fg = (console->info.wAttributes & 7);
        console->bg = (console->info.wAttributes & ~7);
    }
    else if (fore) {
        console->fg = _term_get_win_color(TRUE, color);
    }
    else if (color <= TERM_COLOR_WHITE ||
            (color >= TERM_COLOR_BRIGHT_BLACK && color <= TERM_COLOR_BRIGHT_WHITE)) {
        console->bg = 16 * _term_get_win_color(FALSE, color);
    }
    else
        return;

    win_color = console->bg + console->fg;

    /* Hack: as WinCon does not have color-themes (as Linux have) and no 'TERM_COLOR_BRIGHT_x'
     * codes are used, always use a high-intensity foreground color. This look best in
     * CMD with the default black background color.
     *
     * Note: do not do this for "BLACK" as that would turn it into "GREY".
     */
    if (fore && color != TERM_COLOR_RESET && color != TERM_COLOR_BLACK)
        win_color |= FOREGROUND_INTENSITY;

    fflush(console->file);
    SetConsoleTextAttribute(console->hnd, win_color);
}

/*
 * Cleanup in case we got a SIGINT signal in the middle of a
 * non-default colour output.
 */
static void _term_free(console_t *console)
{
    if (console->hnd && console->hnd != INVALID_HANDLE_VALUE) {
        fflush(console->file);
        SetConsoleTextAttribute(console->hnd, console->info.wAttributes);
    }
    free(console);
}

static BOOL _term_has_color(console_t *console)
{
    return console->hnd && !console->redirected;
}

static void *_term_init(FILE *fp)
{
    console_t *console = calloc(1, sizeof(*console));
    if (!console) {
        fprintf(stderr, "term_init failed\n");
        return NULL; // NOTE: return NULL on alloc failure.
    }

    int fd = fileno(fp);
    if (fd == STDOUT_FILENO) {
        console->hnd = GetStdHandle(STD_OUTPUT_HANDLE);
        console->file = fp;
    }
    else if (fd == STDERR_FILENO) {
        console->hnd = GetStdHandle(STD_ERROR_HANDLE);
        console->file = fp;
    }
    console->redirected = (console->hnd == INVALID_HANDLE_VALUE) ||
                         (!GetConsoleScreenBufferInfo(console->hnd, &console->info)) ||
                         (GetFileType(console->hnd) != FILE_TYPE_CHAR);

    // Test for Windows 10 to enable ANSI output, needs netapi32.dll
    LPWKSTA_INFO_100 pBuf = NULL;
    NET_API_STATUS nStatus;
    nStatus = NetWkstaGetInfo(NULL, 100, (LPBYTE *)&pBuf);
    if (nStatus == NERR_Success) {
        console->ansi = (pBuf->wki100_platform_id == 500) && (pBuf->wki100_ver_major >= 10);
    }
    if (pBuf != NULL) {
        NetApiBufferFree(pBuf);
    }

    // Windows 10 version 1511 added ANSI filters to cmd and terminal.
    // To use ANSI colors in Windows versions 1511 to 1903 requires setting VirtualTerminalLevel
    // by calling the SetConsoleMode API with the ENABLE_VIRTUAL_TERMINAL_PROCESSING flag.
    // ANSI colors are available by default in Windows version 1909 or newer
    if (console->ansi) {
        DWORD dwMode = 0;
        GetConsoleMode(console->hnd, &dwMode);
        dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
        SetConsoleMode(console->hnd, dwMode);
        // Check if it worked, it will fail for Legacy console-mode
        GetConsoleMode (console->hnd, &dwMode);
        if (!(dwMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)) {
            console->ansi = 0;
        }
    }

    _term_set_color(console, FALSE, TERM_COLOR_RESET); /* Set 'console->fg' and 'console->bg' */

    return console;
}
#endif /* _WIN32 */

int term_get_columns(void *ctx)
{
#ifdef _WIN32
    console_t *console = (console_t *)ctx;
    /*
     * Call this again as the screen dimensions could have changes since
     * we called '_term_init()'.
     */
    CONSOLE_SCREEN_BUFFER_INFO c_info;

    if (!console->hnd || console->hnd == INVALID_HANDLE_VALUE)
       return (80);

    if (!GetConsoleScreenBufferInfo(console->hnd, &c_info))
       return (80);
    return (c_info.srWindow.Right - c_info.srWindow.Left + 1);
#else
    FILE *fp = (FILE *)ctx;
    struct winsize w;
    ioctl(fileno(fp), TIOCGWINSZ, &w);
    return w.ws_col;
#endif
}

/**
Returns wether the environment suggests a dark or light
terminal background.

Our default color theme is for dark backgrounds.

Check the COLORFGBG environment variable, which should
be 15;0 for a dark theme and 0;15 for a light theme.

If the last value is 7 or 9 to 15 then assume a light theme.

@return 1 if a light background was detected, 0 for a dark background otherwise
*/
static int term_get_bg(void)
{
    char const *colorfgbg = getenv("COLORFGBG");
    if (!colorfgbg) {
        return 0; // default dark theme
    }
    char *p = strrchr(colorfgbg, ';');
    if (!p) {
        return 0; // default dark theme
    }

    // Check if the last value is 7 or 9 to 15.
    if (p[1] == '7' || p[1] == '9'
            || (p[1] == '1' && p[2] != '\0')) {
        return 1; // light theme
    }
    return 0; // dark theme
}

int term_has_color(void *ctx)
{
#ifdef _WIN32
    return _term_has_color(ctx);
#else
    char const *color = getenv("RTL433_COLOR");
    if (color && strcmp(color, "always") == 0) {
        return 1;
    }
    if (color && strcmp(color, "never") == 0) {
        return 0;
    }

    char const *no_color = getenv("NO_COLOR");
    if (no_color && no_color[0] != '\0') {
        return 0;
    }

    FILE *fp = (FILE *)ctx;
    return isatty(fileno(fp));
#endif
}

void *term_init(FILE *fp)
{
#ifdef _WIN32
    return _term_init(fp);
#else
    return fp;
#endif
}

void term_free(void *ctx)
{
    if (!ctx)
        return;
#ifdef _WIN32
    _term_free(ctx);
#else
    FILE *fp = (FILE *)ctx;
    if (term_has_color(ctx))
        fprintf(fp, "\033[0m");
#endif
}

void term_ring_bell(void *ctx)
{
#ifdef _WIN32
    Beep(800, 20);
    UNUSED(ctx);
#else
    FILE *fp = (FILE *)ctx;
    fprintf(fp, "\a");
#endif
}

void term_set_fg(void *ctx, term_color_t color)
{
    // Cache the detected terminal background color
    static int light_bg = -1;
    if (light_bg == -1) {
        light_bg = term_get_bg();
    }

#ifdef _WIN32
    console_t *console = (console_t *)ctx;
    if (!console->ansi) {
        _term_set_color(ctx, TRUE, color);
        return;
    }
    FILE *fp = console->file;
#else
    FILE *fp = (FILE *)ctx;
#endif
    if (color == TERM_COLOR_RESET) {
        fprintf(fp, "\033[0m");
    }
    else if (light_bg) {
        fprintf(fp, "\033[%dm", color); // normal colors on light backgrounds
    }
    else {
        fprintf(fp, "\033[%d;1m", color); // bright/bold colors on dark backgrounds
    }
}

void term_set_bg(void *ctx, term_color_t bg, term_color_t fg)
{
    // Cache the detected terminal background color
    static int light_bg = -1;
    if (light_bg == -1) {
        light_bg = term_get_bg();
    }
    if (light_bg && fg >= TERM_COLOR_BRIGHT_BLACK && fg <= TERM_COLOR_BRIGHT_WHITE) {
        fg -= 60; // remove bright/bold foreground on light backgrounds
    }

    if (bg < TERM_COLOR_BLACK
            || (bg > TERM_COLOR_WHITE && bg < TERM_COLOR_BRIGHT_BLACK)
            || bg > TERM_COLOR_BRIGHT_WHITE) {
        bg = 0;
    }
    if (fg < TERM_COLOR_BLACK
            || (fg > TERM_COLOR_WHITE && fg < TERM_COLOR_BRIGHT_BLACK)
            || fg > TERM_COLOR_BRIGHT_WHITE) {
        fg = 0;
    }

#ifdef _WIN32
    console_t *console = (console_t *)ctx;
    if (!console->ansi) {
        if (bg)
            _term_set_color(ctx, FALSE, bg);
        if (fg)
            _term_set_color(ctx, TRUE, fg);
        return;
    }
    FILE *fp = console->file;
#else
    FILE *fp = (FILE *)ctx;
#endif
    if (bg && fg)
        fprintf(fp, "\033[%d;%dm", bg + 10, fg);
    else if (bg)
        fprintf(fp, "\033[%dm", bg + 10);
    else if (fg)
        fprintf(fp, "\033[%dm", fg);
}

#define DIM(array) (int) (sizeof(array) / sizeof(array[0]))

static term_color_t color_map[] = {
                    TERM_COLOR_RESET,     /* "~0" */
                    TERM_COLOR_GREEN,
                    TERM_COLOR_WHITE,     /* "~2" */
                    TERM_COLOR_BLUE,
                    TERM_COLOR_CYAN,      /* "~4" */
                    TERM_COLOR_MAGENTA,
                    TERM_COLOR_YELLOW,    /* "~6" */
                    TERM_COLOR_BLACK,
                    TERM_COLOR_RED,       /* "~8" */
                  };

int term_set_color_map(int ascii_idx, term_color_t color)
{
    ascii_idx -= '0';
    if (ascii_idx < 0 || ascii_idx >= DIM(color_map))
        return -1;
    color_map[ascii_idx] = color;
    return ascii_idx;
}

int term_get_color_map(int ascii_idx)
{
    int i;

    ascii_idx -= '0';
    for (i = 0; ascii_idx >= 0 && i < DIM(color_map); i++)
        if (i == ascii_idx)
           return (int)color_map[i];
    return -1;
}

int term_puts(void *ctx, char const *buf)
{
    char const *p = buf;
    int i, len, buf_len, color;
    FILE *fp;

    if (!ctx)
        return fprintf(stderr, "%s", buf);

#ifdef _WIN32
    console_t *console = (console_t *)ctx;
    fp = console->file;
#else
    fp = (FILE *)ctx;
#endif

    if (!fp)
        fp = stderr;

    buf_len = (int)strlen(buf);
    for (i = len = 0; *p && i < buf_len; i++, p++) {
        if (*p == '~') {
            p++;
            color = ctx ? term_get_color_map(*p) : -1;
            if (color >= 0)
                term_set_fg(ctx, (term_color_t)color);
        }
        else {
            fputc(*p, fp);
            len++;
        }
    }
    return len;
}

int term_printf(void *ctx, _Printf_format_string_ char const *format, ...)
{
    int len;
    va_list args;
    char buf[4000];

    va_start(args, format);

    // Terminate first in case a buggy '_MSC_VER < 1900' is used.
    buf[sizeof(buf)-1] = '\0';
    vsnprintf(buf, sizeof(buf)-1, format, args);
    len = term_puts(ctx, buf);
    va_end (args);
    return len;
}

int term_help_fputs(void *ctx, char const *buf, FILE *fp)
{
    char const *p = buf;
    int i, len, buf_len, color, state = 0, set_color = -1, next_color = -1;
    if (!fp) {
        fp = stderr;
    }

    if (!ctx) {
        return fprintf(fp, "%s", buf);
    }

#ifdef _WIN32
    console_t *console = (console_t *)ctx;
    fp = console->file;
#else
    fp = (FILE *)ctx;
#endif

    buf_len = (int)strlen(buf);
    for (i = len = 0; *p && i < buf_len; i++, p++) {
        if (*p == '~') {
            p++;
            color = ctx ? term_get_color_map(*p) : -1;
            if (color >= 0)
                term_set_fg(ctx, (term_color_t)color);
            continue;
        }

        if (state == 0 && *p == '[') {
            state = 1;
            next_color = 5;
        }
        else if ((state == 1 || state == 2) && *p == ']' && (p[1] == ',' || (p[1] == ' ' && p[2] != '|') || p[1] == '\n' || p[1] == '\0')) {
            state = 0;
            set_color = 0;
        }
        else if (state == 1 && *p == ' ') {
            state = 2;
            next_color = 6;
        }
        else if (state == 2 && *p == '|') {
            set_color = 0;
            next_color = 6;
        }

        else if (state == 0 && *p == '=' && p[1] == ' ') {
            state = 3;
            set_color = 1;
        }
        else if (state == 3 && *p == '=') {
            state = 0;
            next_color = 0;
        }

        else if (state == 0 && *p == '\'') {
            state = 4;
            next_color = 4;
        }
        else if (state == 4 && *p == '\'') {
            state = 0;
            set_color = 0;
        }

        else if (state == 0 && *p == '\"') {
            state = 5;
            set_color = 4;
        }
        else if (state == 5 && *p == '\"') {
            state = 0;
            next_color = 0;
        }

        if (set_color >= 0) {
            color = ctx ? (int)color_map[set_color] : -1;
            if (color >= 0)
                term_set_fg(ctx, (term_color_t)color);
        }
        set_color = next_color;
        next_color = -1;

        fputc(*p, fp);
        len++;
    }
    return len;
}

int term_help_fprintf(FILE *fp, _Printf_format_string_ char const *format, ...)
{
    int len;
    va_list args;
    char buf[4000];

    va_start(args, format);

    void *term = term_init(fp);
    if (!term_has_color(term)) {
        term_free(term);
        term = NULL;
    }

    // Terminate first in case a buggy '_MSC_VER < 1900' is used.
    buf[sizeof(buf) - 1] = '\0';
    vsnprintf(buf, sizeof(buf) - 1, format, args);
    len = term_help_fputs(term, buf, fp);

    term_free(term);

    va_end(args);
    return len;
}
