// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file  r_portal.c
/// \brief Software renderer portals.

#include "r_portal.h"
#include "r_plane.h"
#include "r_main.h"
#include "doomstat.h"
#include "p_spec.h" // Skybox viewpoints
#include "z_zone.h"
#include "r_things.h"
#include "r_sky.h"

UINT8 portalrender; /**< When rendering a portal, it establishes the depth of the current BSP traversal. */

// Linked list for portals.
portal_t *portal_base, *portal_cap;

line_t *portalclipline;
sector_t *portalcullsector;
INT32 portalclipstart, portalclipend;

portal_t *g_portal; // is curline a portal seg?

void Portal_InitList(void)
{
	portalrender = 0;
	portal_base = portal_cap = NULL;
}

/** Store the clipping window for a portal in its given range.
 *
 * The window is copied from the current window at the time
 * the function is called, so it is useful for converting one-sided
 * lines into portals.
 */
void Portal_ClipRange(portal_t* portal)
{
	INT32 i;
	INT32 start     = portal->start;
	INT32 end       = portal->end;
	INT16 *ceil     = portal->ceilingclip;
	INT16 *floor    = portal->floorclip;
	fixed_t *scale  = portal->frontscale;

	for (i = 0; i < end-start; i++)
	{
		*ceil = ceilingclip[start+i];
		ceil++;
		*floor = floorclip[start+i];
		floor++;
		*scale = frontscale[start+i];
		scale++;
	}
}

/** Apply the clipping window from a portal.
 */
void Portal_ClipApply(const portal_t* portal)
{
	INT32 i;
	INT32 start     = portal->start;
	INT32 end       = portal->end;
	INT16 *ceil     = portal->ceilingclip;
	INT16 *floor    = portal->floorclip;
	fixed_t *scale  = portal->frontscale;

	for (i = 0; i < end-start; i++)
	{
		ceilingclip[start+i] = *ceil;
		ceil++;
		floorclip[start+i] = *floor;
		floor++;
		frontscale[start+i] = *scale;
		scale++;
	}

	// HACKS FOLLOW
	for (i = 0; i < start; i++)
	{
		floorclip[i] = -1;
		ceilingclip[i] = (INT16)viewheight;
	}
	for (i = end; i < vid.width; i++)
	{
		floorclip[i] = -1;
		ceilingclip[i] = (INT16)viewheight;
	}
}

static portal_t* Portal_Add(const INT16 x1, const INT16 x2)
{
	portal_t *portal          = Z_Malloc(sizeof(portal_t), PU_LEVEL, NULL);
	INT16    *ceilingclipsave = Z_Malloc(sizeof(INT16)*(x2-x1 + 1), PU_LEVEL, NULL);
	INT16    *floorclipsave   = Z_Malloc(sizeof(INT16)*(x2-x1 + 1), PU_LEVEL, NULL);
	fixed_t  *frontscalesave  = Z_Malloc(sizeof(fixed_t)*(x2-x1 + 1), PU_LEVEL, NULL);

	// Linked list.
	if (!portal_base)
	{
		portal_base	= portal;
		portal_cap	= portal;
	}
	else
	{
		portal_cap->next = portal;
		portal_cap = portal;
	}
	portal->next = NULL;

	// Store clipping values so they can be restored once the portal is rendered.
	portal->ceilingclip = ceilingclipsave;
	portal->floorclip   = floorclipsave;
	portal->frontscale  = frontscalesave;
	portal->start       = x1;
	portal->end         = x2;

	// Increase recursion level.
	portal->pass = portalrender+1;

	portal->isskybox = false;

	return portal;
}

void Portal_Remove(portal_t* portal)
{
	portalcullsector = NULL;
	portal_base = portal->next;
	Z_Free(portal->ceilingclip);
	Z_Free(portal->floorclip);
	Z_Free(portal->frontscale);
	Z_Free(portal);
}

/** Creates a portal out of two lines and a determined screen range.
 *
 * line1 determines the entrance, and line2 the exit.
 * x1 and x2 determine the screen's column bounds.

 * The view's offset from the entry line center is obtained,
 * and then rotated&translated to the exit line's center.
 * When the portal renders, it will create the illusion of
 * the two lines being seamed together.
 */
void Portal_Add2Lines(const INT32 line1, const INT32 line2, const INT32 x1, const INT32 x2)
{
	portal_t* portal = Portal_Add(x1, x2);

	// Offset the portal view by the linedef centers
	line_t* start = &lines[line1];
	line_t* dest  = &lines[line2];

	angle_t dangle = R_PointToAngle2(0,0,dest->dx,dest->dy) - R_PointToAngle2(start->dx,start->dy,0,0);

	fixed_t disttopoint;
	angle_t angtopoint;

	vertex_t dest_c, start_c;

	// looking glass center
	start_c.x = (start->v1->x + start->v2->x) / 2;
	start_c.y = (start->v1->y + start->v2->y) / 2;

	// other side center
	dest_c.x = (dest->v1->x + dest->v2->x) / 2;
	dest_c.y = (dest->v1->y + dest->v2->y) / 2;

	if ((mapnamespace == MNS_SRB2KART) && dangle == 0)
	{
		// the entrance goes straight opposite the exit, so we just need to mess with the offset.
		portal->viewx = viewx + (dest_c.x - start_c.x);
		portal->viewy = viewy + (dest_c.y - start_c.y);
	}
	else
	{
		disttopoint   = R_PointToDist2(start_c.x, start_c.y, viewx, viewy);
		angtopoint    = R_PointToAngle2(start_c.x, start_c.y, viewx, viewy);
		angtopoint   += dangle;

		portal->viewx = dest_c.x + FixedMul(FINECOSINE(angtopoint>>ANGLETOFINESHIFT), disttopoint);
		portal->viewy = dest_c.y + FixedMul(FINESINE(angtopoint>>ANGLETOFINESHIFT), disttopoint);
	}

	portal->viewz = viewz + (dest->frontsector->floorheight - start->frontsector->floorheight);

	portal->viewangle = viewangle + dangle;

	portal->clipline = line2;

	if (mapnamespace == MNS_SRB2KART)
		Portal_ClipRange(portal);

	g_portal = portal; // this tells R_StoreWallRange that curline is a portal seg
}

/** Store the clipping window for a portal using a visplane.
 *
 * Since visplanes top/bottom windows work in an identical way,
 * it can just be copied almost directly.
 */
static void Portal_ClipVisplane(const visplane_t* plane, portal_t* portal)
{
	INT32 i;
	INT16 start = portal->start;
	INT16 end   = portal->end;

	for (i = 0; i < end - start; i++)
	{
		// Invalid column.
		if (plane->top[i + start] == UINT16_MAX)
		{
			portal->ceilingclip[i] = -1;
			portal->floorclip[i]   = -1;
			continue;
		}

		portal->ceilingclip[i] = plane->top[i + start] - 1;
		portal->floorclip[i]   = plane->bottom[i + start] + 1;
		portal->frontscale[i]  = INT32_MAX;
	}
}

extern INT32 viewwidth;

static boolean TrimVisplaneBounds(const visplane_t* plane, INT16* start, INT16* end)
{
	*start = plane->minx;
	*end = plane->maxx + 1;

	// Visplanes have 1-px pads on their sides (extra columns).
	// Trim them, else it may render out of bounds.
	if (*end > viewwidth)
		*end = viewwidth;

	if (!(*start < *end))
		return true;

	/** Trims a visplane's horizontal gap to match its render area.
	 *
	 * Visplanes' minx/maxx may sometimes exceed the area they're
	 * covering. This merely adjusts the boundaries to the next
	 * valid area.
	 */

	while (plane->bottom[*start] == 0 && plane->top[*start] == UINT16_MAX && *start < *end)
	{
		(*start)++;
	}

	while (plane->bottom[*end - 1] == 0 && plane->top[*start] == UINT16_MAX && *end > *start)
	{
		(*end)--;
	}

	return false;
}

/** Creates a skybox portal out of a visplane.
 *
 * Applies the necessary offsets and rotation to give
 * a depth illusion to the skybox.
 */
void Portal_AddSkybox(const visplane_t* plane)
{
	INT16 start, end;
	mapheader_t *mh;
	portal_t* portal;

	if (TrimVisplaneBounds(plane, &start, &end))
		return;

	portal = Portal_Add(start, end);

	Portal_ClipVisplane(plane, portal);

	portal->viewx     = skyboxmo[0]->x;
	portal->viewy     = skyboxmo[0]->y;
	portal->viewz     = skyboxmo[0]->z;
	portal->viewangle = viewangle + skyboxmo[0]->angle;

	mh = mapheaderinfo[gamemap-1];

	// If a relative viewpoint exists, offset the viewpoint.
	if (skyboxmo[1])
	{
		fixed_t x = 0, y = 0;
		angle_t ang = skyboxmo[0]->angle>>ANGLETOFINESHIFT;

		if (mh->skybox_scalex > 0)
			x = (viewx - skyboxmo[1]->x) / mh->skybox_scalex;
		else if (mh->skybox_scalex < 0)
			x = (viewx - skyboxmo[1]->x) * -mh->skybox_scalex;

		if (mh->skybox_scaley > 0)
			y = (viewy - skyboxmo[1]->y) / mh->skybox_scaley;
		else if (mh->skybox_scaley < 0)
			y = (viewy - skyboxmo[1]->y) * -mh->skybox_scaley;

		// Apply transform to account for the skybox viewport angle.
		portal->viewx += FixedMul(x,FINECOSINE(ang)) - FixedMul(y,  FINESINE(ang));
		portal->viewy += FixedMul(x,  FINESINE(ang)) + FixedMul(y,FINECOSINE(ang));
	}

	if (mh->skybox_scalez > 0)
		portal->viewz += viewz / mh->skybox_scalez;
	else if (mh->skybox_scalez < 0)
		portal->viewz += viewz * -mh->skybox_scalez;

	portal->clipline = -1;
	portal->isskybox = true;
}

/** Creates portals for the currently existing sky visplanes.
 * The visplanes are also removed and cleared from the list.
 */
void Portal_AddSkyboxPortals(void)
{
	visplane_t *pl;
	INT32 i;
	UINT16 count = 0;

	for (i = 0; i < MAXVISPLANES; i++, pl++)
	{
		for (pl = visplanes[i]; pl; pl = pl->next)
		{
			if (pl->picnum != skyflatnum || pl->damage)
				continue;

			Portal_AddSkybox(pl);

			pl->minx = 0;
			pl->maxx = -1;

			count++;
		}
	}

	g_renderstats.skybox_portals = count;
}
