// 
//---------------------------------------------------------------------------
//
// Copyright(C) 2003-2018 Christoph Oelckers
// All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program.  If not, see http://www.gnu.org/licenses/
//
//--------------------------------------------------------------------------
//
/*
**
** Draws the sky.  Loosely based on the JDoom sky and the ZDoomGL 0.66.2 sky.
**
** for FSkyVertexBuffer::SkyVertex only:
**---------------------------------------------------------------------------
** Copyright 2003 Tim Stump
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
#include "filesystem.h"
#include "cmdlib.h"
#include "bitmap.h"
#include "skyboxtexture.h"
#include "hw_material.h"
#include "hw_skydome.h"
#include "hw_renderstate.h"
#include "v_video.h"
#include "hwrenderer/data/buffers.h"
#include "version.h"

//-----------------------------------------------------------------------------
//
// Shamelessly lifted from Doomsday (written by Jaakko Keränen)
// also shamelessly lifted from ZDoomGL! ;)
//
//-----------------------------------------------------------------------------
CVAR(Float, skyoffset, 0.f, 0)	// for testing


struct SkyColor
{
	FTextureID Texture;
	std::pair<PalEntry, PalEntry> Colors;
};

static TArray<SkyColor> SkyColors;

std::pair<PalEntry, PalEntry>& R_GetSkyCapColor(FGameTexture* tex)
{
	for (auto& sky : SkyColors)
	{
		if (sky.Texture == tex->GetID()) return sky.Colors;
	}

	auto itex = tex->GetTexture();
	SkyColor sky;

	FBitmap bitmap = itex->GetBgraBitmap(nullptr);
	int w = bitmap.GetWidth();
	int h = bitmap.GetHeight();

	const uint32_t* buffer = (const uint32_t*)bitmap.GetPixels();
	if (buffer)
	{
		sky.Colors.first = averageColor((uint32_t*)buffer, w * min(30, h), 0);
		if (h > 30)
		{
			sky.Colors.second = averageColor(((uint32_t*)buffer) + (h - 30) * w, w * 30, 0);
		}
		else sky.Colors.second = sky.Colors.first;
	}
	sky.Texture = tex->GetID();
	SkyColors.Push(sky);

	return SkyColors.Last().Colors;
}



//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

FSkyVertexBuffer::FSkyVertexBuffer()
{
	CreateDome();
	mVertexBuffer = screen->CreateVertexBuffer();

	static const FVertexBufferAttribute format[] = {
		{ 0, VATTR_VERTEX, VFmt_Float3, (int)myoffsetof(FSkyVertex, x) },
		{ 0, VATTR_TEXCOORD, VFmt_Float2, (int)myoffsetof(FSkyVertex, u) },
		{ 0, VATTR_COLOR, VFmt_Byte4, (int)myoffsetof(FSkyVertex, color) },
		{ 0, VATTR_LIGHTMAP, VFmt_Float3, (int)myoffsetof(FSkyVertex, lu) },
	};
	mVertexBuffer->SetFormat(1, 4, sizeof(FSkyVertex), format);
	mVertexBuffer->SetData(mVertices.Size() * sizeof(FSkyVertex), &mVertices[0], BufferUsageType::Static);
}

FSkyVertexBuffer::~FSkyVertexBuffer()
{
	delete mVertexBuffer;
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::SkyVertexDoom(int r, int c, bool zflip)
{
	static const FAngle maxSideAngle = FAngle::fromDeg(60.f);
	static const float scale = 10000.;

	FAngle topAngle = FAngle::fromDeg((c / (float)mColumns * 360.f));
	FAngle sideAngle = maxSideAngle * float(mRows - r) / float(mRows);
	float height = sideAngle.Sin();
	float realRadius = scale * sideAngle.Cos();
	FVector2 pos = topAngle.ToVector(realRadius);
	float z = (!zflip) ? scale * height : -scale * height;

	FSkyVertex vert;

	vert.color = r == 0 ? 0xffffff : 0xffffffff;

	// And the texture coordinates.
	if (!zflip)	// Flipped Y is for the lower hemisphere.
	{
		vert.u = (-c / (float)mColumns);
		vert.v = (r / (float)mRows);
	}
	else
	{
		vert.u = (-c / (float)mColumns);
		vert.v = 1.0f + ((mRows - r) / (float)mRows);
	}

	if (r != 4) z += 300;
	// And finally the vertex.
	vert.x = -pos.X;	// Doom mirrors the sky vertically!
	vert.y = z - 1.f;
	vert.z = pos.Y;

	mVertices.Push(vert);
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::SkyVertexBuild(int r, int c, bool zflip)
{
	static const FAngle maxSideAngle = FAngle::fromDeg(60.f);
	static const float scale = 10000.;

	FAngle topAngle = FAngle::fromDeg((c / (float)mColumns * 360.f));
	FVector2 pos = topAngle.ToVector(scale);
	float z = (!zflip) ? (mRows - r) * 4000.f : -(mRows - r) * 4000.f;

	FSkyVertex vert;

	vert.color = r == 0 ? 0xffffff : 0xffffffff;

	// And the texture coordinates.
	if (zflip) r = mRows * 2 - r;
	vert.u = 0.5f + (-c / (float)mColumns);
	vert.v = (r / (float)(2*mRows));

	// And finally the vertex.
	vert.x = pos.X;
	vert.y = z - 1.f;
	vert.z = pos.Y;

	mVertices.Push(vert);
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::CreateSkyHemisphereDoom(int hemi)
{
	int r, c;
	bool zflip = !!(hemi & SKYHEMI_LOWER);

	mPrimStartDoom.Push(mVertices.Size());

	for (c = 0; c < mColumns; c++)
	{
		SkyVertexDoom(1, c, zflip);
	}

	// The total number of triangles per hemisphere can be calculated
	// as follows: rows * columns * 2 + 2 (for the top cap).
	for (r = 0; r < mRows; r++)
	{
		mPrimStartDoom.Push(mVertices.Size());
		for (c = 0; c <= mColumns; c++)
		{
			SkyVertexDoom(r + zflip, c, zflip);
			SkyVertexDoom(r + 1 - zflip, c, zflip);
		}
	}
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::CreateSkyHemisphereBuild(int hemi)
{
	int r, c;
	bool zflip = !!(hemi & SKYHEMI_LOWER);

	mPrimStartBuild.Push(mVertices.Size());

	for (c = 0; c < mColumns; c++)
	{
		SkyVertexBuild(1, c, zflip);
	}

	// The total number of triangles per hemisphere can be calculated
	// as follows: rows * columns * 2 + 2 (for the top cap).
	for (r = 0; r < mRows; r++)
	{
		mPrimStartBuild.Push(mVertices.Size());
		for (c = 0; c <= mColumns; c++)
		{
			SkyVertexBuild(r + zflip, c, zflip);
			SkyVertexBuild(r + 1 - zflip, c, zflip);
		}
	}
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::CreateDome()
{
	// the first thing we put into the buffer is the fog layer object which is just 4 triangles around the viewpoint.

	mVertices.Reserve(12);
	mVertices[0].Set(1.0f, 1.0f, -1.0f);
	mVertices[1].Set(1.0f, -1.0f, -1.0f);
	mVertices[2].Set(-1.0f, 0.0f, -1.0f);

	mVertices[3].Set(1.0f, 1.0f, -1.0f);
	mVertices[4].Set(1.0f, -1.0f, -1.0f);
	mVertices[5].Set(0.0f, 0.0f, 1.0f);

	mVertices[6].Set(-1.0f, 0.0f, -1.0f);
	mVertices[7].Set(1.0f, 1.0f, -1.0f);
	mVertices[8].Set(0.0f, 0.0f, 1.0f);

	mVertices[9].Set(1.0f, -1.0f, -1.0f);
	mVertices[10].Set(-1.0f, 0.0f, -1.0f);
	mVertices[11].Set(0.0f, 0.0f, 1.0f);

	mColumns = 128;
	mRows = 4;
	CreateSkyHemisphereDoom(SKYHEMI_UPPER);
	CreateSkyHemisphereDoom(SKYHEMI_LOWER);
	mPrimStartDoom.Push(mVertices.Size());

	CreateSkyHemisphereBuild(SKYHEMI_UPPER);
	CreateSkyHemisphereBuild(SKYHEMI_LOWER);
	mPrimStartBuild.Push(mVertices.Size());

	mSideStart = mVertices.Size();
	mFaceStart[0] = mSideStart + 10;
	mFaceStart[1] = mFaceStart[0] + 4;
	mFaceStart[2] = mFaceStart[1] + 4;
	mFaceStart[3] = mFaceStart[2] + 4;
	mFaceStart[4] = mFaceStart[3] + 4;
	mFaceStart[5] = mFaceStart[4] + 4;
	mFaceStart[6] = mFaceStart[5] + 4;
	mVertices.Reserve(10 + 7*4);
	FSkyVertex *ptr = &mVertices[mSideStart];

	// all sides
	ptr[0].SetXYZ(128.f, 128.f, -128.f, 0, 0);
	ptr[1].SetXYZ(128.f, -128.f, -128.f, 0, 1);
	ptr[2].SetXYZ(-128.f, 128.f, -128.f, 0.25f, 0);
	ptr[3].SetXYZ(-128.f, -128.f, -128.f, 0.25f, 1);
	ptr[4].SetXYZ(-128.f, 128.f, 128.f, 0.5f, 0);
	ptr[5].SetXYZ(-128.f, -128.f, 128.f, 0.5f, 1);
	ptr[6].SetXYZ(128.f, 128.f, 128.f, 0.75f, 0);
	ptr[7].SetXYZ(128.f, -128.f, 128.f, 0.75f, 1);
	ptr[8].SetXYZ(128.f, 128.f, -128.f, 1, 0);
	ptr[9].SetXYZ(128.f, -128.f, -128.f, 1, 1);

	// north face
	ptr[10].SetXYZ(128.f, 128.f, -128.f, 0, 0);
	ptr[11].SetXYZ(-128.f, 128.f, -128.f, 1, 0);
	ptr[12].SetXYZ(128.f, -128.f, -128.f, 0, 1);
	ptr[13].SetXYZ(-128.f, -128.f, -128.f, 1, 1);

	// east face
	ptr[14].SetXYZ(-128.f, 128.f, -128.f, 0, 0);
	ptr[15].SetXYZ(-128.f, 128.f, 128.f, 1, 0);
	ptr[16].SetXYZ(-128.f, -128.f, -128.f, 0, 1);
	ptr[17].SetXYZ(-128.f, -128.f, 128.f, 1, 1);

	// south face
	ptr[18].SetXYZ(-128.f, 128.f, 128.f, 0, 0);
	ptr[19].SetXYZ(128.f, 128.f, 128.f, 1, 0);
	ptr[20].SetXYZ(-128.f, -128.f, 128.f, 0, 1);
	ptr[21].SetXYZ(128.f, -128.f, 128.f, 1, 1);

	// west face
	ptr[22].SetXYZ(128.f, 128.f, 128.f, 0, 0);
	ptr[23].SetXYZ(128.f, 128.f, -128.f, 1, 0);
	ptr[24].SetXYZ(128.f, -128.f, 128.f, 0, 1);
	ptr[25].SetXYZ(128.f, -128.f, -128.f, 1, 1);

	// bottom face
	ptr[26].SetXYZ(128.f, -128.f, -128.f, 0, 0);
	ptr[27].SetXYZ(-128.f, -128.f, -128.f, 1, 0);
	ptr[28].SetXYZ(128.f, -128.f, 128.f, 0, 1);
	ptr[29].SetXYZ(-128.f, -128.f, 128.f, 1, 1);

	// top face
	ptr[30].SetXYZ(128.f, 128.f, -128.f, 0, 0);
	ptr[31].SetXYZ(-128.f, 128.f, -128.f, 1, 0);
	ptr[32].SetXYZ(128.f, 128.f, 128.f, 0, 1);
	ptr[33].SetXYZ(-128.f, 128.f, 128.f, 1, 1);

	// top face flipped
	ptr[34].SetXYZ(128.f, 128.f, -128.f, 0, 1);
	ptr[35].SetXYZ(-128.f, 128.f, -128.f, 1, 1);
	ptr[36].SetXYZ(128.f, 128.f, 128.f, 0, 0);
	ptr[37].SetXYZ(-128.f, 128.f, 128.f, 1, 0);
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::SetupMatrices(FGameTexture *tex, float x_offset, float y_offset, bool mirror, int mode, VSMatrix &modelMatrix, VSMatrix &textureMatrix, bool tiled, float xscale, float yscale)
{
	float texw = tex->GetDisplayWidth();
	float texh = tex->GetDisplayHeight();

	modelMatrix.loadIdentity();
	modelMatrix.rotate(-180.0f + x_offset, 0.f, 1.f, 0.f);

	if (xscale == 0) xscale = texw < 1024.f ? floorf(1024.f / float(texw)) : 1.f;
	auto texskyoffset = tex->GetSkyOffset() + skyoffset;
	if (yscale == 0)
	{
		if (texh <= 128 && tiled)
		{
			modelMatrix.translate(0.f, (-40 + texskyoffset) * skyoffsetfactor, 0.f);
			modelMatrix.scale(1.f, 1.2f * 1.17f, 1.f);
			yscale = 240.f / texh;
		}
		else if (texh < 128)
		{
			// smaller sky textures must be tiled. We restrict it to 128 sky pixels, though
			modelMatrix.translate(0.f, -1250.f, 0.f);
			modelMatrix.scale(1.f, 128 / 230.f, 1.f);
			yscale = float(128 / texh);	// intentionally left as integer.
		}
		else if (texh < 200)
		{
			modelMatrix.translate(0.f, -1250.f, 0.f);
			modelMatrix.scale(1.f, texh / 230.f, 1.f);
			yscale = 1.f;
		}
		else if (texh <= 240)
		{
			modelMatrix.translate(0.f, (200 - texh + texskyoffset) * skyoffsetfactor, 0.f);
			modelMatrix.scale(1.f, 1.f + ((texh - 200.f) / 200.f) * 1.17f, 1.f);
			yscale = 1.f;
		}
		else
		{
			modelMatrix.translate(0.f, (-40 + texskyoffset) * skyoffsetfactor, 0.f);
			modelMatrix.scale(1.f, 1.2f * 1.17f, 1.f);
			yscale = 240.f / texh;
		}
	}
	else
	{
		modelMatrix.translate(0.f, (-40 + texskyoffset) * skyoffsetfactor, 0.f);
		modelMatrix.scale(1.f, 0.8f * 1.17f, 1.f);
	}
	textureMatrix.loadIdentity();
	textureMatrix.scale(mirror ? -xscale : xscale, yscale, 1.f);
	textureMatrix.translate(1.f, y_offset / texh, 1.f);
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::RenderRow(FRenderState& state, EDrawType prim, int row, TArray<unsigned int>& mPrimStart, bool apply)
{
	state.Draw(prim, mPrimStart[row], mPrimStart[row + 1] - mPrimStart[row]);
}

//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::DoRenderDome(FRenderState& state, FGameTexture* tex, int mode, bool which, PalEntry color)
{
	auto& primStart = which ? mPrimStartBuild : mPrimStartDoom;
	if (tex && tex->isValid())
	{
		state.SetMaterial(tex, UF_Texture, 0, CLAMP_NONE, 0, -1);
		state.EnableModelMatrix(true);
		state.EnableTextureMatrix(true);
	}

	int rc = mRows + 1;

	// The caps only get drawn for the main layer but not for the overlay.
	if (mode == FSkyVertexBuffer::SKYMODE_MAINLAYER && tex != nullptr)
	{
		auto col = R_GetSkyCapColor(tex);

		col.first.r = col.first.r * color.r / 255;
		col.first.g = col.first.g * color.g / 255;
		col.first.b = col.first.b * color.b / 255;
		col.second.r = col.second.r * color.r / 255;
		col.second.g = col.second.g * color.g / 255;
		col.second.b = col.second.b * color.b / 255;

		state.SetObjectColor(col.first);
		state.EnableTexture(false);
		RenderRow(state, DT_TriangleFan, 0, primStart);

		state.SetObjectColor(col.second);
		RenderRow(state, DT_TriangleFan, rc, primStart);
		state.EnableTexture(true);
	}
	state.SetObjectColor(0xffffffff);
	for (int i = 1; i <= mRows; i++)
	{
		RenderRow(state, DT_TriangleStrip, i, primStart, i == 1);
		RenderRow(state, DT_TriangleStrip, rc + i, primStart, false);
	}

	state.EnableTextureMatrix(false);
	state.EnableModelMatrix(false);
}


//-----------------------------------------------------------------------------
//
// This is only for Doom-style skies.
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::RenderDome(FRenderState& state, FGameTexture* tex, float x_offset, float y_offset, bool mirror, int mode, bool tiled, float xscale, float yscale, PalEntry color)
{
	if (tex)
	{
		SetupMatrices(tex, x_offset, y_offset, mirror, mode, state.mModelMatrix, state.mTextureMatrix, tiled, xscale, yscale);
	}
	DoRenderDome(state, tex, mode, false, color);
}


//-----------------------------------------------------------------------------
//
//
//
//-----------------------------------------------------------------------------

void FSkyVertexBuffer::RenderBox(FRenderState& state, FSkyBox* tex, float x_offset, bool sky2, float stretch, const FVector3& skyrotatevector, const FVector3& skyrotatevector2, PalEntry color)
{
	int faces;

	state.SetObjectColor(color);
	state.EnableModelMatrix(true);
	state.mModelMatrix.loadIdentity();
	state.mModelMatrix.scale(1, 1 / stretch, 1); // Undo the map's vertical scaling as skyboxes are true cubes.

	if (!sky2)
		state.mModelMatrix.rotate(-180.0f + x_offset, skyrotatevector.X, skyrotatevector.Z, skyrotatevector.Y);
	else
		state.mModelMatrix.rotate(-180.0f + x_offset, skyrotatevector2.X, skyrotatevector2.Z, skyrotatevector2.Y);

	if (tex->GetSkyFace(5))
	{
		faces = 4;

		// north
		state.SetMaterial(tex->GetSkyFace(0), UF_Texture, 0, CLAMP_XY, 0, -1);
		state.Draw(DT_TriangleStrip, FaceStart(0), 4);

		// east
		state.SetMaterial(tex->GetSkyFace(1), UF_Texture, 0, CLAMP_XY, 0, -1);
		state.Draw(DT_TriangleStrip, FaceStart(1), 4);

		// south
		state.SetMaterial(tex->GetSkyFace(2), UF_Texture, 0, CLAMP_XY, 0, -1);
		state.Draw(DT_TriangleStrip, FaceStart(2), 4);

		// west
		state.SetMaterial(tex->GetSkyFace(3), UF_Texture, 0, CLAMP_XY, 0, -1);
		state.Draw(DT_TriangleStrip, FaceStart(3), 4);
	}
	else
	{
		faces = 1;
		state.SetMaterial(tex->GetSkyFace(0), UF_Texture, 0, CLAMP_XY, 0, -1);
		state.Draw(DT_TriangleStrip, FaceStart(-1), 10);
	}

	// top
	state.SetMaterial(tex->GetSkyFace(faces), UF_Texture, 0, CLAMP_XY, 0, -1);
	state.Draw(DT_TriangleStrip, FaceStart(tex->GetSkyFlip() ? 6 : 5), 4);

	// bottom
	state.SetMaterial(tex->GetSkyFace(faces + 1), UF_Texture, 0, CLAMP_XY, 0, -1);
	state.Draw(DT_TriangleStrip, FaceStart(4), 4);

	state.EnableModelMatrix(false);
	state.SetObjectColor(0xffffffff);
}

