#include <PianoControl.hpp>

#include <windowsx.h>
#include <stdio.h>

#define MessageErrorWndTitle(hwnd, e, t) MessageBox(hwnd, e, t, MB_ICONERROR)
#define MessageErrorWnd(hwnd, e) MessageBox(hwnd, e, T("Error!"), MB_ICONERROR)
#define MessageError(e) MessageErrorWnd(NULL, e)

#define MessageLastErrorWndTitle(hwnd, e, title)\
    do { \
        LPTSTR s; \
        if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, e, 0, (LPTSTR)&s, 0, NULL) == 0) {\
            TCHAR str[25]; \
            snprintf(str, 25, T("%x"), str); \
            MessageErrorWndTitle(hwnd, str, title); \
        } else { \
            MessageErrorWndTitle(hwnd, s, title); \
            LocalFree(s); \
        } \
    } while (0)
#define MessageLastErrorWnd(hwnd, e) MessageLastErrorWndTitle(hwnd, e, T("Error!"))
#define MessageLastError(e) MessageLastErrorWnd(NULL, e)

#define MessageIntBox(hwnd, i, title, opt) \
    do { \
        CHAR buf[100]; \
        sprintf_s(buf, 100, "%d", i); \
        MessageBoxA(hwnd, buf, title, opt); \
    } while (0)

#define MessageIntWndTitle(hwnd, i, title) MessageIntBox(hwnd, i, title, MB_ICONINFORMATION)
#define MessageIntWnd(hwnd, i) MessageIntWndTitle(hwnd, i, "Debug Info")
#define MessageInt(i) MessageIntWnd(NULL, i)

BOOL PianoControl::WinRegisterClass(WNDCLASS *pwc)
{
    return __super::WinRegisterClass(pwc);
}

LRESULT PianoControl::OnCreate()
{
    NONCLIENTMETRICS ncmMetrics = { sizeof(NONCLIENTMETRICS) };
    RECT client;

    SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &ncmMetrics, 0);
    GetClientRect(m_hwnd, &client);

    hFont = CreateFontIndirect(&ncmMetrics.lfMessageFont);
    hwParent = GetParent(m_hwnd);
    
    hMemDC = NULL;
    hMemBitmap = NULL;
    bmx = bmy = 0;
    
    blackStatus = whiteStatus = NULL;
    blackText = whiteText = NULL;
    
    SetOctaves(2);
    return 0;
}

LRESULT PianoControl::OnDestroy()
{
    if (hMemDC)
        DeleteDC(hMemDC);
    return 0;
}

void PianoControl::SetOctaves(int octaves)
{
    bool *newBlackStatus, *newWhiteStatus;
    LPCWSTR *newBlackText, *newWhiteText;
    
    #define RENEW(type, newname, store) {\
        newname = new type[7 * octaves];\
        if (store) {\
            memcpy(newname, store, min(this->octaves * 7, 7 * octaves) * sizeof(type));\
            delete store;\
        } else \
            memset(newname, 0, 7 * octaves * sizeof(type));\
        store = newname;\
    }
    RENEW(bool, newBlackStatus, blackStatus);
    RENEW(bool, newWhiteStatus, whiteStatus);
    RENEW(LPCWSTR, newBlackText, blackText);
    RENEW(LPCWSTR, newWhiteText, whiteText);
    
    this->octaves = octaves;
}

void PianoControl::UpdateKey(int key, bool black)
{
    RECT client;
    int width, height;
    int wwidth, bwidth, bheight, hbwidth;

    GetClientRect(m_hwnd, &client);
    width = client.right - client.left;
    height = client.bottom - client.top;
    wwidth = width / 7 / octaves; // Displaying 14 buttons.
    bwidth = width / 12 / octaves; // smaller
    bheight = height / 2;
    hbwidth = bwidth / 2;
    
    if (black) {
        client.left += (key * wwidth) - hbwidth + 2;
        client.right = client.left + bwidth - 5;
        client.bottom = client.top + bheight;
        InvalidateRect(m_hwnd, &client, FALSE);
    } else {
        client.left += key * wwidth;
        client.right = client.left  + wwidth;
        client.bottom = client.top + height;
        InvalidateRect(m_hwnd, &client, FALSE);
    }
}

void PianoControl::SetKeyStatus(int key, bool down)
{
    bool black;
    int id = keyIDToInternal(key, black);
    
    (black ? blackStatus : whiteStatus)[id] = down;
    UpdateKey(id, black);
}

bool PianoControl::GetKeyStatus(int key)
{
    bool black;
    int id = keyIDToInternal(key, black);
    
    return (black ? blackStatus : whiteStatus)[id];
}

void PianoControl::SetKeyText(int key, LPCWSTR text)
{
    bool black;
    int id = keyIDToInternal(key, black);
    
    (black ? blackText : whiteText)[id] = text;
    UpdateKey(id, black);
}

LPCWSTR PianoControl::GetKeyText(int key)
{
    bool black;
    int id = keyIDToInternal(key, black);
    
    return (black ? blackText : whiteText)[id];
}

int PianoControl::keyIDToInternal(int id, bool &black) {
    switch (id % 12) {
        case 0:
        case 2:
        case 4:
        case 7:
        case 9:
            black = true;
            break;
        default:
            black = false;
    }
    
    int ret = 0;
    switch (id % 12) {
        case 0:
        case 1:
            ret = 0;
            break;
        case 2:
        case 3:
            ret = 1;
            break;
        case 4:
        case 5:
            ret = 2;
            break;
        case 6:
            ret = 3;
            break;
        case 7:
        case 8:
            ret = 4;
            break;
        case 9:
        case 10:
            ret = 5;
            break;
        case 11:
            ret = 6;
            break;
    }
    
    return id / 12 * 7 + ret;
}

bool PianoControl::haveBlackToLeft(int i) {
    switch (i % 7) {
        case 0: // G
        case 1: // A
        case 2: // B
            return true;
        case 3: // C
            return false;
        case 4: // D
        case 5: // E
            return true;
        case 6: // F
            return false;
    }
    return false; // not reached
}

bool PianoControl::haveBlackToRight(int i) {
    switch (i % 7) {
        case 0: // G
        case 1: // A
            return true;
        case 2: // B
            return false;
        case 3: // C
        case 4: // D
            return true;
        case 5: // E
            return false;
        case 6: // F
            return true;
    }
    return false; // not reached
}

void PianoControl::PaintContent(PAINTSTRUCT *pps)
{
    RECT client, rect;
    int width, height;
    int wwidth, bwidth, bheight, hbwidth;
    HDC hdc = pps->hdc;
    HBRUSH hbFace   = GetSysColorBrush(COLOR_3DFACE),
           hbDC     = GetStockBrush(DC_BRUSH),
           hbOriginal;
    HPEN hPenOriginal, hPenDC = GetStockPen(DC_PEN);
    HFONT hFontOriginal = SelectFont(hdc, hFont), hFontNew;
    LPWSTR szBuffer = NULL;
    int bufsize = 0;
    COLORREF textColourOriginal = GetTextColor(hdc),
             backgroundOriginal = SetBkMode(hdc, TRANSPARENT);
    LOGFONT lf;
    GetClientRect(m_hwnd, &client);
    width = client.right - client.left;
    height = client.bottom - client.top;
    wwidth = width / 7 / octaves; // Displaying 14 buttons.
    bwidth = width / 12 / octaves; // smaller
    bheight = height / 2;
    bheight = height / 2;
    hbwidth = bwidth / 2;
    
    hbOriginal = SelectBrush(hdc, hBackground);
    hPenOriginal = SelectPen(hdc, hPenDC);
    
    GetObject(hFont, sizeof(LOGFONT), &lf);
    lf.lfWidth = 0;
    lf.lfHeight = min(bwidth, bheight / 4);
    hFontNew = CreateFontIndirect(&lf);
    SelectFont(hdc, hFontNew);

    #define MoveTo(hdc, x, y) MoveToEx(hdc, x, y, NULL)
    #define CURVE_SIZE 5
    #define CURVE_CIRCLE (2*CURVE_SIZE)
    #define DRAWLINE(x1, y1, x2, y2) (\
        MoveTo(hdc, x1, y1),\
        LineTo(hdc, x2, y2)\
    )
    #define DRAWVERTICAL(length, x, y, color) (\
        SetDCPenColor(hdc, color),\
        DRAWLINE(x, y, x, y + length)\
    )
    #define DRAWHORIZON(length, x, y, color) (\
        SetDCPenColor(hdc, color),\
        DRAWLINE(x, y, x + length, y)\
    )
    #define DRAWBORDER(length, dx, color) (\
        SetDCPenColor(hdc, color),\
        MoveTo(hdc, sx + dx, 0),\
        LineTo(hdc, sx + dx, length),\
        MoveTo(hdc, ex - dx - 1, 0),\
        LineTo(hdc, ex - dx - 1, length)\
    )
    #define DRAWBOX(start, d, height, color) (\
        SetDCPenColor(hdc, color),\
        RoundRect(hdc, sx + d, start - CURVE_SIZE, ex - d, height - d, CURVE_CIRCLE, CURVE_CIRCLE)\
    )
    #define INITIALIZE_PAINT_TEXT(store) \
        int len = lstrlen(store[i]), bufneed = len * 3 + 6; \
        int bufidx = 0; \
        if (bufsize < bufneed) { \
            if (szBuffer) \
                delete szBuffer; \
            szBuffer = new WCHAR[bufneed]; \
        } \
        for (LPCWSTR c = store[i]; *c; c++) { \
            szBuffer[bufidx++] = *c; \
            szBuffer[bufidx++] = L'\r'; \
            szBuffer[bufidx++] = L'\n'; \
        } \
        szBuffer[bufidx] = 0;
    #define GETBORDER0(down) (down ? GetSysColor(COLOR_3DLIGHT) : RGB(0, 0, 0))
    #define GETBORDER1(down) (down ? GetSysColor(COLOR_3DSHADOW) : GetSysColor(COLOR_3DDKSHADOW))
    #define GETBORDER2(down) (down ? GetSysColor(COLOR_3DDKSHADOW) : GetSysColor(COLOR_3DSHADOW))
    
    rect.top = height - CURVE_SIZE, rect.bottom = height;
    rect.left = client.left, rect.right = client.right;
    FillRect(hdc, &rect, hBackground);
    
    rect.top = client.top, rect.bottom = client.bottom;
    rect.left = client.right - width % (7 * octaves), rect.right = client.right;
    FillRect(hdc, &rect, hBackground);
    for (int i = 0; i < 7 * octaves; ++i) {
        int sx = i * wwidth, ex = i * wwidth + wwidth - 1;
        bool down = whiteStatus[i];
        
        SelectBrush(hdc, hbDC);
        SetDCBrushColor(hdc, GETBORDER1(down));
        DRAWBOX(bheight, 0, height, GETBORDER0(down));
        SetDCBrushColor(hdc, GETBORDER2(down));
        DRAWBOX(bheight, 1, height, GETBORDER1(down));
        SelectBrush(hdc, hbFace);
        DRAWBOX(bheight, 2, height, GETBORDER2(down));
        
        rect.top = 0, rect.bottom = bheight, rect.left = sx, rect.right = ex;
        FillRect(hdc, &rect, hBackground);
        
        switch (haveBlack(i)) {
            case 0: // none
                DRAWBORDER(bheight, 0, GETBORDER0(down));
                DRAWBORDER(bheight, 1, GETBORDER1(down));
                DRAWBORDER(bheight, 2, GETBORDER2(down));
                break;
            case 1: // right
                DRAWVERTICAL(bheight, sx + 0, 0, GETBORDER0(down));
                DRAWVERTICAL(bheight, sx + 1, 0, GETBORDER1(down));
                DRAWVERTICAL(bheight, sx + 2, 0, GETBORDER2(down));
                DRAWVERTICAL(bheight + 0, ex - hbwidth - 0, 0, GETBORDER0(down));
                DRAWVERTICAL(bheight + 1, ex - hbwidth - 1, 0, GETBORDER1(down));
                DRAWVERTICAL(bheight + 2, ex - hbwidth - 2, 0, GETBORDER2(down));
                DRAWHORIZON(hbwidth - 1, ex - hbwidth - 0, bheight + 0, GETBORDER0(down));
                DRAWHORIZON(hbwidth - 1, ex - hbwidth - 1, bheight + 1, GETBORDER1(down));
                DRAWHORIZON(hbwidth - 1, ex - hbwidth - 2, bheight + 2, GETBORDER2(down));
                rect.top = 0, rect.bottom = bheight, rect.left = sx + 3, rect.right = ex - hbwidth - 2;
                FillRect(hdc, &rect, hbFace);
                break;
            case 2: // left
                DRAWVERTICAL(bheight + 0, sx + hbwidth + 0, 0, GETBORDER0(down));
                DRAWVERTICAL(bheight + 1, sx + hbwidth + 1, 0, GETBORDER1(down));
                DRAWVERTICAL(bheight + 2, sx + hbwidth + 2, 0, GETBORDER2(down));
                DRAWVERTICAL(bheight, ex - 1, 0, GETBORDER0(down));
                DRAWVERTICAL(bheight, ex - 2, 0, GETBORDER1(down));
                DRAWVERTICAL(bheight, ex - 3, 0, GETBORDER2(down));
                DRAWHORIZON(hbwidth + 1, sx + 0, bheight + 0, GETBORDER0(down));
                DRAWHORIZON(hbwidth + 1, sx + 1, bheight + 1, GETBORDER1(down));
                DRAWHORIZON(hbwidth + 1, sx + 2, bheight + 2, GETBORDER2(down));
                rect.top = 0, rect.bottom = bheight, rect.left = sx + hbwidth + 3, rect.right = ex - 3;
                FillRect(hdc, &rect, hbFace);
                break;
            case 3: // both
                DRAWVERTICAL(bheight + 0, sx + hbwidth + 0, 0, GETBORDER0(down));
                DRAWVERTICAL(bheight + 1, sx + hbwidth + 1, 0, GETBORDER1(down));
                DRAWVERTICAL(bheight + 2, sx + hbwidth + 2, 0, GETBORDER2(down));
                DRAWVERTICAL(bheight + 0, ex - hbwidth - 0, 0, GETBORDER0(down));
                DRAWVERTICAL(bheight + 1, ex - hbwidth - 1, 0, GETBORDER1(down));
                DRAWVERTICAL(bheight + 2, ex - hbwidth - 2, 0, GETBORDER2(down));
                DRAWHORIZON(hbwidth + 1, sx + 0, bheight + 0, GETBORDER0(down));
                DRAWHORIZON(hbwidth + 1, sx + 1, bheight + 1, GETBORDER1(down));
                DRAWHORIZON(hbwidth + 1, sx + 2, bheight + 2, GETBORDER2(down));
                DRAWHORIZON(hbwidth - 1, ex - hbwidth - 0, bheight + 0, GETBORDER0(down));
                DRAWHORIZON(hbwidth - 1, ex - hbwidth - 1, bheight + 1, GETBORDER1(down));
                DRAWHORIZON(hbwidth - 1, ex - hbwidth - 2, bheight + 2, GETBORDER2(down));
                rect.top = 0, rect.bottom = bheight, rect.left = sx + hbwidth + 3, rect.right = ex - hbwidth - 2;
                FillRect(hdc, &rect, hbFace);
                break;
        }
        
        if (whiteText[i]) {
            INITIALIZE_PAINT_TEXT(whiteText);
            rect.top = bheight + bheight / 7, rect.bottom = height - bheight / 7;
            rect.left = sx, rect.right = ex;
            SetTextColor(hdc, RGB(0, 0, 0));
            DrawText(hdc, szBuffer, -1, &rect, DT_CENTER);
        }
        
        rect.top = client.top, rect.bottom = client.bottom;
        rect.left = ex, rect.right = ex + 1;
        FillRect(hdc, &rect, hBackground);
    }
    for (int i = 0; i < 7 * octaves; ++i) {
        if (!haveBlackToLeft(i))
            continue;
        int sx = (i * wwidth) - hbwidth + 2, ex = sx + bwidth - 5;
        int kj = bwidth / 4, dc = 128 / kj;
        bool down = blackStatus[i];
        SelectBrush(hdc, hbDC);
        for (int j = 0; j < kj; ++j) {
            int gray = down ? j * dc : (128 - j * dc);
            COLORREF colour = RGB(gray, gray, gray);
            SetDCBrushColor(hdc, colour);
            DRAWBOX(-CURVE_SIZE, j, bheight - 2, colour);
        }
        
        if (blackText[i]) {
            INITIALIZE_PAINT_TEXT(blackText);
            rect.top = bheight / 7, rect.bottom = bheight - bheight / 7;
            rect.left = max(0, sx), rect.right = ex;
            SetTextColor(hdc, RGB(255, 255, 255));
            DrawText(hdc, szBuffer, -1, &rect, DT_CENTER);
        }
    }
    #undef MoveTo
    #undef DRAWLINE
    #undef DRAWVERTICAL
    #undef DRAWHORIZON
    #undef DRAWBORDER
    #undef DRAWBOX
    #undef GETBORDER1
    #undef GETBORDER2
    SelectBrush(hdc, hbOriginal);
    SelectPen(hdc, hPenOriginal);
    DeleteObject(hFontNew);
    SelectFont(hdc, hFontOriginal);
    SetTextColor(hdc, textColourOriginal);
    SetBkMode(hdc, backgroundOriginal);
    if (szBuffer)
        delete szBuffer;
}

void PianoControl::OnPaint()
{
    PAINTSTRUCT ps;
    BeginPaint(m_hwnd, &ps);
    
    int x = ps.rcPaint.left;
    int y = ps.rcPaint.top;
    int cx = ps.rcPaint.right - ps.rcPaint.left;
    int cy = ps.rcPaint.bottom - ps.rcPaint.top;
    HDC hdc = ps.hdc;
    
    if (!hMemDC)
        hMemDC = CreateCompatibleDC(hdc);
    if (!hMemBitmap)
        hMemBitmap = CreateCompatibleBitmap(hdc, cx + 50, cy + 50);
    if (cx > bmx || cy > bmy) {
        if (hMemBitmap)
            DeleteObject(hMemBitmap);
        hMemBitmap = CreateCompatibleBitmap(hdc, cx + 50, cy + 50);
    }
    if (hMemDC && hMemBitmap) {
        ps.hdc = hMemDC;
        
        HBITMAP hbmPrev = SelectBitmap(hMemDC, hMemBitmap);
        SetWindowOrgEx(hMemDC, x, y, NULL);

        PaintContent(&ps);
        BitBlt(hdc, x, y, cx, cy, hMemDC, x, y, SRCCOPY);

        SelectObject(hMemDC, hbmPrev);
    } else
        PaintContent(&ps);
    EndPaint(m_hwnd, &ps);
}

LRESULT PianoControl::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg) {
    case WM_CREATE:
        return OnCreate();
    case WM_DESTROY:
        return OnDestroy();
    case WM_NCDESTROY:
        PostQuitMessage(0);
        break;
    case WM_PAINT:
        OnPaint();
        return 0;
    case WM_SIZE:
        InvalidateRect(m_hwnd, NULL, TRUE);
        return 0;
    case WM_LBUTTONDOWN:
        SetFocus(hwParent);
        return 0;
    case WM_KEYDOWN:
    case WM_SYSKEYDOWN:
    case WM_KEYUP:
    case WM_SYSKEYUP:
    case WM_CHAR:
    case WM_DEADCHAR:
    case WM_SYSCHAR:
    case WM_SYSDEADCHAR:
        return SendMessage(hwParent, uMsg, wParam, lParam);
    case WM_GETFONT:
        return (LRESULT) GetFont();
    case WM_SETFONT:
        SetFont((HFONT) wParam);
        if (LOWORD(lParam))
            InvalidateRect(m_hwnd, NULL, TRUE);
    case MPCM_GETKEYSTATUS:
        return GetKeyStatus(wParam);
    case MPCM_SETKEYSTATUS:
        SetKeyStatus(wParam, lParam != 0);
        return 0;
    case MPCM_GETOCTAVES:
        return GetOctaves();
    case MPCM_SETOCTAVES:
        SetOctaves(wParam);
        return 0;
    case MPCM_GETKEYTEXT:
        return GetOctaves();
    case MPCM_SETKEYTEXT:
        SetOctaves(wParam);
        return 0;
    case MPCM_GETBACKGROUND:
        return (LRESULT) GetBackground();
    case MPCM_SETBACKGROUND:
        SetBackground((HBRUSH) wParam);
        return 0;
    }
    return __super::HandleMessage(uMsg, wParam, lParam);
}

PianoControl *PianoControl::Create(LPCTSTR szTitle, HWND hwParent,
                                   DWORD dwStyle, int x, int y, int cx, int cy)
{
    PianoControl *self = new PianoControl();
    if (self &&
        self->WinCreateWindow(0, szTitle, dwStyle, x, y, cx, cy,
                              hwParent, NULL)) {
        return self;
    }
    delete self;
    return NULL;
}