/*
    Copyright 2005-2014 Intel Corporation.  All Rights Reserved.

    This file is part of Threading Building Blocks. Threading Building Blocks is free software;
    you can redistribute it and/or modify it under the terms of the GNU General Public License
    version 2  as  published  by  the  Free Software Foundation.  Threading Building Blocks is
    distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
    implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    See  the GNU General Public License for more details.   You should have received a copy of
    the  GNU General Public License along with Threading Building Blocks; if not, write to the
    Free Software Foundation, Inc.,  51 Franklin St,  Fifth Floor,  Boston,  MA 02110-1301 USA

    As a special exception,  you may use this file  as part of a free software library without
    restriction.  Specifically,  if other files instantiate templates  or use macros or inline
    functions from this file, or you compile this file and link it with other files to produce
    an executable,  this file does not by itself cause the resulting executable to be covered
    by the GNU General Public License. This exception does not however invalidate any other
    reasons why the executable file might be covered by the GNU General Public License.
*/

/////// Common internal implementation of Windows-specific stuff //////////////
///////                  Must be the first included header       //////////////

#ifndef __WINVIDEO_H__
#define __WINVIDEO_H__

#ifndef _CRT_SECURE_NO_DEPRECATE
#define _CRT_SECURE_NO_DEPRECATE
#endif
// Check that the target Windows version has all API calls requried.
#ifndef _WIN32_WINNT
# define _WIN32_WINNT 0x0400
#endif
#if _WIN32_WINNT<0x0400
# define YIELD_TO_THREAD() Sleep(0)
#else
# define YIELD_TO_THREAD() SwitchToThread()
#endif
#include "video.h"
#include <fcntl.h>
#include <io.h>
#include <iostream>
#include <fstream>

#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "user32.lib")

// maximum mumber of lines the output console should have
static const WORD MAX_CONSOLE_LINES = 500;
const COLORREF              RGBKEY = RGB(8, 8, 16); // at least 8 for 16-bit palette
HWND                        g_hAppWnd;           // The program's window handle
HANDLE                      g_handles[2] = {0,0};// thread and wake up event
unsigned int *              g_pImg = 0;          // drawing memory
int                         g_sizex, g_sizey;
static video *              g_video = 0;
WNDPROC                     g_pUserProc = 0;
HINSTANCE                   video::win_hInstance = 0;
int                         video::win_iCmdShow = 0;
static WNDCLASSEX *         gWndClass = 0;
static HACCEL               hAccelTable = 0;
static DWORD                g_msec = 0;
static int g_fps = 0, g_updates = 0, g_skips = 0;

bool DisplayError(LPSTR lpstrErr, HRESULT hres = 0); // always returns false
LRESULT CALLBACK InternalWndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);

//! Create window
bool WinInit(HINSTANCE hInstance, int nCmdShow, WNDCLASSEX *uwc, const char *title, bool fixedsize)
{
    WNDCLASSEX wndclass;  // Our app's windows class
    if(uwc) {
        memcpy(&wndclass, uwc, sizeof(wndclass));
        g_pUserProc = uwc->lpfnWndProc;
    } else {
        memset(&wndclass, 0, sizeof(wndclass));
        wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
        wndclass.lpszClassName = title;
    }
    wndclass.cbSize = sizeof(wndclass);
    wndclass.hInstance = hInstance;
    wndclass.lpfnWndProc = InternalWndProc;
    wndclass.style |= CS_HREDRAW | CS_VREDRAW;
    wndclass.hbrBackground = CreateSolidBrush(RGBKEY);

    if( !RegisterClassExA(&wndclass) ) return false;
    int xaddend = GetSystemMetrics(fixedsize?SM_CXFIXEDFRAME:SM_CXFRAME)*2;
    int yaddend = GetSystemMetrics(fixedsize?SM_CYFIXEDFRAME:SM_CYFRAME)*2 + GetSystemMetrics(SM_CYCAPTION);
    if(wndclass.lpszMenuName) yaddend += GetSystemMetrics(SM_CYMENU);

    // Setup the new window's physical parameters - and tell Windows to create it
    g_hAppWnd = CreateWindowA(wndclass.lpszClassName,  // Window class name
                             title,  // Window caption
                             !fixedsize ? WS_OVERLAPPEDWINDOW :  // Window style
                             WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX,
                             CW_USEDEFAULT,  // Initial x pos: use default placement
                             0,              // Initial y pos: not used here
                             g_sizex+xaddend,// Initial x size
                             g_sizey+yaddend,// Initial y size
                             NULL,      // parent window handle
                             NULL,      // window menu handle
                             hInstance, // program instance handle
                             NULL);     // Creation parameters
    return g_hAppWnd != NULL;
}

//! create console window with redirection
static bool RedirectIOToConsole(void)
{
    int hConHandle; size_t lStdHandle;
    CONSOLE_SCREEN_BUFFER_INFO coninfo;
    FILE *fp;
    // allocate a console for this app
    AllocConsole();

    // set the screen buffer to be big enough to let us scroll text
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo);
    coninfo.dwSize.Y = MAX_CONSOLE_LINES;
    SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize);

    // redirect unbuffered STDOUT to the console
    lStdHandle = (size_t)GetStdHandle(STD_OUTPUT_HANDLE);
    hConHandle = _open_osfhandle(lStdHandle, _O_TEXT);
    if(hConHandle <= 0) return false;
    fp = _fdopen( hConHandle, "w" );
    *stdout = *fp;
    setvbuf( stdout, NULL, _IONBF, 0 );

    // redirect unbuffered STDERR to the console
    lStdHandle = (size_t)GetStdHandle(STD_ERROR_HANDLE);
    hConHandle = _open_osfhandle(lStdHandle, _O_TEXT);
    if(hConHandle > 0) {
        fp = _fdopen( hConHandle, "w" );
        *stderr = *fp;
        setvbuf( stderr, NULL, _IONBF, 0 );
    }

    // redirect unbuffered STDIN to the console
    lStdHandle = (size_t)GetStdHandle(STD_INPUT_HANDLE);
    hConHandle = _open_osfhandle(lStdHandle, _O_TEXT);
    if(hConHandle > 0) {
        fp = _fdopen( hConHandle, "r" );
        *stdin = *fp;
        setvbuf( stdin, NULL, _IONBF, 0 );
    }

    // make cout, wcout, cin, wcin, wcerr, cerr, wclog and clog
    // point to console as well
    std::ios::sync_with_stdio();
    return true;
}


video::video()
    : red_mask(0xff0000), red_shift(16), green_mask(0xff00),
      green_shift(8), blue_mask(0xff), blue_shift(0), depth(24)
{
    assert(g_video == 0);
    g_video = this; title = "Video"; running = threaded = calc_fps = false; updating = true;
}

//! optionally call it just before init() to set own 
void video::win_set_class(WNDCLASSEX &wcex)
{
    gWndClass = &wcex;
}

void video::win_load_accelerators(int idc)
{
    hAccelTable = LoadAccelerators(win_hInstance, MAKEINTRESOURCE(idc));
}

bool video::init_console()
{
    if(RedirectIOToConsole()) {
        if(!g_pImg && g_sizex && g_sizey)
            g_pImg = new unsigned int[g_sizex * g_sizey];
        if(g_pImg) running = true;
        return true;
    }
    return false;
}

video::~video()
{
    if(g_video) terminate();
}

DWORD WINAPI thread_video(LPVOID lpParameter)
{
    video *v = (video*)lpParameter;
    v->on_process();
    return 0;
}

static bool loop_once(video *v)
{
    // screen update notify
    if(int updates = g_updates) {
        g_updates = 0;
        if(g_video->updating) { g_skips += updates-1; g_fps++; }
        else g_skips += updates;
        UpdateWindow(g_hAppWnd);
    }
    // update fps
    DWORD msec = GetTickCount();
    if(v->calc_fps && msec >= g_msec+1000) {
        double sec = (msec - g_msec)/1000.0;
        char buffer[256], n = _snprintf(buffer, 128, "%s: %d fps", v->title, int(double(g_fps + g_skips)/sec));
        if(g_skips) _snprintf(buffer+n, 128, " - %d skipped = %d updates", int(g_skips/sec), int(g_fps/sec));
        SetWindowTextA(g_hAppWnd, buffer);
        g_msec = msec; g_skips = g_fps = 0;
    }
    // event processing, including painting
    MSG msg;
    if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){
        if( msg.message == WM_QUIT ) { v->running = false; return false; }
        if( !hAccelTable || !TranslateAccelerator(msg.hwnd, hAccelTable, &msg) ){
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        return true; // try again
    }
    return false;
}

//! Do standard event loop
void video::main_loop()
{
    // let Windows draw and unroll the window
    InvalidateRect(g_hAppWnd, 0, false);
    g_msec = GetTickCount(); // let's stay for 0,5 sec
    while(g_msec + 500 > GetTickCount()) { loop_once(this); Sleep(1); }
    g_msec = GetTickCount();
    // now, start main process
    if(threaded) {
        g_handles[0] = CreateThread (
            NULL,             // LPSECURITY_ATTRIBUTES security_attrs
            0,                // SIZE_T stacksize
            (LPTHREAD_START_ROUTINE) thread_video,
            this,               // argument
            0, 0);
        if(!g_handles[0]) { DisplayError("Can't create thread"); return; }
        else // harmless race is possible here
            g_handles[1] = CreateEvent(NULL, false, false, NULL);
        while(running) {
            while(loop_once(this));
            YIELD_TO_THREAD(); // give time for processing when running on single CPU
            DWORD r = MsgWaitForMultipleObjects(2, g_handles, false, INFINITE, QS_ALLINPUT^QS_MOUSEMOVE);
            if(r == WAIT_OBJECT_0) break; // thread terminated
        }
        running = false;
        if(WaitForSingleObject(g_handles[0], 3000) == WAIT_TIMEOUT){
            // there was not enough time for graceful shutdown, killing the example with code 1.
            exit(1);
        }
        if(g_handles[0]) CloseHandle(g_handles[0]);
        if(g_handles[1]) CloseHandle(g_handles[1]);
        g_handles[0] = g_handles[1] = 0;
    }
    else on_process();
}

//! Refresh screen picture
bool video::next_frame()
{
    if(!running) return false;
    g_updates++; // Fast but inaccurate counter. The data race here is benign.
    if(!threaded) while(loop_once(this));
    else if(g_handles[1]) {
        SetEvent(g_handles[1]);
        YIELD_TO_THREAD();
    }
    return true;
}

//! Change window title
void video::show_title()
{
    if(g_hAppWnd)
        SetWindowTextA(g_hAppWnd, title);
}

#endif //__WINVIDEO_H__
