// Copyright (c) 2012-2017 VideoStitch SAS
// Copyright (c) 2018 stitchEm

#pragma once

// Procedure used to generate the inverse warping function
// Terminologies:
// - Input : image captured by the camera
// - Intermediate : projection of "Input" to an intermediate sphere of radius = 1
// - Output: an equi-rectangular projection of the Intermediate sphere onto a cylinder

#define WARPCOORD_OUTPUT_TO_INPUT_KERNEL(fromSphereToInput, isWithin, distortionMetersTransform,                                 \
                                         distortionPixelsTransform)                                                              \
  __global__ void                                                                                                                \
      warpCoordOutputToInputKernel_##fromSphereToInput##_##isWithin##_##distortionMetersTransform##_##distortionPixelsTransform( \
          float2* g_odata, uint32_t* g_mask, int id, int offsetX, int offsetY, int croppedWidth, int croppedHeight,              \
          int panoWidth, int panoHeight, int inputWidth, int inputHeight, int cropLeft, int cropRight, int cropTop,              \
          int cropBottom, const float2 panoScale, const vsfloat3x4 perspectiveMatrix, const float2 inputScale,                   \
          const vsDistortion distortion, const float2 centerShift) {                                                             \
    /* Calculate the normalized texture coordinates */                                                                           \
    const int x = blockIdx.x * blockDim.x + threadIdx.x;                                                                         \
    const int y = blockIdx.y * blockDim.y + threadIdx.y;                                                                         \
                                                                                                                                 \
    if (x < croppedWidth && y < croppedHeight) {                                                                                 \
      float2 uv = make_float2((x + offsetX) % panoWidth, y + offsetY);                                                           \
                                                                                                                                 \
      /*To Center coordinates*/                                                                                                  \
      uv.x -= panoWidth / 2.0f;                                                                                                  \
      uv.y -= panoHeight / 2.0f;                                                                                                 \
                                                                                                                                 \
      uv = mapPanoramaToInput_##fromSphereToInput##_##distortionMetersTransform##_##distortionPixelsTransform(                   \
          uv, panoScale, perspectiveMatrix, inputScale, distortion, centerShift);                                                \
                                                                                                                                 \
      /**                                                                                                                        \
       * Move to center-based coordinate                                                                                         \
       */                                                                                                                        \
      uv.x += inputWidth / 2.0f;                                                                                                 \
      uv.y += inputHeight / 2.0f;                                                                                                \
                                                                                                                                 \
      /* check if uv stay inside of the input buffer coordinate */                                                               \
      if (isWithin(uv, (float)inputWidth, (float)inputHeight, (float)cropLeft, (float)cropRight, (float)cropTop,                 \
                   (float)cropBottom)) {                                                                                         \
        g_odata[y * croppedWidth + x] = uv - make_float2(0.5, 0.5);                                                              \
        g_mask[y * croppedWidth + x] = 1 << id;                                                                                  \
      } else if (g_mask[y * croppedWidth + x] == 0) {                                                                            \
        /* This is a hack to solve the discretization problem when smaller map is used for coordinate lookup                     \
         * When it is just a bit outside of the mask, search around its neighbor and set the first valid coord to the            \
         * lookup mask                                                                                                           \
         * @TODO: Find a proper way to deal with this                                                                            \
         */                                                                                                                      \
        const float dir[2] = {1, -1};                                                                                            \
        for (float r = 0; r <= 3; r += 0.5) {                                                                                    \
          for (float j = 0; j <= r; j += 0.1) {                                                                                  \
            float i = sqrt(r * r - j * j);                                                                                       \
            for (int dirX = 0; dirX < 2; dirX++)                                                                                 \
              for (int dirY = 0; dirY < 2; dirY++) {                                                                             \
                if (isWithin(uv + make_float2(dir[dirX] * i, dir[dirY] * j), (float)inputWidth, (float)inputHeight,              \
                             (float)cropLeft, (float)cropRight, (float)cropTop, (float)cropBottom)) {                            \
                  g_odata[y * croppedWidth + x] =                                                                                \
                      uv - make_float2(0.5, 0.5) + make_float2(dir[dirX] * i, dir[dirY] * j);                                    \
                  g_mask[y * croppedWidth + x] = 1 << id;                                                                        \
                  return;                                                                                                        \
                }                                                                                                                \
              }                                                                                                                  \
          }                                                                                                                      \
        }                                                                                                                        \
        g_odata[y * croppedWidth + x] = make_float2(INVALID_FLOW_VALUE, INVALID_FLOW_VALUE);                                     \
        g_mask[y * croppedWidth + x] = 0;                                                                                        \
      }                                                                                                                          \
    }                                                                                                                            \
  }

#define WARPCOORD_OUTPUT_TO_INPUT_KERNEL2(RADIAL1, RADIAL2)                                \
  WARPCOORD_OUTPUT_TO_INPUT_KERNEL(SphereToRect, isWithinCropRect, RADIAL1, RADIAL2)       \
  WARPCOORD_OUTPUT_TO_INPUT_KERNEL(SphereToErect, isWithinCropRect, RADIAL1, RADIAL2)      \
  WARPCOORD_OUTPUT_TO_INPUT_KERNEL(SphereToFisheye, isWithinCropCircle, RADIAL1, RADIAL2)  \
  WARPCOORD_OUTPUT_TO_INPUT_KERNEL(SphereToFisheye, isWithinCropRect, RADIAL1, RADIAL2)    \
  WARPCOORD_OUTPUT_TO_INPUT_KERNEL(SphereToExternal, isWithinCropCircle, RADIAL1, RADIAL2) \
  WARPCOORD_OUTPUT_TO_INPUT_KERNEL(SphereToExternal, isWithinCropRect, RADIAL1, RADIAL2)
