/* bzflag
 * Copyright (c) 1993-2018 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

// Jeff Myers 10/8/97 added hooks to display setings dialog
// crs 10/26/1997 modified JM's changes

#include "WinWindow.h"
#include "OpenGLGState.h"
#include <stdio.h>
#include <math.h>
#include "StateDatabase.h"
#include "TextureManager.h"
#include "CommandsStandard.h"

WinWindow*      WinWindow::first = NULL;
HPALETTE        WinWindow::colormap = NULL;

HWND WinWindow::hwnd = NULL;

WinWindow::WinWindow(const WinDisplay* _display, WinVisual* _visual) :
    BzfWindow(_display),
    display(_display),
    visual(*_visual),
    inDestroy(false),
    hwndChild(NULL),
    hRC(NULL),
    hDC(NULL),
    hDCChild(NULL),
    inactiveDueToDeactivate(false),
    inactiveDueToDeactivateAll(false),
    useColormap(false),
    hasGamma(false),
    gammaVal(1.0f),
    prev(NULL),
    next(NULL),
    mouseGrab(false)
{
    // make window
    hwnd = CreateWindow("BZFLAG", "bzflag",
                        WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP |
                        WS_BORDER | WS_CAPTION,
                        0, 0, 640, 480, NULL, NULL,
                        display->getRep()->hInstance, NULL);
    if (hwnd == NULL) return;

    // create child window.  OpenGL will bind only to the child window.
    createChild();
    if (hwndChild == NULL) return;

    // get DC
    hDC = GetDC(hwnd);

    // set colormap
    if (colormap)
    {
        ::SelectPalette(hDC, colormap, FALSE);
        ::RealizePalette(hDC);
    }

    // other initialization
    SetMapMode(hDC, MM_TEXT);

    // add to list
    display->getRep()->ref();
    prev = NULL;
    next = first;
    if (prev) prev->next = this;
    if (next) next->prev = this;
    first = this;
}

WinWindow::~WinWindow()
{
    destroyChild();

    if (hDC != NULL)
        ReleaseDC(hwnd, hDC);
    if (hwnd != NULL)
        DestroyWindow(hwnd);

    if (prev) prev->next = next;
    if (next) next->prev = prev;
    if (first == this) first = next;

    if (first == NULL && colormap != NULL)
    {
        DeleteObject(colormap);
        colormap = NULL;
    }

    display->getRep()->unref();
}

bool            WinWindow::isValid() const
{
    return hwnd != NULL;
}

void            WinWindow::showWindow(bool on)
{
    ShowWindow(hwnd, on ? SW_SHOW : SW_HIDE);
}

void            WinWindow::getPosition(int& x, int& y)
{
    RECT rect;
    GetWindowRect(hwnd, &rect);
    x = (int)rect.left;
    y = (int)rect.top;
}

void            WinWindow::getSize(int& width, int& height) const
{
    RECT rect;
    GetClientRect(hwnd, &rect);
    width = (int)rect.right - (int)rect.left;
    height = (int)rect.bottom - (int)rect.top;
}

void            WinWindow::setTitle(const char* title)
{
    SetWindowText(hwnd, title);
}

void            WinWindow::setPosition(int x, int y)
{
    RECT rect;
    GetWindowRect(hwnd, &rect);
    MoveWindow(hwnd, x, y, rect.right - rect.left, rect.bottom - rect.top, FALSE);
}

void            WinWindow::setSize(int width, int height)
{
    RECT rect;
    GetWindowRect(hwnd, &rect);
    int dx = rect.right - rect.left;
    int dy = rect.bottom - rect.top;
    RECT crect;
    GetClientRect(hwnd, &crect);
    dx -= crect.right;
    dy -= crect.bottom;
    MoveWindow(hwnd, rect.left, rect.top, width + dx, height + dy, FALSE);
    MoveWindow(hwndChild, 0, 0, width, height, FALSE);
}

void            WinWindow::setMinSize(int, int)
{
    // FIXME
}

void            WinWindow::setFullscreen(bool on)
{
    // Stop grabbing the mouse so the cursor can't get stuck outside of the window
    ungrabMouse();

    if (on)
    {
        // no window decorations
        DWORD style = GetWindowLong(hwnd, GWL_STYLE);
        style &= ~(WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_THICKFRAME);
        SetWindowLong(hwnd, GWL_STYLE, style);

        // take up the whole screen
        if (display->isFullScreenOnly())
            MoveWindow(hwnd, 0, 0,
                       display->getFullWidth(),
                       display->getFullHeight(), FALSE);
        else
            MoveWindow(hwnd, 0, 0,
                       GetDeviceCaps(hDC, HORZRES),
                       GetDeviceCaps(hDC, VERTRES), FALSE);

    }
    else if (!display->isFullScreenOnly())
    {
        // reset the resolution if we changed it before
        display->setDefaultResolution();

        // window stuff
        DWORD style = GetWindowLong(hwnd, GWL_STYLE);
        style |= (WS_BORDER | WS_CAPTION);
        SetWindowLong(hwnd, GWL_STYLE, style);

        // size it
        if (BZDB.isSet("geometry"))
        {
            int w, h, x, y, count;
            char xs, ys;
            count = sscanf(BZDB.get("geometry").c_str(),
                           "%dx%d%c%d%c%d", &w, &h, &xs, &x, &ys, &y);
            if (w < 256) w = 256;
            if (h < 192) h = 192;
            if (count == 6)
            {
                if (xs == '-') x = display->getWidth() - x - w;
                if (ys == '-') y = display->getHeight() - y - h;
                setPosition(x, y);
            }
            setSize(w, h);
        }
        else
        {
            // uh.... right
            setPosition(0,0);
            setSize(640,480);
        }

        // force windows to repaint the whole desktop
        RECT rect;
        GetWindowRect(NULL, &rect);
        InvalidateRect(NULL, &rect, TRUE);
    }

    // resize child
    int width, height;
    getSize(width, height);
    MoveWindow(hwndChild, 0, 0, width, height, FALSE);

    // reset mouse grab
    if (mouseGrab)
        grabMouse();
}

void            WinWindow::iconify()
{
    ShowWindow(hwnd, SW_MINIMIZE);
}

void            WinWindow::warpMouse(int x, int y)
{
    POINT point;
    point.x = x;
    point.y = y;
    ClientToScreen(hwnd, &point);
    SetCursorPos((int)point.x, (int)point.y);
}

void            WinWindow::getMouse(int& x, int& y) const
{
    POINT point;
    GetCursorPos(&point);
    ScreenToClient(hwnd, &point);
    x = (int)point.x;
    y = (int)point.y;
}

void            WinWindow::disableConfineToMotionbox()
{
    ungrabMouse();
}

void            WinWindow::confineToMotionbox(int x1, int y1, int x2, int y2)
{
    // Store the boundary positions as two points (top left and bottom right)
    POINT p1, p2;
    p1.x = x1;
    p1.y = y1;
    p2.x = x2;
    p2.y = y2;

    // Transform them from client space to screen space
    ClientToScreen(hwnd, &p1);
    ClientToScreen(hwnd, &p2);

    // Store them in a rectangle
    RECT rect;
    rect.top = p1.y;
    rect.left = p1.x;
    rect.bottom = p2.y;
    rect.right = p2.x;

    // Restrict cursor to that rectangle
    ClipCursor(&rect);
}

void            WinWindow::grabMouse()
{
    RECT wrect;
    GetWindowRect(hwnd, &wrect);

    int xborder = GetSystemMetrics(SM_CXDLGFRAME);
    int yborder = GetSystemMetrics(SM_CYDLGFRAME);
    int titlebar = GetSystemMetrics(SM_CYCAPTION);

    DWORD style = GetWindowLong(hwnd, GWL_STYLE);
    // don't compensate for window trimmings if they're turned off
    if (!((style & (WS_BORDER | WS_CAPTION | WS_DLGFRAME))
            == (WS_BORDER | WS_CAPTION | WS_DLGFRAME)))
        xborder = yborder = titlebar = 0;

    RECT rect;
    rect.top = wrect.top + titlebar + yborder;
    rect.left = wrect.left + xborder;
    rect.bottom = wrect.bottom - yborder;
    rect.right = wrect.right - xborder;
    ClipCursor(&rect);
}

void            WinWindow::ungrabMouse()
{
    ClipCursor(NULL);
}

void            WinWindow::enableGrabMouse(bool on)
{
    if (on)
    {
        mouseGrab = true;
        grabMouse();
    }
    else
    {
        mouseGrab = false;
        ungrabMouse();
    }
}

void            WinWindow::showMouse()
{
    // FIXME
}

void            WinWindow::hideMouse()
{
    // FIXME
}

void            WinWindow::setGamma(float newGamma)
{
    if (!useColormap && !hasGamma)
        return;

    // save gamma
    gammaVal = newGamma;

    // build gamma ramps or adjust colormap
    if (useColormap)
    {
        makeColormap(pfd);
        paletteChanged();
    }
    else
    {
        WORD map[6][256];
        for (int i = 0; i < 256; i++)
        {
            BYTE v = getIntensityValue((float)i / 255.0f);
            map[0][i] = map[1][i] = map[2][i] =
                                        map[3][i] = map[4][i] = map[5][i] = (WORD)v + ((WORD)v << 8);
        }
        setGammaRamps((const WORD*)map);
    }
}

float           WinWindow::getGamma() const
{
    return gammaVal;
}

bool            WinWindow::hasGammaControl() const
{
    return useColormap || hasGamma;
}

void            WinWindow::makeCurrent()
{
    if (hDCChild != NULL)
    {
        wglMakeCurrent(hDCChild, hRC);
        if (isNewContext)
        {
            OpenGLGState::initContext();
            TextureManager::instance().reloadTextures();
            isNewContext = false;
        }
    }
}

void            WinWindow::swapBuffers()
{
    if (hDCChild != NULL)
        SwapBuffers(hDCChild);
}

void            WinWindow::makeContext()
{
    if (!inactiveDueToDeactivate && !inactiveDueToDeactivateAll)
        if (hDCChild == NULL)
            createChild();
    makeCurrent();
}

void            WinWindow::freeContext()
{
    if (hDCChild != NULL)
        destroyChild();
}

void            WinWindow::createChild()
{
    // get parent size
    int width, height;
    getSize(width, height);

    // make window
    hwndChild = CreateWindow("BZFLAG", "opengl",
                             WS_CLIPSIBLINGS | WS_CLIPCHILDREN |
                             WS_CHILD | WS_VISIBLE,
                             0, 0, width, height, hwnd, NULL,
                             display->getRep()->hInstance, NULL);
    if (hwndChild == NULL) return;

    if (display->isFullScreenOnly())
        setFullscreen(true);

    // get DC
    hDCChild = GetDC(hwndChild);

    // force visual to recalculate the PFD
    visual.reset();

    // set pixel format
    const PIXELFORMATDESCRIPTOR* tmpPFD;
    const int pixelFormat = visual.get(hDCChild, &tmpPFD);
    if (pixelFormat == 0 || !SetPixelFormat(hDCChild, pixelFormat, tmpPFD))
    {
        ReleaseDC(hwndChild, hDCChild);
        DestroyWindow(hwndChild);
        hwndChild = NULL;
        hDCChild = NULL;
        return;
    }

    // get pixel format description
    DescribePixelFormat(hDCChild, pixelFormat, sizeof(pfd), &pfd);

    // make colormap
    useColormap = ((pfd.dwFlags & PFD_NEED_PALETTE) != 0);
    if (colormap == NULL && useColormap)
        makeColormap(pfd);
    if (colormap)
        ::SelectPalette(hDCChild, colormap, FALSE);

    // if no colormap then adjust gamma ramps
    if (!useColormap)
    {
        getGammaRamps(origGammaRamps);
        setGamma(gammaVal);
    }

    // make OpenGL context
    hRC = wglCreateContext(hDCChild);
    if (hRC == NULL)
    {
        ReleaseDC(hwndChild, hDCChild);
        DestroyWindow(hwnd);
        hwndChild = NULL;
        hDCChild = NULL;
        return;
    }

    isNewContext = true;

    if (colormap)
        ::RealizePalette(hDCChild);

    // other initialization
    SetMapMode(hDCChild, MM_TEXT);
}

void            WinWindow::destroyChild()
{
    setGammaRamps(origGammaRamps);
    if (hRC != NULL)
    {
        if (wglGetCurrentContext() == hRC)
            wglMakeCurrent(NULL, NULL);
        wglDeleteContext(hRC);
        hRC = NULL;
    }
    if (hDCChild != NULL)
    {
        ReleaseDC(hwndChild, hDCChild);
        hDCChild = NULL;
    }
    if (hwndChild != NULL)
    {
        DestroyWindow(hwndChild);
        hwndChild = NULL;
    }
}


void            WinWindow::getGammaRamps(WORD* ramps)
{
    if (hDCChild == NULL)
        return;

    // get device gamma ramps
    hasGamma = GetDeviceGammaRamp(hDCChild, ramps) != FALSE;
}

void            WinWindow::setGammaRamps(const WORD* ramps)
{
    if (hDCChild == NULL)
        return;

    if (hasGamma)
        SetDeviceGammaRamp(hDCChild, (LPVOID)ramps);
}

WinWindow*      WinWindow::lookupWindow(HWND hwnd)
{
    if (hwnd == NULL)
        return NULL;

    WinWindow* scan = first;
    while (scan && scan->hwnd != hwnd && scan->hwndChild != hwnd)
        scan = scan->next;
    return scan;
}

void            WinWindow::deactivateAll()
{
    for (WinWindow* scan = first; scan; scan = scan->next)
    {
        scan->freeContext();
        scan->inactiveDueToDeactivateAll = true;
    }
}

void            WinWindow::reactivateAll()
{
    for (WinWindow* scan = first; scan; scan = scan->next)
    {
        const bool hadChild = (scan->hDCChild != NULL);
        scan->inactiveDueToDeactivateAll = false;
        scan->makeContext();
    }
}

HWND            WinWindow::getHandle()
{
    return hwnd;
}

LONG            WinWindow::queryNewPalette()
{
    if (colormap == NULL) return FALSE;

    HPALETTE oldPalette = ::SelectPalette(hDC, colormap, FALSE);
    ::RealizePalette(hDC);
    ::SelectPalette(hDC, oldPalette, FALSE);

    if (hDCChild != NULL)
    {
        oldPalette = ::SelectPalette(hDCChild, colormap, FALSE);
        ::RealizePalette(hDCChild);
        ::SelectPalette(hDCChild, oldPalette, FALSE);
    }

    return TRUE;
}

void            WinWindow::paletteChanged()
{
    if (colormap == NULL) return;

    HPALETTE oldPalette = ::SelectPalette(hDC, colormap, FALSE);
    ::RealizePalette(hDC);
    ::SelectPalette(hDC, oldPalette, FALSE);

    if (hDCChild != NULL)
    {
        oldPalette = ::SelectPalette(hDCChild, colormap, FALSE);
        ::RealizePalette(hDCChild);
        ::SelectPalette(hDCChild, oldPalette, FALSE);
    }
}

bool            WinWindow::activate()
{
    inactiveDueToDeactivate = false;

    // restore window
    ShowWindow(hwnd, SW_RESTORE);
    SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0,
                 SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);

    // recreate OpenGL context
    const bool hadChild = (hDCChild != NULL);
    makeContext();

    if (mouseGrab)
        grabMouse();

    if (!hadChild && hDCChild != NULL)
    {
        // force a redraw
        callExposeCallbacks();

        return true;
    }

    return false;
}

bool            WinWindow::deactivate()
{
    // minimize window while not active.  skip if being destroyed.
    if (!inDestroy)
        ShowWindow(hwnd, SW_MINIMIZE);

    // destroy OpenGL context
    const bool hadChild = (hDCChild != NULL);
    freeContext();

    if (mouseGrab)
        ungrabMouse();

    inactiveDueToDeactivate = true;
    return hadChild;
}

void            WinWindow::onDestroy()
{
    inDestroy = true;
    CommandsStandard::quit();
}

BYTE            WinWindow::getIntensityValue(float i) const
{
    if (i <= 0.0f) return 0;
    if (i >= 1.0f) return 255;
    i = powf(i, 1.0f / gammaVal);
    return (BYTE)(0.5f + 255.0f * i);
}

float           WinWindow::getComponentFromIndex(
    int i, UINT nbits, UINT shift)
{
    const int mask = (1 << nbits) - 1;
    return (float)((i >> shift) & mask) / (float)mask;
}

void            WinWindow::makeColormap(
    const PIXELFORMATDESCRIPTOR& pfd)
{
    // compute number of colors
    const int n = 1 << pfd.cColorBits;

    // make and initialize a logical palette with room for all colors
    LOGPALETTE* logicalPalette = (LOGPALETTE*)(void*)new char[
                                  sizeof(LOGPALETTE) + n * sizeof(PALETTEENTRY)];
    logicalPalette->palVersion = 0x300;
    logicalPalette->palNumEntries = (WORD)n;

    // set the colors in the logical palette
    int i, j;
    for (i = 0; i < n; i++)
    {
        logicalPalette->palPalEntry[i].peRed = getIntensityValue(
                getComponentFromIndex(i, pfd.cRedBits, pfd.cRedShift));
        logicalPalette->palPalEntry[i].peGreen = getIntensityValue(
                    getComponentFromIndex(i, pfd.cGreenBits, pfd.cGreenShift));
        logicalPalette->palPalEntry[i].peBlue = getIntensityValue(
                getComponentFromIndex(i, pfd.cBlueBits, pfd.cBlueShift));
        logicalPalette->palPalEntry[i].peFlags = 0;
    }

    static const bool matchGDI = false;
    if (matchGDI)
    {
        // default colors reserved by system
        static const int NUM_GDI_COLORS = 20;
        static const PALETTEENTRY gdi[NUM_GDI_COLORS] =
        {
            { 0,   0,   0,    0 },
            { 0x80,0,   0,    0 },
            { 0,   0x80,0,    0 },
            { 0x80,0x80,0,    0 },
            { 0,   0,   0x80, 0 },
            { 0x80,0,   0x80, 0 },
            { 0,   0x80,0x80, 0 },
            { 0xC0,0xC0,0xC0, 0 },
            { 192, 220, 192,  0 },
            { 166, 202, 240,  0 },
            { 255, 251, 240,  0 },
            { 160, 160, 164,  0 },
            { 0x80,0x80,0x80, 0 },
            { 0xFF,0,   0,    0 },
            { 0,   0xFF,0,    0 },
            { 0xFF,0xFF,0,    0 },
            { 0,   0,   0xFF, 0 },
            { 0xFF,0,   0xFF, 0 },
            { 0,   0xFF,0xFF, 0 },
            { 0xFF,0xFF,0xFF, 0 }
        };

        // check for exact matches to GDI palette.
        BOOL exactMatch[NUM_GDI_COLORS];
        for (j = 0; j < NUM_GDI_COLORS; j++)
            exactMatch[j] = FALSE;
        for (i = 0; i < n; i++)
        {
            PALETTEENTRY* p = logicalPalette->palPalEntry + i;
            for (j = 0; j < NUM_GDI_COLORS; j++)
                if (p->peRed   == gdi[j].peRed &&
                        p->peGreen == gdi[j].peGreen &&
                        p->peBlue  == gdi[j].peBlue)
                {
                    exactMatch[j] = TRUE;
                    p->peFlags = 1;
                    break;
                }
        }

        // match default GDI palette by least-squares.  we'll use the peFlags
        // member of the palette entries temporarily to track which entries
        // have been matched.
        for (j = 0; j < NUM_GDI_COLORS; j++)
        {
            if (exactMatch[j]) continue;

            int minError = 4 * 256 * 256;     // bigger than largest error
            int bestMatch = -1;

            for (i = 0; i < n; i++)
            {
                const PALETTEENTRY* p = logicalPalette->palPalEntry + i;
                if (p->peFlags != 0) continue;

                int error = 0;
                error += (p->peRed   - gdi[j].peRed)   * (p->peRed   - gdi[j].peRed);
                error += (p->peGreen - gdi[j].peGreen) * (p->peGreen - gdi[j].peGreen);
                error += (p->peBlue  - gdi[j].peBlue)  * (p->peBlue  - gdi[j].peBlue);
                if (error < minError)
                {
                    bestMatch = i;
                    minError = error;
                }
            }

            // adjust my palette to match GDI palette and note the change
            if (bestMatch != -1)
            {
                logicalPalette->palPalEntry[bestMatch] = gdi[j];
                logicalPalette->palPalEntry[bestMatch].peFlags = 1;
            }
        }

        // set the peFlags the way we want
        for (i = 0; i < n; i++)
            logicalPalette->palPalEntry[i].peFlags = 0;
    }

    // create the palette
    if (colormap == NULL)
        colormap = ::CreatePalette(logicalPalette);
    else
        ::SetPaletteEntries(colormap, 0, n, logicalPalette->palPalEntry + 0);

    // free the cruft
    delete [] logicalPalette;
}

// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
