/*
* Descent 3 
* Copyright (C) 2024 Parallax Software
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

--- HISTORICAL COMMENTS FOLLOW ---

 * $Logfile: /DescentIII/Main/ddgr_win32/ddgrWin32GDI.cpp $
 * $Revision: 10 $
 * $Date: 10/02/98 11:15a $
 * $Author: Jeff $
 *
 *	DDGR v2.0 GDI(X) implementation
 *
 * $Log: /DescentIII/Main/ddgr_win32/ddgrWin32GDI.cpp $
 *
 * 10    10/02/98 11:15a Jeff
 * added HBITMAP, HFONT and HBRUSH type casts where needed to satisfy the
 * compiler
 *
 * 9     6/02/98 12:46p Samir
 * removed restriction of ddgr system when going to true-color
 * resolutions.
 *
 * 8     4/23/98 6:38p Jason
 * made bitmaps use 1555 format
 *
 * 7     4/09/98 11:00a Samir
 * took out assertion in ddgr_gdi_Close that caused errors and assertions
 * to 'crap' out.
 *
 * 6     11/14/97 12:32p Samir
 * DirectDraw works under GDIX windowed subsystem (NORMAL cooperative
 * mode).
 *
 * 5     9/23/97 12:15p Jason
 * fixed color bug
 *
 * 4     9/12/97 4:13p Samir
 * Added some private data access functions and more DirectX
 * functionality.
 *
 * 3     8/12/97 10:43p Matt
 * Fixed RGB conversion bug
 *
 * 5     6/16/97 4:46p Samir
 * OpenGL works in window too.
 *
 * 4     6/13/97 3:01p Samir
 * Fixed flip assertions and OpenGL imp.
 *
 * 3     6/13/97 1:24p Samir
 * FlipVideo doesn't flip when no backbuffer, so just do nothing.
 *
 * 2     6/13/97 12:24p Samir
 * Fixed OpenGLDC hack and removed assert in lock if data ptr = NULL
 *
 * 1     6/12/97 6:33p Samir
 * Initial revision
 *
 * $NoKeywords: $
 */

#include "ddgrWin32.h"
#include "ddgrWin32GDI.h"
#include "Application.h"
#include "mono.h"
#include "pserror.h"
#include "gr.h"

/*	Global Library Data
 */
tDDGRGDIInternalData DDGR_GDI_lib_data;

/*	Primary Interface Functions
 */

//	---------------------------------------------------------------------------
//	initializes the objects needed to use GDI

bool ddgr_gdi_Init(oeApplication *app, bool fullscreen, bool ddraw) {
  HRESULT hres;

  GDI_DATA(fullscreen) = fullscreen;
  GDI_DATA(hPrimaryWnd) = (HWND)((oeWin32Application *)app)->m_hWnd;

  GDI_DATA(lpDD) = NULL;
  if (ddraw) {
    hres = DirectDrawCreate(NULL, &GDI_DATA(lpDD), NULL);
    if (hres != DD_OK) {
      ddgr_PushError("Failure to initialize DirectDraw driver (%d)", LOWORD(hres));
      return false;
    }
  }

  //	Initialize DirectDraw exclusive system if we're going fullscreen, and GDIX system
  if (GDI_DATA(fullscreen)) {
    if (ddraw) {
      hres = GDI_DATA(lpDD)->SetCooperativeLevel(GDI_DATA(hPrimaryWnd),
                                                 DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);

      if (hres != DD_OK) {
        ddgr_PushError("Failed to set DirectDraw fullscreen access level (%d)", LOWORD(hres));
        return false;
      }
    } else {
      // grab all device modes for the current display device.
      unsigned i;
      for (i = 0; i < GDI_MAX_DEVMODES; i++) {
        if (EnumDisplaySettings(NULL, i, &GDI_DATA(devmodes[i])) == FALSE) {
          break;
        }
        HDC hdc = GetDC(GDI_DATA(hPrimaryWnd));

        if (GDI_DATA(devmodes[i].dmBitsPerPel) == (uint32_t)GetDeviceCaps(hdc, BITSPIXEL) &&
            GDI_DATA(devmodes[i].dmPelsWidth) == (uint32_t)GetDeviceCaps(hdc, HORZRES) &&
            GDI_DATA(devmodes[i].dmPelsHeight) == (uint32_t)GetDeviceCaps(hdc, VERTRES))
          GDI_DATA(olddispmode) = i;

        ReleaseDC(GDI_DATA(hPrimaryWnd), hdc);
      }
    }

    GDI_DATA(ddraw) = ddraw;
  } else {
    if (ddraw) {
      hres = GDI_DATA(lpDD)->SetCooperativeLevel(GDI_DATA(hPrimaryWnd), DDSCL_NORMAL);
      if (hres != DD_OK) {
        ddgr_PushError("Failed to set DirectDraw normal access level (%d)", LOWORD(hres));
        return false;
      }
    }
  }

  GDI_DATA(hOffscreenDC) = NULL;
  GDI_DATA(vidrefs) = 0; // reset video reference count.
  GDI_DATA(init) = true;

  mprintf(0, "GDI system initialized.\n");

  return true;
}

//	---------------------------------------------------------------------------
//	Deinitialized GDI objects

void ddgr_gdi_Close() {
  if (GDI_DATA(hOffscreenDC)) {
    DeleteDC(GDI_DATA(hOffscreenDC));
  }

  if (GDI_DATA(fullscreen)) {
    if (GDI_DATA(ddraw)) {
      //	For full screen, deinitialize directdraw/
      if (GDI_DATA(lpDD)) {
        GDI_DATA(lpDD)->RestoreDisplayMode();
        GDI_DATA(lpDD)->Release();
      }
      GDI_DATA(lpDD) = NULL;
    } else {
      //	do a ChangeDisplaySettings to old mode.
      LONG lres;
      GDI_DATA(devmodes[GDI_DATA(olddispmode)]).dmFields =
          DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | DM_DISPLAYFREQUENCY;

      lres = ChangeDisplaySettings(&GDI_DATA(devmodes[GDI_DATA(olddispmode)]), 0);
      ASSERT(lres == DISP_CHANGE_SUCCESSFUL);
    }
  }

  GDI_DATA(hPrimaryWnd) = NULL;
  GDI_DATA(init) = false;

  mprintf(0, "GDI system closed.\n");
}

//	---------------------------------------------------------------------------
//	Initializes the video surface for blting
//	note that is the surface specified is a back buffer by sf->flags, then we need
//	to create a GDI bitmap for the back buffer, and our visible screen is the front buffer

bool ddgr_gdi_surf_InitVideo(ddgr_surface *sf) {
  tGDISurface *gdisf, *gdisf2;

  //	we need to create a DC for our GDI bitmaps if this is first ref to video surface.
  if (GDI_DATA(vidrefs) == 0) {
    //	if we are in fullscreen, do display mode change
    HDC hdc;

    if (GDI_DATA(fullscreen)) {
      if (GDI_DATA(ddraw)) {
        HRESULT hres;

        hres = GDI_DATA(lpDD)->SetDisplayMode(sf->w, sf->h, sf->bpp);
        if (hres != DD_OK) {
          ddgr_PushError("Unable to set DirectDraw display mode in fullscreen (%d)\n", LOWORD(hres));
          return false;
        }
      } else {
        // grab all device modes for the current display device.
        unsigned i;
        for (i = 0; i < GDI_MAX_DEVMODES; i++) {
          LONG lres;
          if ((int)GDI_DATA(devmodes[i].dmBitsPerPel) == sf->bpp && (int)GDI_DATA(devmodes[i].dmPelsWidth) == sf->w &&
              (int)GDI_DATA(devmodes[i].dmPelsHeight) == sf->h) {
            //	do a ChangeDisplaySettings to old mode.
            GDI_DATA(devmodes[i]).dmFields =
                DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | DM_DISPLAYFREQUENCY;

            lres = ChangeDisplaySettings(&GDI_DATA(devmodes[i]), 0);
            ASSERT(lres == DISP_CHANGE_SUCCESSFUL);
            break;
          }
        }
      }
    }
    //	HACKS FOR OPENGL, WILL CHANGE SOON.
    hdc = CreateCompatibleDC(NULL);
    if (hdc == NULL) {
      ddgr_PushError("Unable to create offscreen device context (%x).", GetLastError());
      return false;
    }
    GDI_DATA(hOffscreenDC) = hdc;
  }

  GDI_DATA(vidrefs)++; // increment vidref count

  gdisf = new tGDISurface;
  gdisf->hbm = NULL;
  gdisf->data = NULL;
  gdisf->rowsize = NULL;
  gdisf->hwnd = NULL;

  // this means that we are capturing the screen, not creating a screen.
  //	get the screen's width and height and bitdepth
  if (sf->flags == 0) {
    sf->w = (int)GetDeviceCaps(GDI_DATA(hOffscreenDC), HORZRES);
    sf->h = (int)GetDeviceCaps(GDI_DATA(hOffscreenDC), VERTRES);
    sf->bpp = (int)GetDeviceCaps(GDI_DATA(hOffscreenDC), BITSPIXEL);
  }

  HDC hdc = GetDC(NULL);
  int tbpp = GetDeviceCaps(hdc, BITSPIXEL);
  ReleaseDC(NULL, hdc);

  if (tbpp < sf->bpp) {
    ddgr_PushError("Desktop color depth must be set to a %d-bit color mode or greater.\n", sf->bpp);
    return false;
  }

  //	we create a dummy surface for the screen's back buffer, or our accessable buffer
  //	specifying SURFFLAG_BACKBUFFER ensures that we create a GDI bitmap of size requested
  //	in 'sf' and it will be our renderable bitmap, or OUR primary render buffer
  if (sf->flags & SURFFLAG_BACKBUFFER) {
    ddgr_surface back_sf;
    back_sf.w = sf->w;
    back_sf.h = sf->h;
    back_sf.bpp = sf->bpp;
    back_sf.type = SURFTYPE_GENERIC;

    if (!ddgr_gdi_surf_Create(&back_sf)) {
      ddgr_PushError((0, "Failed to create back buffer <%s>", sf->name));
      delete gdisf;
      return false;
    }

    //	set our front buffer to this newly created surface and delete the gdi bitmap object
    gdisf2 = (tGDISurface *)back_sf.obj;
    gdisf->hbm = gdisf2->hbm;
    gdisf->data = gdisf2->data;
    gdisf->rowsize = gdisf2->rowsize;
    gdisf->backbuffer = true;
    delete gdisf2;
  } else {
    gdisf->hbm = NULL; // this is really just the screen.
    gdisf->backbuffer = false;
  }

  gdisf->hwnd = GDI_DATA(hPrimaryWnd);

  sf->obj = (void *)gdisf;

  return true;
}

//	---------------------------------------------------------------------------
//	deinitializes the DC created in InitVideo

void ddgr_gdi_surf_CloseVideo(ddgr_surface *sf) {
  tGDISurface *bm = (tGDISurface *)sf->obj;

  ASSERT(GDI_DATA(vidrefs) > 0);

  GDI_DATA(vidrefs)--;
  if (GDI_DATA(vidrefs) == 0) {
    if (GDI_DATA(fullscreen)) {
    }
    //	HACK for OPENGL support.
    DeleteDC(GDI_DATA(hOffscreenDC));
  }

  if (sf->flags & SURFFLAG_BACKBUFFER)
    DeleteObject(bm->hbm); // eliminate back buffer

  delete bm;
}

//	---------------------------------------------------------------------------
//	flip, does absolutely nothing

bool ddgr_gdi_surf_FlipVideo(ddgr_surface *sf) {
  HBITMAP old_bmp, h_sbm;
  HDC hdc_dest;
  RECT rect;
  int dw, dh;
  tGDISurface *bm = (tGDISurface *)sf->obj;

  ASSERT(bm->hwnd != NULL);

  //	don't give an error if there's no backbuffer.
  if (!bm->backbuffer)
    return true;

  h_sbm = bm->hbm;

  //	if we associated another window with our screem, then make sure their DC is the dest.
  //	other wise, use our base window's DC for destination, and select our front buffer as the
  //	source bitmap into our driver's DC.
  //	if (bm->hwnd == NULL) {
  //		hdc_dest = GetDC(GDI_DATA(hwnd));
  //		GetClientRect(GDI_DATA(hwnd),&rect);
  //	}
  //	else {
  hdc_dest = GetDC(bm->hwnd);
  GetClientRect(bm->hwnd, &rect);
  //	}

  dw = rect.right - rect.left;
  dh = rect.bottom - rect.top;

  old_bmp = (HBITMAP)SelectObject(GDI_DATA(hOffscreenDC), h_sbm);

  BOOL bltres = BitBlt(hdc_dest, 0, 0, dw, dh, GDI_DATA(hOffscreenDC), 0, 0, SRCCOPY);

  SelectObject(GDI_DATA(hOffscreenDC), old_bmp);

  ReleaseDC(bm->hwnd, hdc_dest);

  return (bltres) ? true : false;
}

//	---------------------------------------------------------------------------
//	Creates a GDI DIBitmap

bool ddgr_gdi_surf_Create(ddgr_surface *sf) {
  tGDISurface *bm;

  //	set up bitmap header
  int nw = sf->w;
  if (nw % 4)
    nw = ((sf->w / 4) * 4) + 4;

  bm = new tGDISurface;

  if (sf->bpp == BPP_8) {
    tDIBHeader8 header;

    header.bmi.biSize = sizeof(BITMAPINFOHEADER);
    header.bmi.biWidth = nw;
    header.bmi.biHeight = -sf->h; // Always a top down bitmap!!
    header.bmi.biPlanes = 1;
    header.bmi.biBitCount = sf->bpp;
    header.bmi.biCompression = BI_BITFIELDS;
    header.bmi.biSizeImage = 0;
    header.bmi.biXPelsPerMeter = 0;
    header.bmi.biYPelsPerMeter = 0;
    header.bmi.biClrUsed = 0;
    header.bmi.biClrImportant = 0;

    bm->hbm = CreateDIBSection(GDI_DATA(hOffscreenDC), (BITMAPINFO *)&header, DIB_RGB_COLORS, &bm->data, NULL, 0);
  } else {
    tDIBHeader header;
    header.bmi.biSize = sizeof(BITMAPINFOHEADER);
    header.bmi.biWidth = nw;
    header.bmi.biHeight = -sf->h; // Always a top down bitmap!!
    header.bmi.biPlanes = 1;
    header.bmi.biBitCount = sf->bpp;
    header.bmi.biCompression = BI_BITFIELDS;
    header.bmi.biSizeImage = 0;
    header.bmi.biXPelsPerMeter = 0;
    header.bmi.biYPelsPerMeter = 0;
    header.bmi.biClrUsed = 0;
    header.bmi.biClrImportant = 0;

    //	setup RGB bit masks
    if (sf->bpp == BPP_16) {
      header.red_mask = 0x7c00;
      header.green_mask = 0x03e0;
      header.blue_mask = 0x001f;
      bm->rowsize = nw * 2;
    } else if (sf->bpp == BPP_32 || sf->bpp == BPP_24) {
      header.red_mask = 0x00ff0000;
      header.green_mask = 0x0000ff00;
      header.blue_mask = 0x000000ff;
      bm->rowsize = nw * 4;
    }

    bm->hbm = CreateDIBSection(GDI_DATA(hOffscreenDC), (BITMAPINFO *)&header, DIB_RGB_COLORS, &bm->data, NULL, 0);
  }

  if (!bm->hbm) {
    ddgr_PushError("CreateDIBSection failed.");
    delete bm;
    return false;
  }
  bm->hwnd = NULL; // This is set by application

  sf->obj = (void *)bm;

  return true;
}

//	---------------------------------------------------------------------------
//	destroys the DIB allocated from create

void ddgr_gdi_surf_Destroy(ddgr_surface *sf) {
  tGDISurface *bm = (tGDISurface *)sf->obj;

  ASSERT(bm->hbm);

  DeleteObject(bm->hbm);

  delete bm;
}

//	Clears a GDI surface.
void ddgr_gdi_surf_Clear(ddgr_surface *dsf, ddgr_color col, int l, int t, int w, int h) {
  HBITMAP old_bmp;
  HBRUSH clr_brush, old_brush;
  HDC hdc;
  tGDISurface *dbm = (tGDISurface *)dsf->obj;
  int red, green, blue;
  ddgr_color color = col;

  switch (dsf->bpp) {
  case BPP_16:
    red = GR_COLOR_RED(color);
    green = GR_COLOR_GREEN(color);
    blue = GR_COLOR_BLUE(color);
    break;

  case BPP_32:
  case BPP_24:
    red = (color & 0x00ff0000) >> 16;
    green = (color & 0x0000ff00) >> 8;
    blue = (color & 0x000000ff);
    break;

  default:
    Int3(); // BAD
  }

  clr_brush = CreateSolidBrush(RGB((BYTE)red, (BYTE)green, (BYTE)blue));

  //	Are we clearing the primary display?
  if (dbm->hbm == NULL) {
    if (dbm->hwnd)
      hdc = GetDC(dbm->hwnd);
    else
      hdc = GetDC(GDI_DATA(hPrimaryWnd));
  } else {
    hdc = GDI_DATA(hOffscreenDC);
    old_bmp = (HBITMAP)SelectObject(hdc, dbm->hbm);
  }
  old_brush = (HBRUSH)SelectObject(hdc, clr_brush);

  Rectangle(hdc, l, t, l + w, t + h);

  SelectObject(hdc, old_brush);

  if (dbm->hbm == NULL) {
    // do if clearing was done on primary display.
    if (dbm->hwnd)
      ReleaseDC(dbm->hwnd, hdc);
    else
      ReleaseDC(GDI_DATA(hPrimaryWnd), hdc);
  } else {
    SelectObject(hdc, old_bmp);
  }

  DeleteObject(clr_brush);
}

//	---------------------------------------------------------------------------
//	blts one GDI bitmap to another.  Ability to blt directly to the screen is also
//	supported.

bool ddgr_gdi_surf_Blt(ddgr_surface *dsf, int dx, int dy, ddgr_surface *ssf, int sx, int sy, int sw, int sh) {
  HBITMAP old_bmp;
  HBITMAP old_bmp2;
  HDC hdc_dest;
  tGDISurface *dbm = (tGDISurface *)dsf->obj;
  tGDISurface *sbm = (tGDISurface *)ssf->obj;

  //	note that if our destination bitmap is the 'screen', then we use the GDI hwnd DC
  //	else we use the DC of the window specified in the destination.
  if (dbm->hbm == NULL) {
    if (dbm->hwnd == NULL)
      hdc_dest = GetDC(GDI_DATA(hPrimaryWnd));
    else
      hdc_dest = GetDC(dbm->hwnd);
  } else {
    hdc_dest = CreateCompatibleDC(NULL);
    old_bmp2 = (HBITMAP)SelectObject(hdc_dest, dbm->hbm);
  }

  old_bmp = (HBITMAP)SelectObject(GDI_DATA(hOffscreenDC), sbm->hbm);

  BOOL bltres = BitBlt(hdc_dest, dx, dy, sw, sh, GDI_DATA(hOffscreenDC), sx, sy, SRCCOPY);

  SelectObject(GDI_DATA(hOffscreenDC), old_bmp);

  if (dbm->hbm == NULL) {
    if (dbm->hwnd == NULL)
      ReleaseDC(GDI_DATA(hPrimaryWnd), hdc_dest);
    else
      ReleaseDC(dbm->hwnd, hdc_dest);
  } else {
    SelectObject(hdc_dest, old_bmp2);
    DeleteDC(hdc_dest);
  }

  return (bltres) ? true : false;
}

//	---------------------------------------------------------------------------
//	simply extracts the data and rowsize from the DDGR object in 'sf'

bool ddgr_gdi_surf_Lock(ddgr_surface *sf, void **ptr, int *rowsize) {
  tGDISurface *bm = (tGDISurface *)sf->obj;

  *ptr = bm->data;
  *rowsize = bm->rowsize;

  return true;
}

//	---------------------------------------------------------------------------
//	invalidates the data and rowsize in 'sf'

bool ddgr_gdi_surf_Unlock(ddgr_surface *sf, void *ptr) { return true; }

//	---------------------------------------------------------------------------
//	attaches a window handle to this surface. useful in clearing and blting to
//	screens.

void ddgr_gdi_surf_AttachHandle(ddgr_surface *sf, unsigned handle) {
  tGDISurface *gbm = (tGDISurface *)sf->obj;

  gbm->hwnd = (HWND)handle;
}
