/* $Id: DBGCScreenAscii.cpp $ */
/** @file
 * DBGC - Debugger Console, ASCII screen with optional coloring support.
 */

/*
 * Copyright (C) 2016-2023 Oracle and/or its affiliates.
 *
 * This file is part of VirtualBox base platform packages, as
 * available from https://www.virtualbox.org.
 *
 * 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, in version 3 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses>.
 *
 * SPDX-License-Identifier: GPL-3.0-only
 */


/*********************************************************************************************************************************
*   Header Files                                                                                                                 *
*********************************************************************************************************************************/
#define LOG_GROUP LOG_GROUP_DBGC
#include <VBox/err.h>
#include <VBox/log.h>

#include <iprt/mem.h>
#include <iprt/string.h>

#include "DBGCInternal.h"


/*********************************************************************************************************************************
*   Defined Constants And Macros                                                                                                 *
*********************************************************************************************************************************/



/*********************************************************************************************************************************
*   Structures and Typedefs                                                                                                      *
*********************************************************************************************************************************/

/**
 * Debug console ASCII screen.
 */
typedef struct DBGCSCREENINT
{
    /** Width of the screen. */
    uint32_t                cchWidth;
    /** Height of the screen. */
    uint32_t                cchHeight;
    /** Extra amount of characters at the end of each line (usually terminator). */
    uint32_t                cchStride;
    /** Pointer to the char buffer. */
    char                   *pszScreen;
    /** Color information for each pixel. */
    PDBGCSCREENCOLOR        paColors;
} DBGCSCREENINT;
/** Pointer to an ASCII screen. */
typedef DBGCSCREENINT *PDBGCSCREENINT;


/*********************************************************************************************************************************
*   Internal Functions                                                                                                           *
*********************************************************************************************************************************/


/**
 * Returns the buffer starting at the given position.
 *
 * @returns Pointer to the ASCII buffer.
 * @param   pThis               The screen.
 * @param   uX                  Horizontal position.
 * @param   uY                  Vertical position.
 */
DECLINLINE(char *) dbgcScreenAsciiGetBufferAtPos(PDBGCSCREENINT pThis, uint32_t uX, uint32_t uY)
{
    AssertReturn(uX < pThis->cchWidth && uY < pThis->cchHeight, NULL);
    return pThis->pszScreen + (pThis->cchWidth + pThis->cchStride) * uY + uX;
}


/**
 * Returns the color buffer starting at the given position.
 *
 * @returns Pointer to the color buffer.
 * @param   pThis               The screen.
 * @param   uX                  Horizontal position.
 * @param   uY                  Vertical position.
 */
DECLINLINE(PDBGCSCREENCOLOR) dbgcScreenAsciiGetColorBufferAtPos(PDBGCSCREENINT pThis, uint32_t uX, uint32_t uY)
{
    AssertReturn(uX < pThis->cchWidth && uY < pThis->cchHeight, NULL);
    return &pThis->paColors[pThis->cchWidth  * uY + uX];
}


/**
 * Converts the given color the correct escape sequence.
 *
 * @returns Pointer to the string containing the escape sequence for the given color.
 * @param   enmColor            The color.
 */
static const char *dbgcScreenAsciiColorToEscapeSeq(DBGCSCREENCOLOR enmColor)
{
    const char *psz = NULL;

    switch (enmColor)
    {
        case DBGCSCREENCOLOR_DEFAULT:
            psz = "\033[0m";
            break;
        case DBGCSCREENCOLOR_BLACK:
            psz = "\033[30m";
            break;
        case DBGCSCREENCOLOR_BLACK_BRIGHT:
            psz = "\033[30;1m";
            break;
        case DBGCSCREENCOLOR_RED:
            psz = "\033[31m";
            break;
        case DBGCSCREENCOLOR_RED_BRIGHT:
            psz = "\033[31;1m";
            break;
        case DBGCSCREENCOLOR_GREEN:
            psz = "\033[32m";
            break;
        case DBGCSCREENCOLOR_GREEN_BRIGHT:
            psz = "\033[32;1m";
            break;
        case DBGCSCREENCOLOR_YELLOW:
            psz = "\033[33m";
            break;
        case DBGCSCREENCOLOR_YELLOW_BRIGHT:
            psz = "\033[33;1m";
            break;
        case DBGCSCREENCOLOR_BLUE:
            psz = "\033[34m";
            break;
        case DBGCSCREENCOLOR_BLUE_BRIGHT:
            psz = "\033[34;1m";
            break;
        case DBGCSCREENCOLOR_MAGENTA:
            psz = "\033[35m";
            break;
        case DBGCSCREENCOLOR_MAGENTA_BRIGHT:
            psz = "\033[35;1m";
            break;
        case DBGCSCREENCOLOR_CYAN:
            psz = "\033[36m";
            break;
        case DBGCSCREENCOLOR_CYAN_BRIGHT:
            psz = "\033[36;1m";
            break;
        case DBGCSCREENCOLOR_WHITE:
            psz = "\033[37m";
            break;
        case DBGCSCREENCOLOR_WHITE_BRIGHT:
            psz = "\033[37;1m";
            break;
        default:
            AssertFailed();
    }

    return psz;
}


/**
 * Creates a new ASCII screen for layouting.
 *
 * @returns VBox status code.
 * @param   phScreen            Where to store the handle to the screen instance on success.
 * @param   cchWidth            Width of the screen in characters.
 * @param   cchHeight           Height of the screen in characters.
 */
DECLHIDDEN(int) dbgcScreenAsciiCreate(PDBGCSCREEN phScreen, uint32_t cchWidth, uint32_t cchHeight)
{
    int rc = VINF_SUCCESS;

    PDBGCSCREENINT pThis = (PDBGCSCREENINT)RTMemAllocZ(sizeof(DBGCSCREENINT));
    if (pThis)
    {
        pThis->cchWidth  = cchWidth;
        pThis->cchHeight = cchHeight;
        pThis->cchStride = 1; /* Zero terminators after every line. */
        pThis->pszScreen = RTStrAlloc((cchWidth + 1) * cchHeight * sizeof(char));
        if (RT_LIKELY(pThis->pszScreen))
        {
            pThis->paColors  = (PDBGCSCREENCOLOR)RTMemAllocZ(cchWidth * cchHeight * sizeof(DBGCSCREENCOLOR));
            if (RT_LIKELY(pThis->paColors))
            {
                memset(pThis->pszScreen, 0, (cchWidth + 1) * cchHeight * sizeof(char));
                /* Initialize the screen with spaces. */
                for (uint32_t i = 0; i < cchHeight; i++)
                    dbgcScreenAsciiDrawLineHorizontal(pThis, 0, cchWidth - 1, i, ' ',
                                                      DBGCSCREENCOLOR_DEFAULT);
                *phScreen = pThis;
            }
            else
                rc = VERR_NO_MEMORY;

            if (RT_FAILURE(rc))
                RTStrFree(pThis->pszScreen);
        }
        else
            rc = VERR_NO_STR_MEMORY;

        if (RT_FAILURE(rc))
            RTMemFree(pThis);
    }
    else
        rc = VERR_NO_MEMORY;

    return rc;
}


/**
 * Destroys a given ASCII screen.
 *
 * @param   hScreen             The screen handle.
 */
DECLHIDDEN(void) dbgcScreenAsciiDestroy(DBGCSCREEN hScreen)
{
    PDBGCSCREENINT pThis = hScreen;
    AssertPtrReturnVoid(pThis);

    RTStrFree(pThis->pszScreen);
    RTMemFree(pThis->paColors);
    RTMemFree(pThis);
}


/**
 * Blits the entire screen using the given callback callback.
 *
 * @returns VBox status code.
 * @param   hScreen             The screen to blit.
 * @param   pfnBlit             Blitting callback.
 * @param   pvUser              Opaque user data to pass to the dumper callback.
 * @param   fAddColors          Flag whether to use the color info inserting
 *                              appropriate escape sequences.
 */
DECLHIDDEN(int) dbgcScreenAsciiBlit(DBGCSCREEN hScreen, PFNDGCSCREENBLIT pfnBlit, void *pvUser, bool fAddColors)
{
    int rc = VINF_SUCCESS;
    PDBGCSCREENINT pThis = hScreen;
    AssertPtrReturn(pThis, VERR_INVALID_HANDLE);

    if (!fAddColors)
    {
        for (uint32_t iY = 0; iY < pThis->cchHeight && RT_SUCCESS(rc); iY++)
        {
            /* Play safe and restore line endings. */
            char *psz = dbgcScreenAsciiGetBufferAtPos(pThis, 0, iY);
            psz[pThis->cchWidth] = '\0';
            rc = pfnBlit(psz, pvUser);
            if (RT_SUCCESS(rc))
                rc = pfnBlit("\n", pvUser);
        }
    }
    else
    {
        for (uint32_t iY = 0; iY < pThis->cchHeight && RT_SUCCESS(rc); iY++)
        {
            /* Play safe and restore line endings. */
            char *psz = dbgcScreenAsciiGetBufferAtPos(pThis, 0, iY);
            PDBGCSCREENCOLOR pColor = dbgcScreenAsciiGetColorBufferAtPos(pThis, 0, iY);
            psz[pThis->cchWidth] = '\0';

            /*
             * Blit only stuff with the same color at once so to be able to inject the
             * correct color escape sequences.
             */
            uint32_t uStartX = 0;
            while (   uStartX < pThis->cchWidth
                   && RT_SUCCESS(rc))
            {
                uint32_t cchWrite = 0;
                DBGCSCREENCOLOR enmColorStart = *pColor;
                while (   uStartX + cchWrite < pThis->cchWidth
                       && enmColorStart == *pColor)
                {
                    pColor++;
                    cchWrite++;
                }

                const char *pszEsc = dbgcScreenAsciiColorToEscapeSeq(enmColorStart);
                rc = pfnBlit(pszEsc, pvUser);
                if (RT_SUCCESS(rc))
                {
                    char chTmp = psz[cchWrite];
                    psz[cchWrite] = '\0';
                    rc = pfnBlit(psz, pvUser);
                    psz[cchWrite] = chTmp;
                    uStartX += cchWrite;
                    psz += cchWrite;
                }
            }
            rc = pfnBlit("\n", pvUser);
        }

        /* Restore to default values at the end. */
        if (RT_SUCCESS(rc))
        {
            const char *pszEsc = dbgcScreenAsciiColorToEscapeSeq(DBGCSCREENCOLOR_DEFAULT);
            rc = pfnBlit(pszEsc, pvUser);
        }
    }

    return rc;
}


/**
 * Draws a single character to the screen at the given coordinates.
 *
 * @returns VBox status code.
 * @param   hScreen             The screen handle.
 * @param   uX                  X coordinate.
 * @param   uY                  Y coordinate.
 * @param   ch                  Character to draw.
 * @param   enmColor            The color to use.
 */
DECLHIDDEN(int) dbgcScreenAsciiDrawCharacter(DBGCSCREEN hScreen, uint32_t uX, uint32_t uY, char ch,
                                             DBGCSCREENCOLOR enmColor)
{
    PDBGCSCREENINT pThis = hScreen;
    AssertPtrReturn(pThis, VERR_INVALID_HANDLE);

    char *psz = dbgcScreenAsciiGetBufferAtPos(pThis, uX, uY);
    PDBGCSCREENCOLOR pColor = dbgcScreenAsciiGetColorBufferAtPos(pThis, uX, uY);
    AssertPtrReturn(psz, VERR_INVALID_STATE);
    AssertPtrReturn(pColor, VERR_INVALID_STATE);
    AssertReturn(*psz != '\0', VERR_INVALID_STATE);

    *psz = ch;
    *pColor = enmColor;
    return VINF_SUCCESS;
}


/**
 * Draws a vertical line at the given coordinates.
 *
 * @returns VBox status code.
 * @param   hScreen             The screen handle.
 * @param   uX                  X position to draw.
 * @param   uStartY             Y position to start drawing.
 * @param   uEndY               Y position to draw to (inclusive).
 * @param   ch                  The character to use for drawing.
 * @param   enmColor            The color to use.
 */
DECLHIDDEN(int) dbgcScreenAsciiDrawLineVertical(DBGCSCREEN hScreen, uint32_t uX, uint32_t uStartY,
                                                uint32_t uEndY, char ch, DBGCSCREENCOLOR enmColor)
{
    PDBGCSCREENINT pThis = hScreen;
    AssertPtrReturn(pThis, VERR_INVALID_HANDLE);

    while (uStartY <= uEndY)
    {
        char *psz = dbgcScreenAsciiGetBufferAtPos(pThis, uX, uStartY);
        PDBGCSCREENCOLOR pColor = dbgcScreenAsciiGetColorBufferAtPos(pThis, uX, uStartY);
        AssertPtrReturn(psz, VERR_INVALID_STATE);
        AssertPtrReturn(pColor, VERR_INVALID_STATE);
        *psz = ch;
        *pColor = enmColor;
        uStartY++;
    }

    return VINF_SUCCESS;
}


/**
 * Draws a horizontal line at the given coordinates.
 *
 * @returns VBox status code..
 * @param   hScreen             The screen handle.
 * @param   uStartX             X position to start drawing.
 * @param   uEndX               X position to draw the line to (inclusive).
 * @param   uY                  Y position.
 * @param   ch                  The character to use for drawing.
 * @param   enmColor            The color to use.
 */
DECLHIDDEN(int) dbgcScreenAsciiDrawLineHorizontal(DBGCSCREEN hScreen, uint32_t uStartX, uint32_t uEndX,
                                                  uint32_t uY, char ch, DBGCSCREENCOLOR enmColor)
{
    PDBGCSCREENINT pThis = hScreen;
    AssertPtrReturn(pThis, VERR_INVALID_HANDLE);

    char *psz = dbgcScreenAsciiGetBufferAtPos(pThis, uStartX, uY);
    PDBGCSCREENCOLOR pColor = dbgcScreenAsciiGetColorBufferAtPos(pThis, uStartX, uY);
    AssertPtrReturn(psz, VERR_INVALID_STATE);
    AssertPtrReturn(pColor, VERR_INVALID_STATE);

    memset(psz, ch, uEndX - uStartX + 1);
    for (unsigned i = 0; i < uEndX - uStartX + 1; i++)
        pColor[i] = enmColor;

    return VINF_SUCCESS;
}


/**
 * Draws a given string to the screen.
 *
 * @returns VBox status code..
 * @param   hScreen             The screen handle.
 * @param   uX                  X position to start drawing.
 * @param   uY                  Y position.
 * @param   pszText             The string to draw.
 * @param   enmColor            The color to use.
 */
DECLHIDDEN(int) dbgcScreenAsciiDrawString(DBGCSCREEN hScreen, uint32_t uX, uint32_t uY, const char *pszText,
                                          DBGCSCREENCOLOR enmColor)
{
    PDBGCSCREENINT pThis = hScreen;
    size_t cchText = strlen(pszText);
    AssertPtrReturn(pThis, VERR_INVALID_HANDLE);
    AssertReturn(uX + cchText <= pThis->cchWidth, VERR_OUT_OF_RANGE);
    AssertReturn(uY < pThis->cchHeight, VERR_OUT_OF_RANGE);

    char *psz = dbgcScreenAsciiGetBufferAtPos(pThis, uX, uY);
    PDBGCSCREENCOLOR pColor = dbgcScreenAsciiGetColorBufferAtPos(pThis, uX, uY);
    AssertPtrReturn(psz, VERR_INVALID_STATE);
    AssertPtrReturn(pColor, VERR_INVALID_STATE);

    memcpy(psz, pszText, cchText);

    for (unsigned i = 0; i < cchText; i++)
        pColor[i] = enmColor;

    return VINF_SUCCESS;
}
