#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( 0 );

            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__
