/*
* PROJECT:     ReactOS Character Map
* LICENSE:     GPL - See COPYING in the top level directory
* FILE:        base/applications/charmap/GridView.cpp
* PURPOSE:     Class for for the window which contains the font matrix
* COPYRIGHT:   Copyright 2015 Ged Murphy <gedmurphy@reactos.org>
*/


#include "precomp.h"
#include "GridView.h"
#include "Cell.h"


/* DATA *****************************************************/

extern HINSTANCE g_hInstance;


/* PUBLIC METHODS **********************************************/

CGridView::CGridView() :
    m_xNumCells(20),
    m_yNumCells(10),
    m_ScrollPosition(0),
    m_NumRows(0)
{
    m_szMapWndClass = L"CharGridWClass";
}

CGridView::~CGridView()
{
}

bool
CGridView::Create(
    _In_ HWND hParent
    )
{
    WNDCLASSW wc = { 0 };
    wc.style = CS_DBLCLKS;
    wc.lpfnWndProc = MapWndProc;
    wc.cbWndExtra = sizeof(CGridView *);
    wc.hInstance = g_hInstance;
    wc.hCursor = LoadCursorW(NULL, (LPWSTR)IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszClassName = m_szMapWndClass;

    if (RegisterClassW(&wc))
    {
        m_hwnd = CreateWindowExW(0,
                                 m_szMapWndClass,
                                 NULL,
                                 WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL,
                                 0,0,0,0,
                                 hParent,
                                 NULL,
                                 g_hInstance,
                                 this);

    }

    return !!(m_hwnd != NULL);
}

bool
CGridView::SetFont(
    _In_ CAtlString& FontName
    )
{

    // Create a temporary container for the new font
    CurrentFont NewFont = { 0 };
    NewFont.FontName = FontName;

    // Get the DC for the full grid window
    HDC hdc;
    hdc = GetDC(m_hwnd);
    if (hdc == NULL) return false;

    // Setup the logfont structure
    NewFont.Font.lfHeight = 0; // This is set in WM_SIZE
    NewFont.Font.lfCharSet = DEFAULT_CHARSET;
    StringCchCopyW(NewFont.Font.lfFaceName, LF_FACESIZE, FontName);

    // Get a handle to the new font
    NewFont.hFont = CreateFontIndirectW(&NewFont.Font);
    if (NewFont.hFont == NULL)
    {
        ReleaseDC(m_hwnd, hdc);
        return false;
    }

    // Setup an array of all possible non-BMP indices
    WCHAR ch[MAX_GLYPHS];
    for (int i = 0; i < MAX_GLYPHS; i++)
        ch[i] = (WCHAR)i;

    HFONT hOldFont;
    hOldFont = (HFONT)SelectObject(hdc, NewFont.hFont);

    // Translate all the indices into glyphs
    WORD out[MAX_GLYPHS];
    DWORD Status;
    Status = GetGlyphIndicesW(hdc,
                              ch,
                              MAX_GLYPHS,
                              out,
                              GGI_MARK_NONEXISTING_GLYPHS);
    ReleaseDC(m_hwnd, hdc);
    if (Status == GDI_ERROR)
    {
        SelectObject(hdc, hOldFont);
        return false;
    }

    // Loop all the glyphs looking for valid ones
    // and store those in our font data
    int j = 0;
    for (int i = 0; i < MAX_GLYPHS; i++)
    {
        if (out[i] != 0xffff)
        {
            NewFont.ValidGlyphs[j] = ch[i];
            j++;
        }
    }
    NewFont.NumValidGlyphs = j;

    // Calculate the number of rows required to hold all glyphs
    m_NumRows = NewFont.NumValidGlyphs / m_xNumCells;
    if (NewFont.NumValidGlyphs % m_xNumCells)
        m_NumRows += 1;

    // Set the scrollbar in relation to the rows
    SetScrollRange(m_hwnd, SB_VERT, 0, m_NumRows - m_yNumCells, FALSE);

    // We're done, update the current font
    m_CurrentFont = NewFont;

    // We changed the font, we'll need to repaint the whole window
    InvalidateRect(m_hwnd,
                   NULL,
                   TRUE);

    return true;
}



/* PRIVATE METHODS **********************************************/

bool
CGridView::UpdateCellCoordinates(
    )
{
    // Go through all the cells and calculate
    // their coordinates within the grid
    for (int y = 0; y < m_yNumCells; y++)
    for (int x = 0; x < m_xNumCells; x++)
    {
        RECT CellCoordinates;
        CellCoordinates.left = x * m_CellSize.cx;
        CellCoordinates.top = y * m_CellSize.cy;
        CellCoordinates.right = (x + 1) * m_CellSize.cx + 1;
        CellCoordinates.bottom = (y + 1) * m_CellSize.cy + 1;

        m_Cells[y][x]->SetCellCoordinates(CellCoordinates);
    }

    return true;
}

LRESULT
CGridView::OnCreate(
    _In_ HWND hwnd,
    _In_ HWND hParent
    )
{
    m_hwnd = hwnd;
    m_hParent = hParent;

    // C++ doesn't allow : "CCells ***C = new CCell***[x * y]"
    // so we have to build the 2d array up manually
    m_Cells = new CCell**[m_yNumCells]; // rows
    for (int i = 0; i < m_yNumCells; i++)
        m_Cells[i] = new CCell*[m_xNumCells]; // columns

    for (int y = 0; y < m_yNumCells; y++)
    for (int x = 0; x < m_xNumCells; x++)
    {
        m_Cells[y][x] = new CCell(m_hwnd);
    }

    // Give the first cell focus
    SetCellFocus(m_Cells[0][0]);

    return 0;
}

LRESULT
CGridView::OnSize(
    _In_ INT Width,
    _In_ INT Height
    )
{
    // Get the client area of the main dialog
    RECT ParentRect;
    GetClientRect(m_hParent, &ParentRect);

    // Calculate the grid size using the parent
    m_ClientCoordinates.left = ParentRect.left + 25;
    m_ClientCoordinates.top = ParentRect.top + 50;
    m_ClientCoordinates.right = ParentRect.right - m_ClientCoordinates.left - 10;
    m_ClientCoordinates.bottom = ParentRect.bottom - m_ClientCoordinates.top - 70;

    // Resize the grid window
    SetWindowPos(m_hwnd,
                 NULL,
                 m_ClientCoordinates.left,
                 m_ClientCoordinates.top,
                 m_ClientCoordinates.right,
                 m_ClientCoordinates.bottom,
                 SWP_NOZORDER | SWP_SHOWWINDOW);

    // Get the client area we can draw on. The position we set above includes
    // a scrollbar which we obviously can't draw on. GetClientRect gives us
    // the size without the scroll, and it's more efficient than getting the
    // scroll metrics and calculating the size from that
    RECT ClientRect;
    GetClientRect(m_hwnd, &ClientRect);
    m_CellSize.cx = ClientRect.right / m_xNumCells;
    m_CellSize.cy = ClientRect.bottom / m_yNumCells;

    // Let all the cells know about their new coords
    UpdateCellCoordinates();

    // We scale the font size up or down depending on the cell size
    if (m_CurrentFont.hFont)
    {
        // Delete the existing font
        DeleteObject(m_CurrentFont.hFont);

        HDC hdc;
        hdc = GetDC(m_hwnd);
        if (hdc)
        {
            // Update the font size with respect to the cell size
            m_CurrentFont.Font.lfHeight = (m_CellSize.cy - 5);
            m_CurrentFont.hFont = CreateFontIndirectW(&m_CurrentFont.Font);
            ReleaseDC(m_hwnd, hdc);
        }
    }

    // Redraw the whole grid
    InvalidateRect(m_hwnd, &ClientRect, TRUE);

    return 0;
}

VOID
CGridView::OnVScroll(_In_ INT Value,
                     _In_ INT Pos)
{
    INT PrevScrollPosition = m_ScrollPosition;

    switch (Value)
    {
    case SB_LINEUP:
        m_ScrollPosition -= 1;
        break;

    case SB_LINEDOWN:
        m_ScrollPosition += 1;
        break;

    case SB_PAGEUP:
        m_ScrollPosition -= m_yNumCells;
        break;

    case SB_PAGEDOWN:
        m_ScrollPosition += m_yNumCells;
        break;

    case SB_THUMBTRACK:
        m_ScrollPosition = Pos;
        break;

    default:
        break;
    }

    // Make sure we don't scroll past row 0 or max rows
    m_ScrollPosition = max(0, m_ScrollPosition);
    m_ScrollPosition = min(m_ScrollPosition, m_NumRows);

    // Check if there's a difference from the previous position
    INT ScrollDiff;
    ScrollDiff = PrevScrollPosition - m_ScrollPosition;
    if (ScrollDiff)
    {
        // Set the new scrollbar position in the scroll box
        SetScrollPos(m_hwnd,
                     SB_VERT,
                     m_ScrollPosition,
                     TRUE);

        // Check if the scrollbar has moved more than the
        // number of visible rows (draged or paged)
        if (abs(ScrollDiff) < m_yNumCells)
        {
            RECT rect;
            GetClientRect(m_hwnd, &rect);

            // Scroll the visible cells which remain within the grid
            // and invalidate any new ones which appear from the top / bottom
            ScrollWindowEx(m_hwnd,
                           0,
                           ScrollDiff * m_CellSize.cy,
                           &rect,
                           &rect,
                           NULL,
                           NULL,
                           SW_INVALIDATE);
        }
        else
        {
            // All the cells need to be redrawn
            InvalidateRect(m_hwnd,
                           NULL,
                           TRUE);
        }
    }
}

LRESULT
CGridView::OnPaint(
    _In_opt_ HDC hdc
    )
{
    PAINTSTRUCT PaintStruct = { 0 };
    HDC LocalHdc = NULL;
    BOOL bSuccess = FALSE;

    // Check if we were passed a DC
    if (hdc == NULL)
    {
        // We weren't, let's get one
        LocalHdc = BeginPaint(m_hwnd, &PaintStruct);
        if (LocalHdc) bSuccess = TRUE;
    }
    else
    {
        // Use the existing DC and just get the region to paint
        bSuccess = GetUpdateRect(m_hwnd,
                                 &PaintStruct.rcPaint,
                                 TRUE);
        if (bSuccess)
        {
            // Update the struct with the DC we were passed
            PaintStruct.hdc = (HDC)hdc;
        }
    }

    // Make sure we have a valid DC
    if (bSuccess)
    {
        // Paint the grid and chars
        DrawGrid(&PaintStruct);

        if (LocalHdc)
        {
            EndPaint(m_hwnd, &PaintStruct);
        }
    }

    return 0;
}

LRESULT
CALLBACK
CGridView::MapWndProc(
    HWND hwnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam
    )
{
    CGridView *This;
    LRESULT RetCode = 0;

    // Get the object pointer from window context
    This = (CGridView *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    if (This == NULL)
    {
        // Check that this isn't a create message
        if (uMsg != WM_CREATE)
        {
            // Don't handle null info pointer
            goto HandleDefaultMessage;
        }
    }

    switch (uMsg)
    {
    case WM_CREATE:
    {
        // Get the object pointer from the create param
        This = (CGridView *)((LPCREATESTRUCT)lParam)->lpCreateParams;

        // Store the pointer in the window's global user data
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)This);

        This->OnCreate(hwnd, ((LPCREATESTRUCTW)lParam)->hwndParent);
        break;
    }

    case WM_SIZE:
    {
        INT Width, Height;
        Width = LOWORD(lParam);
        Height = HIWORD(lParam);

        This->OnSize(Width, Height);
        break;
    }

    case WM_VSCROLL:
    {
        INT Value, Pos;
        Value = LOWORD(wParam);
        Pos = HIWORD(wParam);

        This->OnVScroll(Value, Pos);
        break;
    }

    case WM_PAINT:
    {
        This->OnPaint((HDC)wParam);
        break;
    }

    case WM_DESTROY:
    {
        This->DeleteCells();
        break;
    }

    default:
    {
HandleDefaultMessage:
        RetCode = DefWindowProcW(hwnd, uMsg, wParam, lParam);
        break;
    }
    }

    return RetCode;
}


void
CGridView::DrawGrid(
    _In_ LPPAINTSTRUCT PaintStruct
    )
{
    // Calculate which glyph to start at based on scroll position
    int i;
    i = m_xNumCells * m_ScrollPosition;

    // Make sure we have the correct font on the DC
    HFONT hOldFont;
    hOldFont = (HFONT)SelectFont(PaintStruct->hdc,
                                 m_CurrentFont.hFont);

    // Traverse all the cells
    for (int y = 0; y < m_yNumCells; y++)
    for (int x = 0; x < m_xNumCells; x++)
    {
        // Update the glyph for this cell
        WCHAR ch = (WCHAR)m_CurrentFont.ValidGlyphs[i];
        m_Cells[y][x]->SetChar(ch);

        // Tell it to paint itself
        m_Cells[y][x]->OnPaint(*PaintStruct);
        i++;
    }

    SelectObject(PaintStruct->hdc, hOldFont);

}

void
CGridView::DeleteCells()
{
    if (m_Cells == nullptr)
        return;

    // Free cells withing the 2d array
    for (int i = 0; i < m_yNumCells; i++)
        delete[] m_Cells[i];
    delete[] m_Cells;

    m_Cells = nullptr;
}

void
CGridView::SetCellFocus(
    _In_ CCell* NewActiveCell
    )
{
    if (m_ActiveCell)
    {
        // Remove focus from any existing cell
        m_ActiveCell->SetFocus(false);
        InvalidateRect(m_hwnd, m_ActiveCell->GetCellCoordinates(), TRUE);
    }

    // Set the new active cell and give it focus
    m_ActiveCell = NewActiveCell;
    m_ActiveCell->SetFocus(true);
}