// r_splitscreen.cpp -- see r_splitscreen.h.

#include "r_splitscreen.h"

#include <cmath>

#include "doomstat.h"
#include "d_player.h"
#include "d_main.h"
#include "i_time.h"
#include "v_video.h"
#include "v_draw.h"
#include "v_font.h"
#include "textures.h"
#include "gametexture.h"
#include "r_utility.h"
#include "scene/hw_drawinfo.h"
#include "hw_renderstate.h"
#include "flatvertices.h"

EXTERN_CVAR(Bool, ui_classic)

namespace
{
	struct WJSplitCell
	{
		FGameTexture *gameTex = nullptr;	// owns the wrapped FCanvasTexture (ref-counted)
		FCanvasTexture *canvasTex = nullptr;
		int width = 0, height = 0;
	};

	WJSplitCell wj_cells[MAXPLAYERS];

	void FreeCell(WJSplitCell &cell)
	{
		if (cell.gameTex != nullptr)
		{
			delete cell.gameTex;	// ref-counted Base releases the FCanvasTexture too
		}
		cell.gameTex = nullptr;
		cell.canvasTex = nullptr;
		cell.width = cell.height = 0;
	}

	void EnsureCell(int slot, int w, int h)
	{
		WJSplitCell &cell = wj_cells[slot];
		if (cell.canvasTex != nullptr && cell.width == w && cell.height == h)
			return;

		FreeCell(cell);

		cell.canvasTex = new FCanvasTexture(w, h);
		cell.gameTex = MakeGameTexture(cell.canvasTex, nullptr, ETextureType::Wall);
		cell.width = w;
		cell.height = h;
	}

	// Lay out `count` cells to fill a widthxheight area as evenly as
	// possible: pick the column count that keeps cells closest to the
	// area's own aspect ratio, then divide rows evenly under that.
	struct WJGridLayout
	{
		int cols, rows;
	};

	WJGridLayout ComputeGrid(int count, int width, int height)
	{
		if (count <= 1)
			return { 1, 1 };

		// Keep the grid itself close to square (cols ~= rows) rather than
		// trying to match each cell's aspect to the window's. Matching the
		// window aspect sounds right but isn't: for count=2 it picks a
		// 1-column, 2-row stack, giving cells over 3x wider than tall on a
		// normal widescreen window. The renderer's FOV/aspect math (mirrored
		// from the normal single-view path, itself only ever exercised
		// against sane window aspect ratios) isn't meant for cells that
		// extreme, and geometry starts vanishing at the edges. A square-ish
		// grid keeps every cell's aspect within shouting distance of the
		// window's own, which is what that math actually assumes -- and
		// incidentally matches how every real splitscreen game lays out 2
		// players (side by side), not this engine being unusual.
		int cols = (int)std::ceil(std::sqrt((double)count));
		int rows = (count + cols - 1) / cols;
		return { cols, rows };
	}

	// Player slots currently occupying a grid cell, in a stable order (by
	// slot index) so a given player doesn't jump cells frame to frame.
	int ActivePlayers(int *out)
	{
		int n = 0;
		for (int i = 0; i < MAXPLAYERS; i++)
		{
			if (playeringame[i] && players[i].mo != nullptr)
				out[n++] = i;
		}
		return n;
	}
}

bool WJ_SplitscreenActive()
{
	for (int i = 0; i < MAXPLAYERS; i++)
	{
		if (playeringame[i] && players[i].RemoteInputSlot)
			return true;
	}
	return false;
}

void WJ_UpdateSplitscreenViews()
{
	int active[MAXPLAYERS];
	int count = ActivePlayers(active);
	if (count == 0)
		return;

	// The normal single-view path (RenderView(player_t*) in hw_entrypoint.cpp)
	// resets this shared per-frame vertex buffer once at the top, and every
	// view rendered afterward that frame (camera textures, the main view)
	// accumulates into it. Since we replace that whole function, we must
	// do the same reset ourselves -- once per frame here, not once per
	// player -- or the buffer fills up over a handful of frames and the
	// renderer hits its "out of vertex memory" fatal error.
	screen->RenderState()->SetVertexBuffer(screen->mVertexData);
	screen->mVertexData->Reset();

	int fullWidth = screen->GetWidth();
	int fullHeight = screen->GetHeight();
	WJGridLayout grid = ComputeGrid(count, fullWidth, fullHeight);
	int cellW = fullWidth / grid.cols;
	int cellH = fullHeight / grid.rows;

	// hw_weapon.cpp's GetWeaponRect positions the weapon sprite using the
	// GLOBAL viewwidth/viewheight/viewwindowx/y -- the normal single-view
	// window size, which we never touch and which stays full-window-sized
	// throughout splitscreen play. Left alone, every cell's weapon gets
	// positioned as if it were rendering into the whole window, floating
	// wherever that math lands within our much smaller cell instead of
	// pinned to *this* cell's own bottom. The software renderer's
	// RenderViewToCanvas (r_scene.cpp) has the same requirement and saves/
	// overrides/restores these same globals around its own off-screen
	// render for exactly this reason -- this mirrors that.
	int savedViewwindowx = viewwindowx, savedViewwindowy = viewwindowy;
	int savedViewwidth = viewwidth, savedViewheight = viewheight;

	for (int n = 0; n < count; n++)
	{
		int slot = active[n];
		EnsureCell(slot, cellW, cellH);

		AActor *camera = players[slot].camera != nullptr ? players[slot].camera : players[slot].mo;
		if (camera == nullptr)
			continue;

		float fov = (float)camera->GetFOV(I_GetTimeFrac());
		float ratio = cellH > 0 ? float(cellW) / cellH : 1.f;
		float fovratio = (ratio >= 1.3f) ? 1.333333f : ratio;

		FCanvasTexture *canvasTex = wj_cells[slot].canvasTex;
		screen->RenderTextureView(canvasTex, [&](IntRect &bounds)
			{
				// Canvas textures are always their own self-contained
				// render target starting at (0,0) -- bounds.left/top are
				// 0 here, not this cell's position within the final
				// composited window (that offset is applied later, by
				// WJ_DrawSplitscreenGrid's ordinary 2D blit).
				viewwindowx = bounds.left;
				viewwindowy = bounds.top;
				viewwidth = bounds.width;
				viewheight = bounds.height;

				FRenderViewpoint texvp;
				// toscreen=true (despite this being an offscreen canvas
				// texture, not the window) is deliberate: it's what makes
				// ProcessScene prepare this player's weapon sprite --
				// see PreparePlayerSprites2D/3D in hw_weapon.cpp and the
				// mainview=false handling in RenderViewpoint. Without it
				// every cell renders the world but not the gun.
				RenderViewpoint(texvp, camera, &bounds, fov, ratio, fovratio, false, true);
			});
	}

	viewwindowx = savedViewwindowx;
	viewwindowy = savedViewwindowy;
	viewwidth = savedViewwidth;
	viewheight = savedViewheight;

	// The normal single-view path always finishes by rendering the main
	// view with bounds=NULL (RenderViewpoint(..., nullptr, ..., true,
	// true) in hw_entrypoint.cpp), which resets the GL viewport back to
	// the full window -- that's *why* the per-camera-texture viewport
	// changes above don't normally leak into the 2D/HUD pass that follows.
	// We replace that whole call, so nothing else does this reset for us;
	// without it the 2D pass inherits whichever player's cell-sized
	// viewport was set last, and everything (including this grid) draws
	// into that one small leftover rectangle instead of the full window.
	screen->SetViewportRects(nullptr);
}

void WJ_DrawSplitscreenGrid()
{
	int active[MAXPLAYERS];
	int count = ActivePlayers(active);
	if (count == 0)
		return;

	int fullWidth = twod->GetWidth();
	int fullHeight = twod->GetHeight();
	WJGridLayout grid = ComputeGrid(count, fullWidth, fullHeight);
	int cellW = fullWidth / grid.cols;
	int cellH = fullHeight / grid.rows;

	for (int n = 0; n < count; n++)
	{
		int slot = active[n];
		WJSplitCell &cell = wj_cells[slot];
		if (cell.gameTex == nullptr)
			continue;

		int col = n % grid.cols;
		int row = n / grid.cols;
		int x = col * cellW;
		int y = row * cellH;

		// TODO verify empirically whether canvas-texture content needs
		// DTA_FlipY here (FBO-rendered content is often Y-flipped relative
		// to a normal 2D draw) -- check a screenshot before shipping this.
		//
		// DTA_VirtualWidth/Height must be passed alongside DTA_DestWidth/
		// Height and set equal to the real canvas size: ParseDrawTextureTags
		// resolves an unset virtual size from parms->viewport, but that
		// field isn't populated until later in SetTextureParms, so leaving
		// it unset reads a not-yet-initialized rect and produces a wildly
		// wrong (tiny, mispositioned) destination rect.
		DrawTexture(twod, cell.gameTex, x, y,
			DTA_DestWidth, cellW, DTA_DestHeight, cellH,
			DTA_VirtualWidth, fullWidth, DTA_VirtualHeight, fullHeight,
			DTA_TopLeft, true,
			TAG_DONE);

		FFont *font = ui_classic ? SmallFont : NewSmallFont;
		int health = players[slot].mo != nullptr ? players[slot].mo->health : 0;
		FString label;
		label.Format("%s  %d", players[slot].userinfo.GetName(), health);
		DrawText(twod, font, CR_WHITE, x + 4, y + cellH - font->GetHeight() - 4, label.GetChars(), TAG_DONE);
	}
}
