#version 450
#pragma shader_stage(compute)

//#extension GL_EXT_nonuniform_qualifier : enable
#extension GL_EXT_samplerless_texture_functions : enable

layout (constant_id = 0) const uint PASSES = 6;
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

#define COMPUTE 1
#define QUERY_MIPMAPS 1
#define DEPTH_BIAS 0.00005

#include "../../common/macros.h"
#include "../../common/structs.h"

float mipLevels( vec2 size ) {
	return floor(log2(max(size.x, size.y)));
}
float mipLevels( ivec2 size ) {
	return floor(log2(max(size.x, size.y)));
}

vec4 aabbToSphere( Bounds bounds ) {
	vec4 sphere;
	sphere.xyz = (bounds.max + bounds.min) * 0.5;
	sphere.w = length((bounds.max - bounds.min) * 0.5);
	return sphere;
}

// 2D Polyhedral Bounds of a Clipped, Perspective-Projected 3D Sphere. Michael Mara, Morgan McGuire. 2013
bool projectSphere(vec3 C, float r, float znear, float P00, float P11, out vec4 aabb)
{
	if (C.z < r + znear)
		return false;

	vec2 cx = -C.xz;
	vec2 vx = vec2(sqrt(dot(cx, cx) - r * r), r);
	vec2 minx = mat2(vx.x, vx.y, -vx.y, vx.x) * cx;
	vec2 maxx = mat2(vx.x, -vx.y, vx.y, vx.x) * cx;

	vec2 cy = -C.yz;
	vec2 vy = vec2(sqrt(dot(cy, cy) - r * r), r);
	vec2 miny = mat2(vy.x, vy.y, -vy.y, vy.x) * cy;
	vec2 maxy = mat2(vy.x, -vy.y, vy.y, vy.x) * cy;

	aabb = vec4(minx.x / minx.y * P00, miny.x / miny.y * P11, maxx.x / maxx.y * P00, maxy.x / maxy.y * P11);
	aabb = aabb.xwzy * vec4(0.5f, -0.5f, 0.5f, -0.5f) + vec4(0.5f); // clip space -> uv space

	return true;
}

layout( push_constant ) uniform PushBlock {
  uint pass;
  uint passes;
} PushConstant;

layout (binding = 0) uniform Camera {
	Viewport viewport[PASSES];
} camera;

layout (std140, binding = 1) buffer DrawCommands {
	DrawCommand drawCommands[];
};

layout (std140, binding = 2) buffer Instances {
	Instance instances[];
};

layout (binding = 3) uniform sampler2D samplerDepth;

struct Frustum {
	vec4 planes[6];
};

vec4 normalizePlane( vec4 p ) {
	return p / length(p.xyz);
}

bool frustumCull( uint id ) {
	if ( PushConstant.passes == 0 ) return true;
	
	const DrawCommand drawCommand = drawCommands[id];
	const Instance instance = instances[drawCommand.instanceID];

	bool visible = false;
	for ( uint pass = 0; pass < PushConstant.passes; ++pass ) {
#if 0
		vec4 sphere = aabbToSphere( instance.bounds );
		vec3 center = vec3( camera.viewport[pass].view * instance.model * vec4(  ) );
#else
		mat4 mat = camera.viewport[pass].projection * camera.viewport[pass].view * instance.model;
	#if 1
		vec4 planes[6]; {
			for (int i = 0; i < 3; ++i)
			for (int j = 0; j < 2; ++j) {
				planes[i*2+j].x = mat[0][3] + (j == 0 ? mat[0][i] : -mat[0][i]);
				planes[i*2+j].y = mat[1][3] + (j == 0 ? mat[1][i] : -mat[1][i]);
				planes[i*2+j].z = mat[2][3] + (j == 0 ? mat[2][i] : -mat[2][i]);
				planes[i*2+j].w = mat[3][3] + (j == 0 ? mat[3][i] : -mat[3][i]);
				planes[i*2+j] = normalizePlane( planes[i*2+j] );
			}
		}
		for ( uint p = 0; p < 6; ++p ) {
			float d = max(instance.bounds.min.x * planes[p].x, instance.bounds.max.x * planes[p].x)
					+ max(instance.bounds.min.y * planes[p].y, instance.bounds.max.y * planes[p].y)
					+ max(instance.bounds.min.z * planes[p].z, instance.bounds.max.z * planes[p].z);
			if ( d > -planes[p].w ) return true;
		}
	#else
		vec4 corners[8] = {
			vec4( instance.bounds.min.x, instance.bounds.min.y, instance.bounds.min.z, 1.0 ),
			vec4( instance.bounds.max.x, instance.bounds.min.y, instance.bounds.min.z, 1.0 ),
			vec4( instance.bounds.max.x, instance.bounds.max.y, instance.bounds.min.z, 1.0 ),
			vec4( instance.bounds.min.x, instance.bounds.max.y, instance.bounds.min.z, 1.0 ),

			vec4( instance.bounds.min.x, instance.bounds.min.y, instance.bounds.max.z, 1.0 ),
			vec4( instance.bounds.max.x, instance.bounds.min.y, instance.bounds.max.z, 1.0 ),
			vec4( instance.bounds.max.x, instance.bounds.max.y, instance.bounds.max.z, 1.0 ),
			vec4( instance.bounds.min.x, instance.bounds.max.y, instance.bounds.max.z, 1.0 ),
		};
		vec4 planes[6]; {
			#pragma unroll 3
			for (int i = 0; i < 3; ++i)
			#pragma unroll 2
			for (int j = 0; j < 2; ++j) {
				planes[i*2+j].x = mat[0][3] + (j == 0 ? mat[0][i] : -mat[0][i]);
				planes[i*2+j].y = mat[1][3] + (j == 0 ? mat[1][i] : -mat[1][i]);
				planes[i*2+j].z = mat[2][3] + (j == 0 ? mat[2][i] : -mat[2][i]);
				planes[i*2+j].w = mat[3][3] + (j == 0 ? mat[3][i] : -mat[3][i]);
				planes[i*2+j] = normalizePlane( planes[i*2+j] );
			}
		}
		#pragma unroll 8
		for ( uint p = 0; p < 8; ++p ) corners[p] = mat * corners[p];
		#pragma unroll 6
		for ( uint p = 0; p < 6; ++p ) {
			#pragma unroll 8
			for ( uint q = 0; q < 8; ++q ) {
				if ( dot( corners[q], planes[p] ) > 0 ) return true;
			}
			return false;
		}
	#endif
#endif
	}
	return visible;
}

bool occlusionCull( uint id ) {
	if ( PushConstant.passes == 0 ) return true;
	
	const DrawCommand drawCommand = drawCommands[id];
	const Instance instance = instances[drawCommand.instanceID];

	bool visible = true;
	for ( uint pass = 0; pass < PushConstant.passes; ++pass ) {
#if 1
		vec4 aabb;
		vec4 sphere = aabbToSphere( instance.bounds );
		vec3 center = (camera.viewport[pass].view * instance.model * vec4(sphere.xyz, 1)).xyz;
		float radius = (instance.model * vec4(sphere.w, 0, 0, 0)).x;
	//	center.y *= -1;
		mat4 proj = camera.viewport[pass].projection;
		float znear = proj[3][2];
		float P00 = proj[0][0];
		float P11 = proj[1][1];
		if (projectSphere(center, radius, znear, P00, P11, aabb)) {
			ivec2 pyramidSize = textureSize( samplerDepth, 0 );
			float mips = mipLevels( pyramidSize );

			float width = (aabb.z - aabb.x) * pyramidSize.x;
			float height = (aabb.w - aabb.y) * pyramidSize.y;

			//find the mipmap level that will match the screen size of the sphere
			float level = floor(log2(max(width, height)));
		//	if ( level == mips )
				--level;
			level = clamp( level, 0, mips );

			//sample the depth pyramid at that specific level
			float depth = textureLod(samplerDepth, (aabb.xy + aabb.zw) * 0.5, level).x;

			float depthSphere = znear / (center.z - radius);

			instances[drawCommand.instanceID].bounds.padding1 = depth;
			instances[drawCommand.instanceID].bounds.padding2 = proj[3][2];

			//if the depth of the sphere is in front of the depth pyramid value, then the object is visible
			visible = visible && depthSphere >= depth - DEPTH_BIAS;
		}

#else
		mat4 mat = camera.viewport[pass].projection * camera.viewport[pass].view * instance.model;
		vec3 boundsSize = instance.bounds.max - instance.bounds.min;
		vec3 points[8] = {
			instance.bounds.min.xyz,
			instance.bounds.min.xyz + vec3(boundsSize.x,0,0),
			instance.bounds.min.xyz + vec3(0, boundsSize.y,0),
			instance.bounds.min.xyz + vec3(0, 0, boundsSize.z),
			instance.bounds.min.xyz + vec3(boundsSize.xy,0),
			instance.bounds.min.xyz + vec3(0, boundsSize.yz),
			instance.bounds.min.xyz + vec3(boundsSize.x, 0, boundsSize.z),
			instance.bounds.min.xyz + boundsSize.xyz,
		};
		vec2 minXY = vec2(1);
		vec2 maxXY = vec2(0);
		
		float minZ = 1;
		float maxZ = 0;

		#pragma unroll 8
		for ( uint i = 0; i < 8; ++i ) {
			vec4 clip = mat * vec4( points[i], 1 );
			clip.xyz /= clip.w;
			clip.xy = clip.xy * 0.5 + 0.5;

			minXY.x = min(minXY.x, clip.x);
			minXY.y = min(minXY.y, clip.y);
			
			maxXY.x = max(maxXY.x, clip.x);
			maxXY.y = max(maxXY.y, clip.y);

		#if INVERSE
			clip.z = 1.0 - clip.z;
			maxZ = max(maxZ, clip.z);
		#else
			minZ = min(minZ, clip.z);
		#endif
		}

		if ( maxXY.x <= 0 || maxXY.y <= 0 ) return false;
		if ( minXY.x >= 1 || minXY.y >= 1 ) return false;
		
		ivec2 depthSize = textureSize( samplerDepth, 0 );
		float mips = mipLevels( depthSize );

		vec4 uv = vec4(minXY, maxXY);

		ivec2 clipSize = ivec2(maxXY - minXY) * depthSize;
		float mip = mipLevels( clipSize );
		mip = clamp( mip, 0, mips );
		if ( mip == 0 ) {
			mip = 1;
		} else {
			float lower = max(mip - 1, 0);
			float scale = exp2(-lower);
			vec2 a = floor(uv.xy * scale);
			vec2 b = ceil(uv.zw * scale);
			vec2 dims = b - a;

			// Use the lower level if we only touch <= 2 texels in both dimensions
			if (dims.x <= 2 && dims.y <= 2) mip = lower;
		}

		float depths[4] = {
			textureLod( samplerDepth, uv.xy, mip ).r,
			textureLod( samplerDepth, uv.zy, mip ).r,
			textureLod( samplerDepth, uv.xw, mip ).r,
			textureLod( samplerDepth, uv.zw, mip ).r,
		};
	#if INVERSE
		float minDepth = 1.0 - min(min(min(depths[0], depths[1]), depths[2]), depths[3]);
	#else
		float maxDepth = max(max(max(depths[0], depths[1]), depths[2]), depths[3]);
	#endif

		instances[drawCommand.instanceID].bounds.padding1 = minZ;
		instances[drawCommand.instanceID].bounds.padding2 = maxDepth;

		return minZ <= maxDepth;
#endif
	}
	return visible;
}

void main() {
	const uint gID = gl_GlobalInvocationID.x;
	if ( !(0 <= gID && gID < drawCommands.length()) ) return;

	bool visible = frustumCull( gID );
//	if ( visible ) visible = occlusionCull( gID );
//	bool visible = occlusionCull( gID );
	drawCommands[gID].instances = visible ? 1 : 0;
}


/*
		Frustum frustum;
		for (int i = 0; i < 3; ++i)
		for (int j = 0; j < 2; ++j) {
			frustum.planes[i*2+j].x = mat[0][3] + (j == 0 ? mat[0][i] : -mat[0][i]);
			frustum.planes[i*2+j].y = mat[1][3] + (j == 0 ? mat[1][i] : -mat[1][i]);
			frustum.planes[i*2+j].z = mat[2][3] + (j == 0 ? mat[2][i] : -mat[2][i]);
			frustum.planes[i*2+j].w = mat[3][3] + (j == 0 ? mat[3][i] : -mat[3][i]);
			frustum.planes[i*2+j]*= length(frustum.planes[i*2+j].xyz);
		}
		for ( uint i = 0; i < 6; ++i ) {
			vec4 plane = frustum.planes[i];
			float d = dot(instance.bounds.center, plane.xyz);
			float r = dot(instance.bounds.extent, abs(plane.xyz));
			bool inside = d + r > -plane.w;
			if ( !inside ) return 0;
		}
		return true;
*/
/*
		vec4 plane;
		vec4 center = vec4( (max + min) * 0.5, 1 );
		vec4 extent = vec4( (max - min) * 0.5, 1 );
		center = mat * center;
		extent = mat * extent;
		center.xyz /= center.w;
		extent.xyz /= extent.w;
		for (int i = 0; i < 4; ++i ) plane[i] = mat[i][3] + mat[i][0]; // left
		visible = dot(center.xyz + extent.xyz * sign(plane.xyz), plane.xyz ) > -plane.w;
		if ( visible ) return true;
		
		for (int i = 0; i < 4; ++i ) plane[i] = mat[i][3] - mat[i][0]; // right
		visible = dot(center.xyz + extent.xyz * sign(plane.xyz), plane.xyz ) > -plane.w;
		if ( visible ) return true;
		
		for (int i = 0; i < 4; ++i ) plane[i] = mat[i][3] + mat[i][1]; // bottom
		visible = dot(center.xyz + extent.xyz * sign(plane.xyz), plane.xyz ) > -plane.w;
		if ( visible ) return true;
		
		for (int i = 0; i < 4; ++i ) plane[i] = mat[i][3] - mat[i][1]; // top
		visible = dot(center.xyz + extent.xyz * sign(plane.xyz), plane.xyz ) > -plane.w;
		if ( visible ) return true;
		
		for (int i = 0; i < 4; ++i ) plane[i] = mat[i][3] + mat[i][2]; // near
		visible = dot(center.xyz + extent.xyz * sign(plane.xyz), plane.xyz ) > -plane.w;
		if ( visible ) return true;

		for (int i = 0; i < 4; ++i ) plane[i] = mat[i][3] - mat[i][2]; // far
		visible = dot(center.xyz + extent.xyz * sign(plane.xyz), plane.xyz ) > -plane.w;
		if ( visible ) return true;

*/
/*
	for ( uint p = 0; p < 8; ++p ) {
		vec4 t = corners[p];
		float w = abs(t.w);
		visible = -w <= t.x && t.x <= w && -w <= t.y && t.y <= w && 0 <= t.z && t.z <= w; // && -w <= t.z && t.z <= w;
	}
*/
/*
mat4 convert( mat4 proj ) {
	float f = -proj[1][1];
	float raidou = f / proj[0][0];
	float zNear = proj[3][2];
	float zFar = 32;
	
	float range = zNear - zFar;

	float Sx = f * raidou;
	float Sy = f;
	float Sz = (-zNear - zFar) / range;
	float Pz = 2 * zFar * zNear / range;

	mat4 new = mat4(1.0);
	new[0][0] = Sx;
	new[1][1] = -Sy;
	new[2][2] = Sz;
	new[3][2] = Pz;
	new[2][3] = 1;
	return new;
}
*/