/*
-------------------------------------------------------------------------
 CxxTest: A lightweight C++ unit testing library.
 Copyright (c) 2008 Sandia Corporation.
 This software is distributed under the LGPL License v3
 For more information, see the COPYING file in the top CxxTest directory.
 Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
 the U.S. Government retains certain rights in this software.
-------------------------------------------------------------------------
*/

#ifndef __cxxtest__Win32Gui_h__
#define __cxxtest__Win32Gui_h__

//
// The Win32Gui displays a simple progress bar using the Win32 API.
//
// It accepts the following command line options:
//   -minimized    Start minimized, pop up on error
//   -keep         Don't close the window at the end
//   -title TITLE  Set the window caption
//
// If both -minimized and -keep are specified, GUI will only keep the
// window if it's in focus.
//
// N.B. If you're wondering why this class doesn't use any standard
// library or STL (<string> would have been nice) it's because it only
// uses "straight" Win32 API.
//

#include <cxxtest/Gui.h>

#include <windows.h>
#include <commctrl.h>

namespace CxxTest
{
class Win32Gui : public GuiListener
{
public:
    void enterGui(int &argc, char **argv)
    {
        parseCommandLine(argc, argv);
    }

    void enterWorld(const WorldDescription &wd)
    {
        getTotalTests(wd);
        _testsDone = 0;
        startGuiThread();
    }

    void guiEnterSuite(const char *suiteName)
    {
        showSuiteName(suiteName);
        reset(_suiteStart);
    }

    void guiEnterTest(const char *suiteName, const char *testName)
    {
        ++ _testsDone;
        setTestCaption(suiteName, testName);
        showTestName(testName);
        showTestsDone();
        progressBarMessage(PBM_STEPIT);
        reset(_testStart);
    }

    void yellowBar()
    {
        setColor(255, 255, 0);
        setIcon(IDI_WARNING);
        getTotalTests();
    }

    void redBar()
    {
        if (_startMinimized)
        {
            showMainWindow(SW_SHOWNORMAL);
        }
        setColor(255, 0, 0);
        setIcon(IDI_ERROR);
        getTotalTests();
    }

    void leaveGui()
    {
        if (keep())
        {
            showSummary();
            WaitForSingleObject(_gui, INFINITE);
        }
        DestroyWindow(_mainWindow);
    }

private:
    const char *_title;
    bool _startMinimized, _keep;
    HANDLE _gui;
    WNDCLASSEX _windowClass;
    HWND _mainWindow, _progressBar, _statusBar;
    HANDLE _canStartTests;
    unsigned _numTotalTests, _testsDone;
    char _strTotalTests[WorldDescription::MAX_STRLEN_TOTAL_TESTS];
    enum
    {
        STATUS_SUITE_NAME, STATUS_SUITE_TIME,
        STATUS_TEST_NAME, STATUS_TEST_TIME,
        STATUS_TESTS_DONE, STATUS_WORLD_TIME,
        STATUS_TOTAL_PARTS
    };
    int _statusWidths[STATUS_TOTAL_PARTS];
    unsigned _statusOffsets[STATUS_TOTAL_PARTS];
    unsigned _statusTotal;
    char _statusTestsDone[sizeof("1000000000 of  (100%)") + WorldDescription::MAX_STRLEN_TOTAL_TESTS];
    DWORD _worldStart, _suiteStart, _testStart;
    char _timeString[sizeof("00:00:00")];

    void parseCommandLine(int argc, char **argv)
    {
        _startMinimized = _keep = false;
        _title = argv[0];

        for (int i = 1; i < argc; ++ i)
        {
            if (!lstrcmpA(argv[i], "-minimized"))
            {
                _startMinimized = true;
            }
            else if (!lstrcmpA(argv[i], "-keep"))
            {
                _keep = true;
            }
            else if (!lstrcmpA(argv[i], "-title") && (i + 1 < argc))
            {
                _title = argv[++i];
            }
        }
    }

    void getTotalTests()
    {
        getTotalTests(tracker().world());
    }

    void getTotalTests(const WorldDescription &wd)
    {
        _numTotalTests = wd.numTotalTests();
        wd.strTotalTests(_strTotalTests);
    }

    void startGuiThread()
    {
        _canStartTests = CreateEvent(NULL, TRUE, FALSE, NULL);
        DWORD threadId;
        _gui = CreateThread(NULL, 0, &(Win32Gui::guiThread), (LPVOID)this, 0, &threadId);
        WaitForSingleObject(_canStartTests, INFINITE);
    }

    static DWORD WINAPI guiThread(LPVOID parameter)
    {
        ((Win32Gui *)parameter)->gui();
        return 0;
    }

    void gui()
    {
        registerWindowClass();
        createMainWindow();
        initCommonControls();
        createProgressBar();
        createStatusBar();
        centerMainWindow();
        showMainWindow();
        startTimer();
        startTests();

        messageLoop();
    }

    void registerWindowClass()
    {
        _windowClass.cbSize = sizeof(_windowClass);
        _windowClass.style = CS_HREDRAW | CS_VREDRAW;
        _windowClass.lpfnWndProc = &(Win32Gui::windowProcedure);
        _windowClass.cbClsExtra = 0;
        _windowClass.cbWndExtra = sizeof(LONG);
        _windowClass.hInstance = (HINSTANCE)NULL;
        _windowClass.hIcon = (HICON)NULL;
        _windowClass.hCursor = (HCURSOR)NULL;
        _windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        _windowClass.lpszMenuName = NULL;
        _windowClass.lpszClassName = TEXT("CxxTest Window Class");
        _windowClass.hIconSm = (HICON)NULL;

        RegisterClassEx(&_windowClass);
    }

    void createMainWindow()
    {
        _mainWindow = createWindow(_windowClass.lpszClassName, WS_OVERLAPPEDWINDOW);
    }

    void initCommonControls()
    {
        HMODULE dll = LoadLibraryA("comctl32.dll");
        if (!dll) { return; }

        typedef void (WINAPI * FUNC)(void);
        FUNC func = (FUNC)GetProcAddress(dll, "InitCommonControls");
        if (!func) { return; }
        func();
    }

    void createProgressBar()
    {
        _progressBar = createWindow(PROGRESS_CLASS, WS_CHILD | WS_VISIBLE | PBS_SMOOTH, _mainWindow);

#ifdef PBM_SETRANGE32
        progressBarMessage(PBM_SETRANGE32, 0, _numTotalTests);
#else // No PBM_SETRANGE32, use PBM_SETRANGE
        progressBarMessage(PBM_SETRANGE, 0, MAKELPARAM(0, (WORD)_numTotalTests));
#endif // PBM_SETRANGE32
        progressBarMessage(PBM_SETPOS, 0);
        progressBarMessage(PBM_SETSTEP, 1);
        greenBar();
        UpdateWindow(_progressBar);
    }

    void createStatusBar()
    {
        _statusBar = createWindow(STATUSCLASSNAME, WS_CHILD | WS_VISIBLE, _mainWindow);
        setRatios(4, 1, 3, 1, 3, 1);
    }

    void setRatios(unsigned suiteNameRatio, unsigned suiteTimeRatio,
                   unsigned testNameRatio, unsigned testTimeRatio,
                   unsigned testsDoneRatio, unsigned worldTimeRatio)
    {
        _statusTotal = 0;
        _statusOffsets[STATUS_SUITE_NAME] = (_statusTotal += suiteNameRatio);
        _statusOffsets[STATUS_SUITE_TIME] = (_statusTotal += suiteTimeRatio);
        _statusOffsets[STATUS_TEST_NAME] = (_statusTotal += testNameRatio);
        _statusOffsets[STATUS_TEST_TIME] = (_statusTotal += testTimeRatio);
        _statusOffsets[STATUS_TESTS_DONE] = (_statusTotal += testsDoneRatio);
        _statusOffsets[STATUS_WORLD_TIME] = (_statusTotal += worldTimeRatio);
    }

    HWND createWindow(LPCTSTR className, DWORD style, HWND parent = (HWND)NULL)
    {
        return CreateWindow(className, NULL, style, 0, 0, 0, 0, parent,
                            (HMENU)NULL, (HINSTANCE)NULL, (LPVOID)this);
    }

    void progressBarMessage(UINT message, WPARAM wParam = 0, LPARAM lParam = 0)
    {
        SendMessage(_progressBar, message, wParam, lParam);
    }

    void centerMainWindow()
    {
        RECT screen;
        getScreenArea(screen);

        LONG screenWidth = screen.right - screen.left;
        LONG screenHeight = screen.bottom - screen.top;

        LONG xCenter = (screen.right + screen.left) / 2;
        LONG yCenter = (screen.bottom + screen.top) / 2;

        LONG windowWidth = (screenWidth * 4) / 5;
        LONG windowHeight = screenHeight / 10;
        LONG minimumHeight = 2 * (GetSystemMetrics(SM_CYCAPTION) + GetSystemMetrics(SM_CYFRAME));
        if (windowHeight < minimumHeight)
        {
            windowHeight = minimumHeight;
        }

        SetWindowPos(_mainWindow, HWND_TOP,
                     xCenter - (windowWidth / 2), yCenter - (windowHeight / 2),
                     windowWidth, windowHeight, 0);
    }

    void getScreenArea(RECT &area)
    {
        if (!getScreenAreaWithoutTaskbar(area))
        {
            getWholeScreenArea(area);
        }
    }

    bool getScreenAreaWithoutTaskbar(RECT &area)
    {
        return (SystemParametersInfo(SPI_GETWORKAREA, sizeof(RECT), &area, 0) != 0);
    }

    void getWholeScreenArea(RECT &area)
    {
        area.left = area.top = 0;
        area.right = GetSystemMetrics(SM_CXSCREEN);
        area.bottom = GetSystemMetrics(SM_CYSCREEN);
    }

    void showMainWindow()
    {
        showMainWindow(_startMinimized ? SW_MINIMIZE : SW_SHOWNORMAL);
        UpdateWindow(_mainWindow);
    }

    void showMainWindow(int mode)
    {
        ShowWindow(_mainWindow, mode);
    }

    enum { TIMER_ID = 1, TIMER_DELAY = 1000 };

    void startTimer()
    {
        reset(_worldStart);
        reset(_suiteStart);
        reset(_testStart);
        SetTimer(_mainWindow, TIMER_ID, TIMER_DELAY, 0);
    }

    void reset(DWORD &tick)
    {
        tick = GetTickCount();
    }

    void startTests()
    {
        SetEvent(_canStartTests);
    }

    void messageLoop()
    {
        MSG message;
        while (BOOL haveMessage = GetMessage(&message, NULL, 0, 0))
        {
            if (haveMessage != -1)
            {
                DispatchMessage(&message);
            }
        }
    }

    static LRESULT CALLBACK windowProcedure(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
    {
        if (message == WM_CREATE)
        {
            setUp(window, (LPCREATESTRUCT)lParam);
        }

        Win32Gui *that = (Win32Gui *)GetWindowLong(window, GWL_USERDATA);
        return that->handle(window, message, wParam, lParam);
    }

    static void setUp(HWND window, LPCREATESTRUCT create)
    {
        SetWindowLong(window, GWL_USERDATA, (LONG)create->lpCreateParams);
    }

    LRESULT handle(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
    {
        switch (message)
        {
        case WM_SIZE: resizeControls(); break;

        case WM_TIMER: updateTime(); break;

        case WM_CLOSE:
        case WM_DESTROY:
        case WM_QUIT:
            ExitProcess(tracker().failedTests());

        default: return DefWindowProc(window, message, wParam, lParam);
        }
        return 0;
    }

    void resizeControls()
    {
        RECT r;
        GetClientRect(_mainWindow, &r);
        LONG width = r.right - r.left;
        LONG height = r.bottom - r.top;

        GetClientRect(_statusBar, &r);
        LONG statusHeight = r.bottom - r.top;
        LONG resizeGripWidth = statusHeight;
        LONG progressHeight = height - statusHeight;

        SetWindowPos(_progressBar, HWND_TOP, 0, 0, width, progressHeight, 0);
        SetWindowPos(_statusBar, HWND_TOP, 0, progressHeight, width, statusHeight, 0);
        setStatusParts(width - resizeGripWidth);
    }

    void setStatusParts(LONG width)
    {
        for (unsigned i = 0; i < STATUS_TOTAL_PARTS; ++ i)
        {
            _statusWidths[i] = (width * _statusOffsets[i]) / _statusTotal;
        }

        statusBarMessage(SB_SETPARTS, STATUS_TOTAL_PARTS, _statusWidths);
    }

    void statusBarMessage(UINT message, WPARAM wParam = 0, const void *lParam = 0)
    {
        SendMessage(_statusBar, message, wParam, (LPARAM)lParam);
    }

    void greenBar()
    {
        setColor(0, 255, 0);
        setIcon(IDI_INFORMATION);
    }

#ifdef PBM_SETBARCOLOR
    void setColor(BYTE red, BYTE green, BYTE blue)
    {
        progressBarMessage(PBM_SETBARCOLOR, 0, RGB(red, green, blue));
    }
#else // !PBM_SETBARCOLOR
    void setColor(BYTE, BYTE, BYTE)
    {
    }
#endif // PBM_SETBARCOLOR

    void setIcon(LPCTSTR icon)
    {
        SendMessage(_mainWindow, WM_SETICON, ICON_BIG, (LPARAM)loadStandardIcon(icon));
    }

    HICON loadStandardIcon(LPCTSTR icon)
    {
        return LoadIcon((HINSTANCE)NULL, icon);
    }

    void setTestCaption(const char *suiteName, const char *testName)
    {
        setCaption(suiteName, "::", testName, "()");
    }

    void setCaption(const char *a = "", const char *b = "", const char *c = "", const char *d = "")
    {
        unsigned length = lstrlenA(_title) + sizeof(" - ") +
                          lstrlenA(a) + lstrlenA(b) + lstrlenA(c) + lstrlenA(d);
        char *name = allocate(length);
        lstrcpyA(name, _title);
        lstrcatA(name, " - ");
        lstrcatA(name, a);
        lstrcatA(name, b);
        lstrcatA(name, c);
        lstrcatA(name, d);
        SetWindowTextA(_mainWindow, name);
        deallocate(name);
    }

    void showSuiteName(const char *suiteName)
    {
        setStatusPart(STATUS_SUITE_NAME, suiteName);
    }

    void showTestName(const char *testName)
    {
        setStatusPart(STATUS_TEST_NAME, testName);
    }

    void showTestsDone()
    {
        wsprintfA(_statusTestsDone, "%u of %s (%u%%)",
                  _testsDone, _strTotalTests,
                  (_testsDone * 100) / _numTotalTests);
        setStatusPart(STATUS_TESTS_DONE, _statusTestsDone);
    }

    void updateTime()
    {
        setStatusTime(STATUS_WORLD_TIME, _worldStart);
        setStatusTime(STATUS_SUITE_TIME, _suiteStart);
        setStatusTime(STATUS_TEST_TIME, _testStart);
    }

    void setStatusTime(unsigned part, DWORD start)
    {
        unsigned total = (GetTickCount() - start) / 1000;
        unsigned hours = total / 3600;
        unsigned minutes = (total / 60) % 60;
        unsigned seconds = total % 60;

        if (hours)
        {
            wsprintfA(_timeString, "%u:%02u:%02u", hours, minutes, seconds);
        }
        else
        {
            wsprintfA(_timeString, "%02u:%02u", minutes, seconds);
        }

        setStatusPart(part, _timeString);
    }

    bool keep()
    {
        if (!_keep)
        {
            return false;
        }
        if (!_startMinimized)
        {
            return true;
        }
        return (_mainWindow == GetForegroundWindow());
    }

    void showSummary()
    {
        stopTimer();
        setSummaryStatusBar();
        setSummaryCaption();
    }

    void setStatusPart(unsigned part, const char *text)
    {
        statusBarMessage(SB_SETTEXTA, part, text);
    }

    void stopTimer()
    {
        KillTimer(_mainWindow, TIMER_ID);
        setStatusTime(STATUS_WORLD_TIME, _worldStart);
    }

    void setSummaryStatusBar()
    {
        setRatios(0, 0, 0, 0, 1, 1);
        resizeControls();

        const char *tests = (_numTotalTests == 1) ? "test" : "tests";
        if (tracker().failedTests())
        {
            wsprintfA(_statusTestsDone, "Failed %u of %s %s",
                      tracker().failedTests(), _strTotalTests, tests);
        }
        else
        {
            wsprintfA(_statusTestsDone, "%s %s passed", _strTotalTests, tests);
        }

        setStatusPart(STATUS_TESTS_DONE, _statusTestsDone);
    }

    void setSummaryCaption()
    {
        setCaption(_statusTestsDone);
    }

    char *allocate(unsigned length)
    {
        return (char *)HeapAlloc(GetProcessHeap(), 0, length);
    }

    void deallocate(char *data)
    {
        HeapFree(GetProcessHeap(), 0, data);
    }
};
}

#endif // __cxxtest__Win32Gui_h__
