#include "UtilsMath.h"
#include "UtilsCubemap.h"

#include <cstdio>
#include <glm/glm.hpp>
#include <glm/ext.hpp>

#include "stb_image_resize2.h"

using glm::vec2;
using glm::vec3;
using glm::vec4;
using glm::ivec2;

/// From Henry J. Warren's "Hacker's Delight"
float radicalInverse_VdC(uint32_t bits)
{
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return float(bits) * 2.3283064365386963e-10f; // / 0x100000000
}

// The i-th point is then computed by

/// From http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html

vec2 hammersley2d(uint32_t i, uint32_t N)
{
    return vec2(float(i)/float(N), radicalInverse_VdC(i));
}

void convolveLambertian(const vec3* data, int srcW, int srcH, int dstW, int dstH, vec3* output, int numMonteCarloSamples)
{
	// only equirectangular maps are supported
	assert(srcW == 2 * srcH);

	if (srcW != 2 * srcH) return;

	std::vector<vec3> tmp(dstW * dstH);

	stbir_resize(
      reinterpret_cast<const float*>(data), srcW, srcH, 0, reinterpret_cast<float*>(tmp.data()), dstW, dstH, 0, STBIR_RGB, STBIR_TYPE_FLOAT,
      STBIR_EDGE_WRAP, STBIR_FILTER_CUBICBSPLINE);

	const vec3* scratch = tmp.data();
	srcW = dstW;
	srcH = dstH;

	for (int y = 0; y != dstH; y++)
	{
		printf("Line %i...\n", y);
		const float theta1 = float(y) / float(dstH) * Math::PI;
		for (int x = 0; x != dstW; x++)
		{
			const float phi1 = float(x) / float(dstW) * Math::TWOPI;
			const vec3 V1 = vec3(sin(theta1) * cos(phi1), sin(theta1) * sin(phi1), cos(theta1));
			vec3 color = vec3(0.0f);
			float weight = 0.0f;
			for (int i = 0; i != numMonteCarloSamples; i++)
			{
				const vec2 h = hammersley2d(i, numMonteCarloSamples);
				const int x1 = int(floor(h.x * srcW));
				const int y1 = int(floor(h.y * srcH));
				const float theta2 = float(y1) / float(srcH) * Math::PI;
				const float phi2 = float(x1) / float(srcW) * Math::TWOPI;
				const vec3 V2 = vec3(sin(theta2) * cos(phi2), sin(theta2) * sin(phi2), cos(theta2));
				const float D = std::max(0.0f, glm::dot(V1, V2));
				if (D > 0.01f)
				{
					color += scratch[y1 * srcW + x1] * D;
					weight += D;
				}
			}
			output[y * dstW + x] = color / weight;
		}
	}
}

void convolveGGX(const vec3* data, int srcW, int srcH, int dstW, int dstH, vec3* output, int numMonteCarloSamples)
{
  // only equirectangular maps are supported
  assert(srcW == 2 * srcH);

  if (srcW != 2 * srcH)
    return;

  std::vector<vec3> tmp(dstW * dstH);

  stbir_resize(
      reinterpret_cast<const float*>(data), srcW, srcH, 0, reinterpret_cast<float*>(tmp.data()), dstW, dstH, 0, STBIR_RGB, STBIR_TYPE_FLOAT,
      STBIR_EDGE_WRAP, STBIR_FILTER_CUBICBSPLINE);

  const vec3* scratch = tmp.data();
  srcW                = dstW;
  srcH                = dstH;

  for (int y = 0; y != dstH; y++) {
    printf("Line %i...\n", y);
    const float theta1 = float(y) / float(dstH) * Math::PI;
    for (int x = 0; x != dstW; x++) {
      const float phi1 = float(x) / float(dstW) * Math::TWOPI;
      const vec3 V1    = vec3(sin(theta1) * cos(phi1), sin(theta1) * sin(phi1), cos(theta1));
      vec3 color       = vec3(0.0f);
      float weight     = 0.0f;
      for (int i = 0; i != numMonteCarloSamples; i++) {
        const vec2 h       = hammersley2d(i, numMonteCarloSamples);
        const int x1       = int(floor(h.x * srcW));
        const int y1       = int(floor(h.y * srcH));


        const float theta2 = float(y1) / float(srcH) * Math::PI;
        const float phi2   = float(x1) / float(srcW) * Math::TWOPI;
        const vec3 V2      = vec3(sin(theta2) * cos(phi2), sin(theta2) * sin(phi2), cos(theta2));
        const float D      = std::max(0.0f, glm::dot(V1, V2));
        if (D > 0.01f) {
          color += scratch[y1 * srcW + x1] * D;
          weight += D;
        }
      }
      output[y * dstW + x] = color / weight;
    }
  }
}


vec3 faceCoordsToXYZ(int i, int j, int faceID, int faceSize)
{
	const float A = 2.0f * float(i) / faceSize;
	const float B = 2.0f * float(j) / faceSize;

	if (faceID == 0) return vec3(-1.0f, A - 1.0f, B - 1.0f);
	if (faceID == 1) return vec3(A - 1.0f, -1.0f, 1.0f - B);
	if (faceID == 2) return vec3(1.0f, A - 1.0f, 1.0f - B);
	if (faceID == 3) return vec3(1.0f - A, 1.0f, 1.0f - B);
	if (faceID == 4) return vec3(B - 1.0f, A - 1.0f, 1.0f);
	if (faceID == 5) return vec3(1.0f - B, A - 1.0f, -1.0f);

	return vec3();
}

Bitmap convertEquirectangularMapToVerticalCross(const Bitmap& b)
{
	if (b.type_ != eBitmapType_2D) return Bitmap();

	const int faceSize = b.w_ / 4;

	const int w = faceSize * 3;
	const int h = faceSize * 4;

	Bitmap result(w, h, b.comp_, b.fmt_);

	const ivec2 kFaceOffsets[] =
	{
		ivec2(faceSize, faceSize * 3),
		ivec2(0, faceSize),
		ivec2(faceSize, faceSize),
		ivec2(faceSize * 2, faceSize),
		ivec2(faceSize, 0),
		ivec2(faceSize, faceSize * 2)
	};

	const int clampW = b.w_ - 1;
	const int clampH = b.h_ - 1;

	for (int face = 0; face != 6; face++)
	{
		for (int i = 0; i != faceSize; i++)
		{
			for (int j = 0; j != faceSize; j++)
			{
				const vec3 P = faceCoordsToXYZ(i, j, face, faceSize);
				const float R = hypot(P.x, P.y);
				const float theta = atan2(P.y, P.x);
				const float phi = atan2(P.z, R);
				//	float point source coordinates
				const float Uf = float(2.0f * faceSize * (theta + M_PI) / M_PI);
				const float Vf = float(2.0f * faceSize * (M_PI / 2.0f - phi) / M_PI);
				// 4-samples for bilinear interpolation
				const int U1 = clamp(int(floor(Uf)), 0, clampW);
				const int V1 = clamp(int(floor(Vf)), 0, clampH);
				const int U2 = clamp(U1 + 1, 0, clampW);
				const int V2 = clamp(V1 + 1, 0, clampH);
				// fractional part
				const float s = Uf - U1;
				const float t = Vf - V1;
				// fetch 4-samples
				const vec4 A = b.getPixel(U1, V1);
				const vec4 B = b.getPixel(U2, V1);
				const vec4 C = b.getPixel(U1, V2);
				const vec4 D = b.getPixel(U2, V2);
				// bilinear interpolation
				const vec4 color = A * (1 - s) * (1 - t) + B * (s) * (1 - t) + C * (1 - s) * t + D * (s) * (t);
				result.setPixel(i + kFaceOffsets[face].x, j + kFaceOffsets[face].y, color);
			}
		};
	}

	return result;
}

Bitmap convertVerticalCrossToCubeMapFaces(const Bitmap& b)
{
  const int faceWidth  = b.w_ / 3;
  const int faceHeight = b.h_ / 4;

  Bitmap cubemap(faceWidth, faceHeight, 6, b.comp_, b.fmt_);
  cubemap.type_ = eBitmapType_Cube;

  const uint8_t* src = b.data_.data();
  uint8_t* dst       = cubemap.data_.data();

  /*
      ------
      | +Y |
   ----------------
   | -X | -Z | +X |
   ----------------
      | -Y |
      ------
      | +Z |
      ------
  */

  const int pixelSize = cubemap.comp_ * Bitmap::getBytesPerComponent(cubemap.fmt_);

  for (int face = 0; face != 6; ++face) {
    for (int j = 0; j != faceHeight; ++j) {
      for (int i = 0; i != faceWidth; ++i) {
        int x = 0;
        int y = 0;

        switch (face) {
          // CUBE_MAP_POSITIVE_X
        case 0:
          x = 2 * faceWidth + i;
          y = 1 * faceHeight + j;
          break;

          // CUBE_MAP_NEGATIVE_X
        case 1:
          x = i;
          y = faceHeight + j;
          break;

          // CUBE_MAP_POSITIVE_Y
        case 2:
          x = 1 * faceWidth + i;
          y = j;
          break;

          // CUBE_MAP_NEGATIVE_Y
        case 3:
          x = 1 * faceWidth + i;
          y = 2 * faceHeight + j;
          break;

          // CUBE_MAP_POSITIVE_Z
        case 4:
          x = faceWidth + i;
          y = faceHeight + j;
          break;

          // CUBE_MAP_NEGATIVE_Z
        case 5:
          x = 2 * faceWidth - (i + 1);
          y = b.h_ - (j + 1);
          break;
        }

        memcpy(dst, src + (y * b.w_ + x) * pixelSize, pixelSize);

        dst += pixelSize;
      }
    }
  }

  return cubemap;
}
