/*
**  Polygon Doom software renderer
**  Copyright (c) 2016 Magnus Norddahl
**
**  This software is provided 'as-is', without any express or implied
**  warranty.  In no event will the authors be held liable for any damages
**  arising from the use of this software.
**
**  Permission is granted to anyone to use this software for any purpose,
**  including commercial applications, and to alter it and redistribute it
**  freely, subject to the following restrictions:
**
**  1. The origin of this software must not be misrepresented; you must not
**     claim that you wrote the original software. If you use this software
**     in a product, an acknowledgment in the product documentation would be
**     appreciated but is not required.
**  2. Altered source versions must be plainly marked as such, and must not be
**     misrepresented as being the original software.
**  3. This notice may not be removed or altered from any source distribution.
**
*/

#include <stdlib.h>
#include "templates.h"
#include "doomdef.h"
#include "sbar.h"
#include "r_data/r_translate.h"
#include "poly_decal.h"
#include "polyrenderer/poly_renderer.h"
#include "polyrenderer/scene/poly_light.h"
#include "polyrenderer/poly_renderthread.h"
#include "a_sharedglobal.h"
#include "swrenderer/scene/r_scene.h"

void RenderPolyDecal::RenderWallDecals(PolyRenderThread *thread, const seg_t *line, uint32_t stencilValue)
{
	if (line->linedef == nullptr && line->sidedef == nullptr)
		return;

	for (DBaseDecal *decal = line->sidedef->AttachedDecals; decal != nullptr; decal = decal->WallNext)
	{
		RenderPolyDecal render;
		render.Render(thread, decal, line, stencilValue);
	}
}

void RenderPolyDecal::Render(PolyRenderThread *thread, DBaseDecal *decal, const seg_t *line, uint32_t stencilValue)
{
	if (decal->RenderFlags & RF_INVISIBLE || !viewactive || !decal->PicNum.isValid())
		return;

	FTexture *tex = TexMan(decal->PicNum, true);
	if (tex == nullptr || tex->UseType == ETextureType::Null)
		return;

	sector_t *front, *back;
	GetDecalSectors(decal, line, &front, &back);

	// Calculate unclipped position and UV coordinates

	// decals should not use renderer specific offsets.
	double edge_left = tex->GetLeftOffset(0) * decal->ScaleX;
	double edge_right = (tex->GetWidth() - tex->GetLeftOffset(0)) * decal->ScaleX;	

	DVector2 angvec = (line->v2->fPos() - line->v1->fPos()).Unit();
	DVector2 normal = { angvec.Y, -angvec.X };

	double dcx, dcy;
	decal->GetXY(line->sidedef, dcx, dcy);
	DVector2 decal_pos = DVector2(dcx, dcy) + normal;
	DVector2 decal_left = decal_pos - edge_left * angvec;
	DVector2 decal_right = decal_pos + edge_right * angvec;

	bool flipTextureX = (decal->RenderFlags & RF_XFLIP) == RF_XFLIP;
	double u_left = flipTextureX ? 1.0 : 0.0;
	double u_right = flipTextureX ? 1.0 - tex->Scale.X : tex->Scale.X;
	double u_unit = (u_right - u_left) / (edge_left + edge_right);

	double zpos = GetDecalZ(decal, line, front, back);
	double spriteHeight = decal->ScaleY / tex->Scale.Y * tex->GetHeight();
	double ztop = zpos + spriteHeight - spriteHeight * 0.5;
	double zbottom = zpos - spriteHeight * 0.5;

	double v_top = 0.0;
	double v_bottom = tex->Scale.Y;
	double v_unit = (v_bottom - v_top) / (zbottom - ztop);

	// Clip decal to wall part

	double walltopz, wallbottomz;
	GetWallZ(decal, line, front, back, walltopz, wallbottomz);

	double clip_left_v1 = (decal_left - line->v1->fPos()) | angvec;
	double clip_right_v1 = (decal_right - line->v1->fPos()) | angvec;
	double clip_left_v2 = (decal_left - line->v2->fPos()) | angvec;
	double clip_right_v2 = (decal_right - line->v2->fPos()) | angvec;

	if ((clip_left_v1 <= 0.0 && clip_right_v1 <= 0.0) || (clip_left_v2 >= 0.0 && clip_right_v2 >= 0.0))
		return;

	if (clip_left_v1 < 0.0)
	{
		decal_left -= angvec * clip_left_v1;
		u_left -= u_unit * clip_left_v1;
	}
	if (clip_right_v1 < 0.0)
	{
		decal_right -= angvec * clip_right_v1;
		u_right -= u_unit * clip_right_v1;
	}
	if (clip_left_v2 > 0.0)
	{
		decal_left -= angvec * clip_left_v2;
		u_left -= u_unit * clip_left_v2;
	}
	if (clip_right_v2 > 0.0)
	{
		decal_right -= angvec * clip_right_v2;
		u_right -= u_unit * clip_right_v2;
	}

	double clip_top_floor = ztop - wallbottomz;
	double clip_bottom_floor = zbottom - wallbottomz;
	double clip_top_ceiling = ztop - walltopz;
	double clip_bottom_ceiling = zbottom - walltopz;

	if ((clip_top_floor <= 0.0 && clip_bottom_floor <= 0.0) || (clip_top_ceiling >= 0.0 && clip_bottom_ceiling >= 0.0))
		return;

	if (clip_top_floor < 0.0)
	{
		ztop -= clip_top_floor;
		v_top -= v_unit * clip_top_floor;
	}
	if (clip_bottom_floor < 0.0)
	{
		zbottom -= clip_bottom_floor;
		v_bottom -= v_unit * clip_bottom_floor;
	}
	if (clip_top_ceiling > 0.0)
	{
		ztop -= clip_top_ceiling;
		v_top -= v_unit * clip_top_ceiling;
	}
	if (clip_bottom_ceiling > 0.0)
	{
		zbottom -= clip_bottom_ceiling;
		v_bottom -= v_unit * clip_bottom_ceiling;
	}

	// Generate vertices for the decal

	TriVertex *vertices = thread->FrameMemory->AllocMemory<TriVertex>(4);
	vertices[0].x = (float)decal_left.X;
	vertices[0].y = (float)decal_left.Y;
	vertices[0].z = (float)ztop;
	vertices[0].w = 1.0f;
	vertices[0].u = (float)u_left;
	vertices[0].v = (float)v_top;
	vertices[1].x = (float)decal_right.X;
	vertices[1].y = (float)decal_right.Y;
	vertices[1].z = (float)ztop;
	vertices[1].w = 1.0f;
	vertices[1].u = (float)u_right;
	vertices[1].v = (float)v_top;
	vertices[2].x = (float)decal_right.X;
	vertices[2].y = (float)decal_right.Y;
	vertices[2].z = (float)zbottom;
	vertices[2].w = 1.0f;
	vertices[2].u = (float)u_right;
	vertices[2].v = (float)v_bottom;
	vertices[3].x = (float)decal_left.X;
	vertices[3].y = (float)decal_left.Y;
	vertices[3].z = (float)zbottom;
	vertices[3].w = 1.0f;
	vertices[3].u = (float)u_left;
	vertices[3].v = (float)v_bottom;

	// Light calculations

	bool foggy = false;
	int actualextralight = foggy ? 0 : PolyRenderer::Instance()->Viewpoint.extralight << 4;
	bool fullbrightSprite = (decal->RenderFlags & RF_FULLBRIGHT) == RF_FULLBRIGHT;
	int lightlevel = fullbrightSprite ? 255 : front->lightlevel + actualextralight;

	PolyDrawArgs args;
	args.SetLight(GetColorTable(front->Colormap), lightlevel, PolyRenderer::Instance()->Light.WallGlobVis(foggy), fullbrightSprite);
	args.SetColor(0xff000000 | decal->AlphaColor, decal->AlphaColor >> 24);
	args.SetStyle(decal->RenderStyle, decal->Alpha, decal->AlphaColor, decal->Translation, tex, false);
	args.SetStencilTestValue(stencilValue);
	args.SetDepthTest(true);
	args.SetWriteStencil(false);
	args.SetWriteDepth(false);
	args.DrawArray(thread->DrawQueue, vertices, 4, PolyDrawMode::TriangleFan);
}

void RenderPolyDecal::GetDecalSectors(DBaseDecal *decal, const seg_t *line, sector_t **front, sector_t **back)
{
	// for 3d-floor segments use the model sector as reference
	if ((decal->RenderFlags&RF_CLIPMASK) == RF_CLIPMID)
		*front = decal->Sector;
	else
		*front = line->frontsector;

	*back = (line->backsector != nullptr) ? line->backsector : line->frontsector;
}

double RenderPolyDecal::GetDecalZ(DBaseDecal *decal, const seg_t *line, sector_t *front, sector_t *back)
{
	switch (decal->RenderFlags & RF_RELMASK)
	{
	default:
		return decal->Z;
	case RF_RELUPPER:
		if (line->linedef->flags & ML_DONTPEGTOP)
			return decal->Z + front->GetPlaneTexZ(sector_t::ceiling);
		else
			return decal->Z + back->GetPlaneTexZ(sector_t::ceiling);
	case RF_RELLOWER:
		if (line->linedef->flags & ML_DONTPEGBOTTOM)
			return decal->Z + front->GetPlaneTexZ(sector_t::ceiling);
		else
			return decal->Z + back->GetPlaneTexZ(sector_t::floor);
		break;
	case RF_RELMID:
		if (line->linedef->flags & ML_DONTPEGBOTTOM)
			return decal->Z + front->GetPlaneTexZ(sector_t::floor);
		else
			return decal->Z + front->GetPlaneTexZ(sector_t::ceiling);
	}
}

void RenderPolyDecal::GetWallZ(DBaseDecal *decal, const seg_t *line, sector_t *front, sector_t *back, double &walltopz, double &wallbottomz)
{
	double frontceilz1 = front->ceilingplane.ZatPoint(line->v1);
	double frontfloorz1 = front->floorplane.ZatPoint(line->v1);
	double frontceilz2 = front->ceilingplane.ZatPoint(line->v2);
	double frontfloorz2 = front->floorplane.ZatPoint(line->v2);
	if (back == nullptr)
	{
		walltopz = MAX(frontceilz1, frontceilz2);
		wallbottomz = MIN(frontfloorz1, frontfloorz2);
	}
	else
	{
		double backceilz1 = back->ceilingplane.ZatPoint(line->v1);
		double backfloorz1 = back->floorplane.ZatPoint(line->v1);
		double backceilz2 = back->ceilingplane.ZatPoint(line->v2);
		double backfloorz2 = back->floorplane.ZatPoint(line->v2);
		double topceilz1 = frontceilz1;
		double topceilz2 = frontceilz2;
		double topfloorz1 = MAX(MIN(backceilz1, frontceilz1), frontfloorz1);
		double topfloorz2 = MAX(MIN(backceilz2, frontceilz2), frontfloorz2);
		double bottomceilz1 = MIN(MAX(frontfloorz1, backfloorz1), frontceilz1);
		double bottomceilz2 = MIN(MAX(frontfloorz2, backfloorz2), frontceilz2);
		double bottomfloorz1 = frontfloorz1;
		double bottomfloorz2 = frontfloorz2;
		double middleceilz1 = topfloorz1;
		double middleceilz2 = topfloorz2;
		double middlefloorz1 = MIN(bottomceilz1, middleceilz1);
		double middlefloorz2 = MIN(bottomceilz2, middleceilz2);

		switch (decal->RenderFlags & RF_RELMASK)
		{
		default:
			walltopz = MAX(frontceilz1, frontceilz2);
			wallbottomz = MIN(frontfloorz1, frontfloorz2);
			break;
		case RF_RELUPPER:
			walltopz = MAX(topceilz1, topceilz2);
			wallbottomz = MIN(topfloorz1, topfloorz2);
			break;
		case RF_RELLOWER:
			walltopz = MAX(bottomceilz1, bottomceilz2);
			wallbottomz = MIN(bottomfloorz1, bottomfloorz2);
			break;
		case RF_RELMID:
			walltopz = MAX(middleceilz1, middleceilz2);
			wallbottomz = MIN(middlefloorz1, middlefloorz2);
		}
	}
}
