/****************************************************************************

    PROGRAM: wterm.c

    PURPOSE: Implementation of TermWClass Windows

    FUNCTIONS:


    COMMENTS:


****************************************************************************/

#include "windows.h"
#include "stdlib.h"
#include "memory.h"
#include "wterm.h"

#define MAX_ROWS 24
#define MAX_COLS 80

typedef struct WData
{
    // Function to execute for processing a menu
    MFUNCP pMenuProc;

    // Function to execute for processing a single character
    CFUNCP pCharProc;

    // Function to execute when window is closed (terminated)
    TFUNCP pCloseProc;

    // Pass on callback
    void *pvCallBackData;

    BOOL fGotFocus;

    BOOL fCaretHidden;

    // Rows on the screen
    int cRows;

    // Columns on the screen
    int cCols;

    // Row at top of screen
    int iTopRow;

    // Row at bottom of the screen
    int iBottomRow;

    // First Column on screen
    int iFirstCol;

    // Column at bottom of the screen
    int iBottomCol;

    // Row for next character
    int iNextRow;

    // Row for next column
    int iNextCol;

    // Width of character
    int cxChar;

    // Height of character
    int cyChar;

    // Memory image of screen this is treated as a circular buffer
    TCHAR aImage[MAX_ROWS] [MAX_COLS];

    // First row in circular screen buffer
    int iBufferTop;
} WData;

static HANDLE hInst = 0;
TCHAR BlankLine[80];

static int
row_diff(
    int row1,
    int row2)
{
    return (row2 > row1)
        ? MAX_ROWS - (row2 - row1)
        : row1 - row2;
}

static void
set_vscroll_pos(
    HWND hwnd,
    WData *pwdata)
{
    if (pwdata->cRows != 0)
    {
        // Save a few indirections by caching cRows
        register int cRows = pwdata->cRows;

        // calculate distance bottom of screen from top of data buffer
        register int top_from_row = row_diff(pwdata->iBottomRow,
            pwdata->iBufferTop);

        // Output position of scroll bar
        int new_pos = 0;

        if (top_from_row >= cRows)
        {
            // Calculate number of screens to display entire buffer
            int screens_for_data = MAX_ROWS / cRows
               + ((MAX_ROWS % cRows != 0) ? 1 : 0);

            // Figure out which screen the row falls in
            int screen_loc = top_from_row / cRows
                + ((top_from_row % cRows != 0) ? 1 : 0);

            // If the screen is in the last one set box to max
            new_pos = (screen_loc == screens_for_data)
                ? MAX_ROWS : screen_loc * cRows;
        }

        SetScrollPos(hwnd, SB_VERT, new_pos, TRUE);
    }
}

static int
calc_row(
    register int row,
    WData *pwdata)
{
    register int top = pwdata->iTopRow;
    static int boopa = 0;

    if (top > row)
        boopa++;

    return (row >= top) ? row - top : (MAX_ROWS - (top - row));
}

static void
display_text(
    HWND hwnd,
    int row,
    int col,
    LPTSTR text,
    int text_len,
    WData *pWData)
{
    // Get the DC to display the text
    HDC hdc = GetDC(hwnd);

    // Select Font
    SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));

    // Hide caret while we are printing
    HideCaret(hwnd);

    // Update the screen
    TextOut(hdc, (col - pWData->iFirstCol) * pWData->cxChar,
        calc_row(row, pWData) * pWData->cyChar, text, text_len);

    // Done with DC
    ReleaseDC(hwnd, hdc);

    // Put the caret back now that we are done
    ShowCaret(hwnd);
}

static void
display_char(
    HWND hwnd,
    TCHAR char_to_display,
    WData *pWData)
{
    // Update image buffer
    pWData->aImage[pWData->iNextRow][pWData->iNextCol] = char_to_display;

    display_text(hwnd, pWData->iNextRow, pWData->iNextCol,
      &char_to_display, 1, pWData);
}

static void
do_backspace(
    HWND hwnd,
    WData *pWData)
{
    // Point to the previous character in the line
    if (--pWData->iNextCol < 0)
    {
        // Can't backspace beyond the current line
        pWData->iNextCol = 0;
        return;
    }

    display_char(hwnd, ' ', pWData);

    // Null character for repaint
    pWData->aImage[pWData->iNextRow][pWData->iNextCol] = '\0';
}

static int
inc_row(
    int row,
    int increment)
{
    row += increment;

    if (row >= MAX_ROWS)
    {
        row -= MAX_ROWS;
    }
    else if (row < 0)
    {
        row += MAX_ROWS;
    }

    return row;
}

void
inc_next_row(
    HWND hwnd,
    WData *pWData)
{
    if (pWData->iNextRow == pWData->iBottomRow)
    {
        // Line is at bottom -- scroll the client area one row
        ScrollWindow(hwnd, 0, -pWData->cyChar, NULL, NULL);

        // Increment the top & bottom of the screen
        pWData->iTopRow = inc_row(pWData->iTopRow, 1);
        pWData->iBottomRow = inc_row(pWData->iBottomRow, 1);
    }

    // Increment the row
    pWData->iNextRow = inc_row(pWData->iNextRow, 1);

    if (pWData->iNextRow == pWData->iBufferTop)
    {
        // Have to reset circular buffer to next
        pWData->iBufferTop = inc_row(pWData->iBufferTop, 1);

        // Reset line to nulls for repaint
        memset(&pWData->aImage[pWData->iNextRow][0], '\0', MAX_COLS);
    }

    pWData->iNextCol = 0;
}

static void
do_cr(
    HWND hwnd,
    WData *pWData)
{
    // Set position to next row
    inc_next_row(hwnd, pWData);
    pWData->iNextCol = 0;

    // Make sure next character is null for repaint of line
    pWData->aImage[pWData->iNextRow][pWData->iNextCol] = '\0';

    // Update the vertical scroll bar's position
    set_vscroll_pos(hwnd, pWData);
}

static void
do_char(
    HWND hwnd,
    WPARAM wParam,
    WData *pWData)
{
    display_char(hwnd, (TCHAR) wParam, pWData);

    // Point to the next character in the line
    if (++pWData->iNextCol > MAX_COLS)
    {
        // Handle switch to next line
        inc_next_row(hwnd, pWData);
    }
}

static void
do_tab(
    HWND hwnd,
    WData *pWData)
{
    int c = pWData->iNextCol % 8;

    if ((pWData->iNextCol + c) <= MAX_COLS)
    {
        for ( ; c; c--)
        {
            do_char(hwnd, ' ', pWData);
        }
    }
    else
    {
        do_cr(hwnd, pWData);
    }
}

static void
EchoChar(
    HWND hwnd,
    WORD cRepeats,
    WPARAM wParam,
    WData *pWData)
{
    for ( ; cRepeats; cRepeats--)
    {
        switch (wParam)
        {
        // Backspace
        case '\b':
            do_backspace(hwnd, pWData);
            break;

        // Carriage return
        case '\n':
        case '\r':
            do_cr(hwnd, pWData);
            break;

        // Tab
        case '\t':
            do_tab(hwnd, pWData);
            break;

        // Regular characters
        default:
            do_char(hwnd, wParam, pWData);
        }
    }

    // The row is guaranteed to be on the screen because we will
    // scroll on a CR. However, the next column for input may be
    // beyond the window we are working in.
    if (pWData->iNextCol > pWData->iBottomCol)
    {
        // We are out of the window so scroll the window one
        // column to the right.
        SendMessage(hwnd, WM_HSCROLL, SB_LINEDOWN, 0L);
    }
    else if (pWData->iNextCol < pWData->iFirstCol)
    {
        // We are out of the window so repaint the window using
        // iNextCol as the first column for the screen.
        pWData->iFirstCol = pWData->iNextCol;
        pWData->iBottomCol = pWData->iFirstCol + pWData->cCols - 1;

        // Reset scroll bar
        SetScrollPos(hwnd, SB_HORZ, pWData->iFirstCol, TRUE);

        // Tell window to update itself.
        InvalidateRect(hwnd, NULL, TRUE);
        UpdateWindow(hwnd);
    }
    else
    {
        // Reset Caret's position
        SetCaretPos((pWData->iNextCol - pWData->iFirstCol) * pWData->cxChar,
            calc_row(pWData->iNextRow, pWData) * pWData->cyChar);
    }
}

/****************************************************************************

    FUNCTION: WmCreate(HWND)

    PURPOSE:  Initializes control structures for a TermWClass Window

    MESSAGES:
              WM_CREATE

    COMMENTS:

            This prepares a window for processing character based
            I/O. In particular it does stuff like calculate the
            size of the window needed.

****************************************************************************/
static void
WmCreate(
    HWND hwnd,
    CREATESTRUCT *pInit)
{
    WData *pData = (WData *) (pInit->lpCreateParams);
    HDC hdc = GetDC(hwnd);
    TEXTMETRIC tm;

    // Store pointer to window data
    SetWindowLong(hwnd, 0, (LONG) pData);

    // Set font to system fixed font
    SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));

    // Calculate size of a character
    GetTextMetrics(hdc, &tm);
    pData->cxChar = tm.tmAveCharWidth;
    pData->cyChar = tm.tmHeight;
    ReleaseDC(hwnd, hdc);

    // Set up vertical scroll bars
    SetScrollRange(hwnd, SB_VERT, 0, MAX_ROWS, TRUE);
    SetScrollPos(hwnd, SB_VERT, 0, TRUE);

    // Set up horizontal scroll bars
    SetScrollRange(hwnd, SB_HORZ, 0, MAX_COLS, TRUE);
    SetScrollPos(hwnd, SB_HORZ, 0, TRUE);
}

/****************************************************************************

    FUNCTION: WmSize(HWND, WORD, LONG)

    PURPOSE:  Processes a size message

    MESSAGES:

    COMMENTS:

****************************************************************************/
static void
WmSize(
    HWND hwnd,
    WPARAM wParam,
    LONG lParam,
    WData *pwdata)
{
    // Get the new size of the window
    int cxClient;
    int cyClient;
    int cRowChange = pwdata->cRows;
    RECT rect;

    // Get size of client area
    GetClientRect(hwnd, &rect);

    // Calculate size of client area
    cxClient = rect.right - rect.left;
    cyClient = rect.bottom - rect.top;

    // Calculate size of area in rows
    pwdata->cCols = cxClient / pwdata->cxChar;
    pwdata->cRows = min(MAX_ROWS, cyClient / pwdata->cyChar);
    pwdata->iBottomCol = min(pwdata->iFirstCol + pwdata->cCols, MAX_COLS);
    cRowChange = pwdata->cRows - cRowChange;

    // Keep input line toward bottom of screen
    if (cRowChange < 0)
    {
        // Screen has shrunk in size.
        if (pwdata->iNextRow != pwdata->iTopRow)
        {
            // Has input row moved out of screen?
            if (row_diff(pwdata->iNextRow, pwdata->iTopRow) >= pwdata->cRows)
            {
                // Yes -- Calculate top new top that puts input line on
                // the bottom.
                pwdata->iTopRow =
                    inc_row(pwdata->iNextRow, 1 - pwdata->cRows);
            }
        }
    }
    else
    {
        // Screen has gotten bigger -- Display more text if possible
        if (pwdata->iTopRow != pwdata->iBufferTop)
        {
            pwdata->iTopRow = inc_row(pwdata->iTopRow,
                -(min(row_diff(pwdata->iTopRow, pwdata->iBufferTop),
                    cRowChange)));
        }
    }

    // Calculate new bottom
    pwdata->iBottomRow = inc_row(pwdata->iTopRow, pwdata->cRows - 1);

    InvalidateRect(hwnd, NULL, TRUE);
    UpdateWindow(hwnd);
}

static void
WmSetFocus(
    HWND hwnd,
    WData *pwdata)
{
    // save indirections
    register int cxchar = pwdata->cxChar;
    register int cychar = pwdata->cyChar;
    pwdata->fGotFocus = TRUE;
    CreateCaret(hwnd, NULL, cxchar, cychar);

    if (!pwdata->fCaretHidden)
    {
        SetCaretPos(pwdata->iNextCol * cxchar,
            calc_row(pwdata->iNextRow, pwdata) * cychar);
    }

    ShowCaret(hwnd);
}

static void
WmKillFocus(
    HWND hwnd,
    WData *pwdata)
{
    pwdata->fGotFocus = FALSE;

    if (!pwdata->fCaretHidden)
    {
        HideCaret(hwnd);
    }

    DestroyCaret();
}

static void
WmVscroll(
    HWND hwnd,
    WPARAM wParam,
    LONG lParam,
    WData *pwdata)
{
    int cVscrollInc = 0;
    register int top_diff = row_diff(pwdata->iTopRow, pwdata->iBufferTop);
    register int bottom_diff = MAX_ROWS - (top_diff + pwdata->cRows);

    switch(wParam)
    {
    case SB_TOP:

        if (top_diff != 0)
        {
            cVscrollInc = -top_diff;
        }

        break;

    case SB_BOTTOM:

        if (bottom_diff != 0)
        {
            cVscrollInc = bottom_diff;
        }

        break;

    case SB_LINEUP:

        if (top_diff != 0)
        {
            cVscrollInc = -1;
        }

        break;

    case SB_LINEDOWN:

        if (bottom_diff != 0)
        {
            cVscrollInc = 1;
        }

        break;

    case SB_PAGEUP:

        if (top_diff != 0)
        {
            cVscrollInc = - ((top_diff > pwdata->cRows)
                ? pwdata->cRows : top_diff);
        }

        break;

    case SB_PAGEDOWN:

        if (bottom_diff != 0)
        {
            cVscrollInc = (bottom_diff > pwdata->cRows)
                ? pwdata->cRows : bottom_diff;
        }

        break;

    case SB_THUMBTRACK:

        if (LOWORD(lParam) != 0)
        {
            cVscrollInc = LOWORD(lParam)
                - row_diff(pwdata->iTopRow, pwdata->iBufferTop);
        }
    }

    // Cacluate new top row
    if (cVscrollInc != 0)
    {
        // Calculate new top and bottom
        pwdata->iTopRow = inc_row(pwdata->iTopRow, cVscrollInc);
        pwdata->iBottomRow = inc_row(pwdata->iTopRow, pwdata->cRows);

        // Scroll window
        ScrollWindow(hwnd, 0, pwdata->cyChar * cVscrollInc, NULL, NULL);

        // Reset scroll bar
        set_vscroll_pos(hwnd, pwdata);

        // Tell window to update itself.
        InvalidateRect(hwnd, NULL, TRUE);
        UpdateWindow(hwnd);
    }
}

static void
WmHscroll(
    HWND hwnd,
    WPARAM wParam,
    LONG lParam,
    WData *pwdata)
{
    register int cHscrollInc = 0;

    switch(wParam)
    {
    case SB_LINEUP:

        cHscrollInc = -1;
        break;

    case SB_LINEDOWN:

        cHscrollInc = 1;
        break;

    case SB_PAGEUP:

        cHscrollInc = -8;
        break;

    case SB_PAGEDOWN:

        cHscrollInc = 8;
        break;

    case SB_THUMBTRACK:

        if (LOWORD(lParam) != 0)
        {
            cHscrollInc = LOWORD(lParam) - pwdata->iFirstCol;
        }
    }

    if (cHscrollInc != 0)
    {
        // Cacluate new first column
        register int NormalizedScrollInc = cHscrollInc + pwdata->iFirstCol;

        if (NormalizedScrollInc < 0)
        {
            cHscrollInc = -pwdata->iFirstCol;
        }
        else if (NormalizedScrollInc > MAX_COLS - pwdata->cCols)
        {
            cHscrollInc = (MAX_COLS - pwdata->cCols) - pwdata->iFirstCol;
        }

        pwdata->iFirstCol += cHscrollInc;
        pwdata->iBottomCol = pwdata->iFirstCol + pwdata->cCols - 1;

        // Scroll window
        ScrollWindow(hwnd, -(pwdata->cxChar * cHscrollInc), 0, NULL, NULL);

        // Reset scroll bar
        SetScrollPos(hwnd, SB_HORZ, pwdata->iFirstCol, TRUE);

        // Tell window to update itself.
        InvalidateRect(hwnd, NULL, TRUE);
        UpdateWindow(hwnd);
    }
}

static void
WmPaint(
    HWND hwnd,
    WData *pwdata)
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);
    register int row = pwdata->iTopRow;
    register int col = pwdata->iFirstCol;
    int bottom_row = pwdata->iBottomRow;
    int cxChar = pwdata->cxChar;
    int cyChar = pwdata->cyChar;
    int y;

    // Select System Font
    SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));

    while (TRUE)
    {
	int len = lstrlen(&pwdata->aImage[row][col]);

        if (len != 0)
        {
            y = calc_row(row, pwdata) * cyChar;
	    TextOut(hdc, 0, y, &pwdata->aImage[row][col], len);
        }

        if (row == bottom_row)
        {
            break;
        }

        row = inc_row(row, 1);
    }

    if (pwdata->fGotFocus)
    {
        if ((pwdata->iNextCol >= pwdata->iFirstCol)
            && (row_diff(pwdata->iNextRow, pwdata->iTopRow) < pwdata->cRows))
        {
            if (pwdata->fCaretHidden)
            {
                pwdata->fCaretHidden = FALSE;
                ShowCaret(hwnd);
            }

            SetCaretPos(
                (pwdata->iNextCol - pwdata->iFirstCol) * pwdata->cxChar,
                calc_row(pwdata->iNextRow, pwdata) * pwdata->cyChar);
        }
        else
        {
            if (!pwdata->fCaretHidden)
            {
                pwdata->fCaretHidden = TRUE;
                HideCaret(hwnd);
            }
        }
    }

    EndPaint(hwnd, &ps);
}





//
//  FUNCTION:   WmPrintLine
//
//  PURPOSE:    Print a line on the screen.
//
//  Note: this is a user message not an intrinsic Window's message.
//
void
WmPrintLine(
    HWND hwnd,
    WPARAM wParam,
    LONG lParam,
    WData *pTermData)
{
    TCHAR *pBuf = (TCHAR *) lParam;

    // MessageBox(hwnd, L"WmPrintLine", L"Debug", MB_OK);

    // DebugBreak();

    while (wParam--)
    {
        // Is character a lf?
        if (*pBuf == '\n')
        {
            // Convert to cr since that is what this window uses
            *pBuf = '\r';
        }

        // Write the character to the window
        EchoChar(hwnd, 1, *pBuf++, pTermData);
    }

}

//
//  FUNCTION:   WmPutc
//
//  PURPOSE:    Print a single character on the screen
//
//  Note: this is a user message not an intrinsic Window's message.
//
void
WmPutc(
    HWND hwnd,
    WPARAM wParam,
    WData *pTermData)
{
    // Is character a lf?
    if (wParam == '\n')
    {
        // Convert to cr since that is what this window uses
        wParam = '\r';
    }

    // Write the character to the window
    EchoChar(hwnd, 1, wParam, pTermData);
}


/****************************************************************************

    FUNCTION: TermWndProc(HWND, unsigned, WORD, LONG)

    PURPOSE:  Processes messages

    MESSAGES:

    COMMENTS:

****************************************************************************/

long TermWndProc(
    HWND hWnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam)
{
    WData *pTerm = (WData *) GetWindowLong(hWnd, 0);

    switch (message)
    {
        case WM_CREATE:
            WmCreate(hWnd, (CREATESTRUCT *) lParam);
            break;

        case WM_COMMAND:
        case WM_SYSCOMMAND:
            // Call procedure that processes the menus
            return (*(pTerm->pMenuProc))(hWnd, message, wParam, lParam,
                pTerm->pvCallBackData);

        case WM_SIZE:
            WmSize(hWnd, wParam, lParam, pTerm);
            break;

        case WM_SETFOCUS:
            WmSetFocus(hWnd, pTerm);
            break;

        case WM_KILLFOCUS:
            WmKillFocus(hWnd, pTerm);
            break;

        case WM_VSCROLL:
            WmVscroll(hWnd, wParam, lParam, pTerm);
            break;

        case WM_HSCROLL:
            WmHscroll(hWnd, wParam, lParam, pTerm);
            break;

        case WM_CHAR:
            // Character message echo and put in buffer
            return (*(pTerm->pCharProc))(hWnd, message, wParam, lParam,
                pTerm->pvCallBackData);

        case WM_PAINT:
            WmPaint(hWnd, pTerm);
            break;

        case WM_CLOSE:
            DestroyWindow(hWnd);
            break;

        case WM_NCDESTROY:
            // Call close notification procedure
            return (*(pTerm->pCloseProc))(hWnd, message, wParam, lParam,
                pTerm->pvCallBackData);

        case WM_PRINT_LINE:
            WmPrintLine(hWnd, wParam, lParam, pTerm);
            break;

        case WM_PUTC:
            WmPutc(hWnd, wParam, pTerm);
            break;

	case WM_DESTROY:
	    PostQuitMessage(0);
	    break;

	case WM_TERM_WND:
	    DestroyWindow(hWnd);
	    break;

	default:			  /* Passes it on if unproccessed    */
            return (DefWindowProc(hWnd, message, wParam, lParam));
    }

    return 0;
}


/****************************************************************************

    FUNCTION: TermRegisterClass(HANDLE)

    PURPOSE:  Register a class for a terminal window

    COMMENTS:


****************************************************************************/

BOOL TermRegisterClass(
    HANDLE hInstance,
    LPTSTR MenuName,
    LPTSTR ClassName,
    LPTSTR Icon)
{
    WNDCLASS  wc;
    BOOL retVal;

    // Make sure blank line is blank
    memset(BlankLine, ' ', 80);

    /* Fill in window class structure with parameters that describe the       */
    /* main window.                                                           */

    wc.style = 0;
    wc.lpfnWndProc = TermWndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = sizeof(WData *);
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, Icon);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName =  MenuName;
    wc.lpszClassName = ClassName;

    /* Register the window class and return success/failure code. */
    if (retVal = RegisterClass(&wc))
    {
        // Class got registered -- so finish set up
        hInst = hInstance;
    }

    return retVal;
}


/****************************************************************************

    FUNCTION:  TermCreateWindow(LPTSTR, LPTSTR, HMENU, void *, void *, int)

    PURPOSE:   Create a window of a previously registered window class

    COMMENTS:


****************************************************************************/

BOOL
TermCreateWindow(
    LPTSTR lpClassName,
    LPTSTR lpWindowName,
    HMENU hMenu,
    MFUNCP MenuProc,
    CFUNCP CharProc,
    TFUNCP CloseProc,
    int nCmdShow,
    HWND *phNewWindow,
    void *pvCallBackData)
{
    HWND            hWnd;               // Main window handle.
    WData           *pTermData;

    // Allocate control structure for the window
    if ((pTermData = malloc(sizeof(WData))) == NULL)
    {
        return FALSE;
    }

    // Set entire structure to nulls
    memset((TCHAR *) pTermData, '\0', sizeof(WData));

    // Initialize function pointers
    pTermData->pMenuProc = MenuProc;
    pTermData->pCharProc = CharProc;
    pTermData->pCloseProc = CloseProc;

    // Initialize callback data
    pTermData->pvCallBackData = pvCallBackData;

    // Create a main window for this application instance.
    hWnd = CreateWindow(
        lpClassName,
        lpWindowName,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        hMenu,
        hInst,
	(LPTSTR) pTermData
    );

    // If window could not be created, return "failure"

    if (!hWnd)
    {
        free(pTermData);
        return FALSE;
    }

    SetFocus(hWnd);

    // Make the window visible; update its client area; and return "success"

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    *phNewWindow = hWnd;
    return (TRUE);
}