/******************************************************************************/
/*
  Project   - MudBun
  Publisher - Long Bunny Labs
              http://LongBunnyLabs.com
  Author    - Ming-Lun "Allen" Chou
              http://AllenChou.net
*/
/******************************************************************************/

#ifndef MUDBUN_RAY_MARCHING
#define MUDBUN_RAY_MARCHING

#include "Render/ShaderCommon.cginc"

#define kLightTypeDirectional (1)
#define kLightTypePoint       (2)

#if MUDBUN_VALID

#include "BrushFuncs.cginc"

int maxRayMarchSteps;
float rayMarchHitDistance;
float rayMarchMaxRayDistance;

int numLightMarchSteps;
float rayMarchStepSize;
float rayMarchVolumeDensity;
float4 rayMarchLightPositionType;
float4 rayMarchLightDirection;
float4 rayMarchAbsorption; // x: volume, y: light
float rayMarchDarknesThreshold;
float rayMarchTransmittanceCurve;

float rayMarchNoiseThreshold;
float rayMarchNoiseEdgeFade;
float4 rayMarchNoiseScrollSpeed;
float4 rayMarchNoiseBaseOctaveSize;
int rayMarchNoiseNumOctaves;
float rayMarchNoiseOctaveOffsetFactor;

struct RayMarchResults
{
  bool hit;
  float3 pos;
  SdfBrushMaterial mat;
};

RayMarchResults init_ray_march_results()
{
    RayMarchResults res;
    res.hit = false;
    res.pos = 0.0f;
    res.mat = init_brush_material();

    return res;
}

#define SDF_RAY_MARCH_MASKED_BRUSHES(res, p, brushMask, mat, outputMat)        \
{                                                                              \
  int iStack = -1;                                                             \
  float3 pStack[kMaxBrushGroupDepth];                                          \
  float resStack[kMaxBrushGroupDepth];                                         \
  SdfBrushMaterial matStack[kMaxBrushGroupDepth];                              \
                                                                               \
  res = kInfinity;                                                             \
  mat = init_brush_material();                                                 \
  float3 groupP = p;                                                           \
  float groupRes = kInfinity;                                                  \
  SdfBrushMaterial groupMat = init_brush_material();                           \
  FOR_EACH_BRUSH_EXTERN_MASK(brushMask, /* TODO: skip brushes whose AABBs do not contain p */ \
    switch (aBrush[iBrush].type)                                               \
    {                                                                          \
      case kSdfBeginGroup:                                                     \
        iStack = min(kMaxBrushGroupDepth - 1, iStack + 1);                     \
        pStack[iStack] = p;                                                    \
        resStack[iStack] = res;                                                \
        matStack[iStack] = mat;                                                \
        res = kInfinity;                                                       \
        mat = init_brush_material();                                           \
        break;                                                                 \
      case kSdfEndGroup:                                                       \
        groupP = p;                                                            \
        groupRes = res;                                                        \
        groupMat = mat;                                                        \
        p = pStack[iStack];                                                    \
        res = resStack[iStack];                                                \
        mat = matStack[iStack];                                                \
        break;                                                                 \
    }                                                                          \
    res = sdf_brush_apply(res, groupRes, groupMat, groupP, aBrush[iBrush], mat, outputMat); \
    switch (aBrush[iBrush].type)                                               \
    {                                                                          \
      case kSdfEndGroup:                                                       \
        iStack = max(-1, iStack - 1);                                          \
        break;                                                                 \
    }                                                                          \
  );                                                                           \
}

// macro that generates less inline code
#define SDF_RAY_MARCH_NORMAL(normal, p, brushMask, h)                           \
  {                                                                             \
    float3 aSign[4] =                                                           \
    {                                                                           \
      float3( 1.0f, -1.0f, -1.0f),                                              \
      float3(-1.0f, -1.0f,  1.0f),                                              \
      float3(-1.0f,  1.0f, -1.0f),                                              \
      float3( 1.0f,  1.0f,  1.0f),                                              \
    };                                                                          \
    float3 aDelta[4] =                                                          \
    {                                                                           \
      float3( (h), -(h), -(h)),                                                 \
      float3(-(h), -(h),  (h)),                                                 \
      float3(-(h),  (h), -(h)),                                                 \
      float3( (h * 1.0001f), (h * 1.0002f), (h * 1.0003f)),                     \
    };                                                                          \
    float3 ss = 0.0f;                                                           \
    SdfBrushMaterial nmat;                                                      \
    [loop] for (int iDelta = 0; iDelta < 4; ++iDelta)                           \
    {                                                                           \
      float rr = 0.0f;                                                          \
      float3 pp = p + aDelta[iDelta];                                           \
      SDF_RAY_MARCH_MASKED_BRUSHES(rr, pp, brushMask, nmat, false);             \
      ss += aSign[iDelta] * rr;                                                 \
    }                                                                           \
    normal = normalize_safe(ss, float3(0.0f, 0.0f, 0.0f));                      \
  }

float3 ray_march_aabb_extents(Aabb aabb)
{
  // TODO: use max blend
  aabb.boundsMin -= 5.0f;
  aabb.boundsMax += 5.0f;

  return 0.5f * (aabb.boundsMax - aabb.boundsMin);
}

bool ray_march_aabb_intersects(Aabb a, Aabb b)
{
  // TODO: use max blend
  a.boundsMin -= 5.0f;
  a.boundsMax += 5.0f;

  return all(a.boundsMin <= b.boundsMax && a.boundsMax >= b.boundsMin);
}

float2 ray_march_aabb_ray_cast(Aabb aabb, float3 from, float3 to)
{
  // TODO: use max blend
  aabb.boundsMin -= 5.0f;
  aabb.boundsMax += 5.0f;

  float tMin = -kFltMax;
  float tMax = +kFltMax;

  float3 d = to - from;
  float3 absD = abs(d);
  bool3 isZero = absD < kEpsilon;

  // parallel?
  if (any(isZero && ((from < aabb.boundsMin) || (aabb.boundsMax < from))))
    return -kFltMax;

  float3 invD = sign(d) / max(kEpsilon, absD);
  float3 t1 = (aabb.boundsMin - from) * invD;
  float3 t2 = (aabb.boundsMax - from) * invD;
  float3 minComps = isZero ? (-kFltMax) : min(t1, t2);
  float3 maxComps = isZero ? (+kFltMax) : max(t1, t2);

  tMin = max(minComps.x, max(minComps.y, minComps.z));
  tMax = min(maxComps.x, min(maxComps.y, maxComps.z));

  if (tMin > tMax)
    return -kFltMax;

  if (tMin > 1.0f)
    return -kFltMax;

  return float2(max(0.0f, tMin), min(1.0f, tMax));
}

// stmt = statements processing "iData" of hit leaf AABB nodes
// will gracefully handle maxed-out stacks
#define RAY_MARCH_AABB_TREE_RAY_CAST(tree, root, rayFrom, rayTo, stmt)         \
{                                                                              \
  float3 rayDir = normalize_safe(rayTo - rayFrom, kUnitZ);                     \
  float3 rayDirOrtho = normalize_safe(find_ortho(rayDir), kUnitX);             \
  float3 rayDirOrthoAbs = abs(rayDirOrtho);                                    \
                                                                               \
  Aabb rayBounds;                                                              \
  rayBounds.boundsMin = min(rayFrom, rayTo);                                   \
  rayBounds.boundsMax = max(rayFrom, rayTo);                                   \
                                                                               \
  int stackTop = 0;                                                            \
  int stack[kAabbTreeNodeStackSize];                                           \
  stack[stackTop] = root;                                                      \
                                                                               \
  int numIters = 0;                                                            \
  while (stackTop >= 0 && numIters < 128 /* safeguard */)                      \
  {                                                                            \
    int index = stack[stackTop--];                                             \
    if (index < 0)                                                             \
      continue;                                                                \
                                                                               \
    if (!ray_march_aabb_intersects(tree[index].aabb, rayBounds))               \
      continue;                                                                \
                                                                               \
    float3 aabbCenter = aabb_center(tree[index].aabb);                         \
    float3 aabbHalfExtents = ray_march_aabb_extents(tree[index].aabb);         \
    float separation =                                                         \
      abs(dot(rayDirOrtho, rayFrom - aabbCenter))                              \
      - dot(rayDirOrthoAbs, aabbHalfExtents);                                  \
    if (separation > 0.0f)                                                     \
      continue;                                                                \
                                                                               \
    float2 t = ray_march_aabb_ray_cast(tree[index].aabb, rayFrom, rayTo);      \
    if (t.x < 0.0f)                                                            \
        continue;                                                              \
                                                                               \
    if (tree[index].iChildA < 0)                                               \
    {                                                                          \
      int iData = tree[index].iData;                                           \
                                                                               \
      stmt                                                                     \
    }                                                                          \
    else                                                                       \
    {                                                                          \
      stackTop = min(stackTop + 1, kAabbTreeNodeStackSize - 1);                \
      stack[stackTop] = tree[index].iChildA;                                   \
      stackTop = min(stackTop + 1, kAabbTreeNodeStackSize - 1);                \
      stack[stackTop] = tree[index].iChildB;                                   \
    }                                                                          \
  }                                                                            \
}

float sample_noise(float3 p, float sdfSample, float detailWeight = 0.0f, float detailScale = 0.5f, float detailOffsetScale = 2.0f)
{
  if (rayMarchNoiseThreshold < kEpsilon)
    return 1.0f;

  // base noise
  float base = 
    mbn_cached_noise
    (
      p / rayMarchNoiseBaseOctaveSize.xyz 
      + rayMarchNoiseScrollSpeed.xyz * _Time.y
    );

  float n = base;
  if (detailWeight > kEpsilon)
  {
    float detail = 
      mbn_cached_noise
      (
        p / (rayMarchNoiseBaseOctaveSize.xyz * detailScale) 
        + rayMarchNoiseScrollSpeed.xyz * _Time.y * detailOffsetScale
      );

    n = (base + detailWeight * detail) / (1.0f + max(0.0f, detailWeight));
  }
  n += 0.5f; // normalize to [0, 1]

  // apply threshold
  n = saturate(saturate(1.4f * n) - rayMarchNoiseThreshold);
  //n = rayMarchNoiseThreshold - saturate(1.4f * n);
  n = lerp(0.0f, n, saturate(10.0f * rayMarchNoiseThreshold));

  return n;
}

float sample_density(float3 p, float sdfSample, float detailWeight = 0.0f, float detailScale = 0.5f, float detailOffsetScale = 2.0f)
{
  if (rayMarchNoiseThreshold < kEpsilon)
    return -sdfSample;

  if (rayMarchNoiseEdgeFade < kEpsilon)
    return -sdfSample;

  float n = sample_noise(p, sdfSample, detailWeight, detailScale, detailOffsetScale);
  float noiseWeight = 1.0f - saturate(-sdfSample / rayMarchNoiseEdgeFade);
  float w = lerp(sdfSample, n, noiseWeight);
  //sdfSample = sdf_int_cubic(sdfSample, w, 1.0f);

  float density = n * (-sdfSample);
  density = density * pow(density, max(kEpsilon, rayMarchTransmittanceCurve));

  return density;
}

float light_transmittance(float3 rayFrom, float3 rayDirection)
{
  BRUSH_MASK(brushMask);
  BRUSH_MASK_CLEAR_ALL(brushMask);

  float3 rayTo = rayFrom + rayMarchMaxRayDistance * rayDirection;
  float tMin = kFltMax;
  float tMax = -kFltMax;
  RAY_MARCH_AABB_TREE_RAY_CAST(aabbTree, aabbRoot, rayFrom, rayTo, 
    BRUSH_MASK_SET(brushMask, iData);
    tMin = min(tMin, t.x);
    tMax = max(tMax, t.y);
  );
  float rayDist = (tMax - tMin) * rayMarchMaxRayDistance;

  float transmittance = 1.0f;
  float stepSize = rayDist / numLightMarchSteps;
  float3 rayStep = rayDirection * stepSize;
  float3 p = rayFrom;
  [loop] for (int iStep = 0; iStep < numLightMarchSteps; ++iStep)
  {
      p += rayStep;

      float sdfSample = 0.0f;
      SdfBrushMaterial mat;
      SDF_RAY_MARCH_MASKED_BRUSHES(sdfSample, p, brushMask, mat, true);
      if (sdfSample < 0.0f)
      {
        float density = rayMarchVolumeDensity * sample_density(p, sdfSample);
        transmittance *= exp(-density * stepSize * rayMarchAbsorption.y);

        if (transmittance < 0.1f)
        {
          transmittance = 0.0f;
          break;
        }
      }
  }

  return rayMarchDarknesThreshold + (1.0f - rayMarchDarknesThreshold) * transmittance;
}


// https://github.com/TheAllenChou/unity-ray-marching/blob/master/unity-ray-marching/Assets/Shader/Ray%20Marching/Resources/RayMarcherCs.compute
RayMarchResults ray_march_surface
(
  float3 rayFrom, 
  float3 rayDirection, 
  out float3 normal
)
{
  RayMarchResults results;
  results.hit = false;
  results.pos = rayFrom;
  results.mat = init_brush_material();

  normal = -rayDirection;

  BRUSH_MASK(brushMask);
  BRUSH_MASK_CLEAR_ALL(brushMask);

  // gather shapes around ray by casting it against AABB tree
  float3 rayTo = rayFrom + rayMarchMaxRayDistance * rayDirection;
  float tMin = kFltMax;
  float tMax = -kFltMax;
  RAY_MARCH_AABB_TREE_RAY_CAST(aabbTree, aabbRoot, rayFrom, rayTo, 
    BRUSH_MASK_SET(brushMask, iData);
    tMin = min(tMin, t.x);
    tMax = max(tMax, t.y);
  );
  float rayDist = (tMax - tMin) * rayMarchMaxRayDistance;

  // miss any AABB (tMin > 1.0f) ?
  clip(1.0f - tMin);

  // start at ray's earliest intersection with AABB tree
  float3 p = lerp(rayFrom, rayTo, tMin);

  // march ray
  float dist = 0.0f;
  [loop] for (int iStep = 0; iStep < maxRayMarchSteps; ++iStep)
  {
    float sdfSample = 0.0f;
    SDF_RAY_MARCH_MASKED_BRUSHES(sdfSample, p, brushMask, results.mat, false);
    if (sdfSample < rayMarchHitDistance)
    {
      SDF_RAY_MARCH_MASKED_BRUSHES(sdfSample, p, brushMask, results.mat, true);

#ifdef MUDBUN_RAY_MARCHING_COMPUTE_NORMAL
      SDF_RAY_MARCH_NORMAL(normal, p, brushMask, 1e-3f);
#endif

      results.hit = true;
      results.pos = p;
      break;
    }
    
    p += sdfSample * rayDirection;
    dist += sdfSample;

    if (dist > rayMarchMaxRayDistance || iStep == maxRayMarchSteps - 1)
      discard;
  }

  if (!results.hit)
    discard;

  return results;
}

// https://github.com/TheAllenChou/unity-ray-marching/blob/master/unity-ray-marching/Assets/Shader/Ray%20Marching/Resources/RayMarcherCs.compute
// https://shaderbits.com/blog/creating-volumetric-ray-marcher
// https://github.com/SebLague/Clouds/blob/master/Assets/Scripts/Clouds/Shaders/Clouds.shader
RayMarchResults ray_march_volume
(
  float3 rayFrom, 
  float3 rayDirection, 
  float3 backgroundColor, 
  sampler2D ditherTexture, 
  int ditherTextureSize, 
  float2 screenPos
)
{
  RayMarchResults results;
  results.hit = false;
  results.pos = rayFrom;
  results.mat = init_brush_material();





  // TODO: this is temp
  return results;





  BRUSH_MASK(brushMask);
  BRUSH_MASK_CLEAR_ALL(brushMask);

  // gather shapes around ray by casting it against AABB tree
  float3 rayTo = rayFrom + rayMarchMaxRayDistance * rayDirection;
  float tMin = kFltMax;
  float tMax = -kFltMax;
  RAY_MARCH_AABB_TREE_RAY_CAST(aabbTree, aabbRoot, rayFrom, rayTo, 
    BRUSH_MASK_SET(brushMask, iData);
    tMin = min(tMin, t.x);
    tMax = max(tMax, t.y);
  );
  float rayDist = (tMax - tMin) * rayMarchMaxRayDistance;

  // miss any AABB (tMin > 1.0f) ?
  clip(1.0f - tMin);

  // ray march step size is step size along camera direction
  // actual ray step size is different for each ray
  float3 camDir = mul(unity_CameraToWorld, float4(0.0f, 0.0f, 1.0f, 0.0f)).xyz;
  float actualStepSize = rayMarchStepSize / dot(camDir, rayDirection);
  float3 rayStep = rayDirection * actualStepSize;

  // start at ray's earliest intersection with AABB tree
  float3 p = lerp(rayFrom, rayTo, tMin);

  // snap ray start position to view-aligned planes
  //p -= fmod(dot(p - rayFrom, rayDirection), actualStepSize) * rayDirection;

  // jitter start position
  p -= tex2D(ditherTexture, screenPos / ditherTextureSize).r * rayStep;

  // march ray
  float transmittance = 1.0f;
  float3 lightEnergy = 0.0f;
  float3 toLightDir = -rayMarchLightDirection.xyz;
  int numRayMarchSteps = min(maxRayMarchSteps, ceil(rayDist / actualStepSize));
  [loop] for (int iStep = 0; iStep < numRayMarchSteps; ++iStep)
  {
    p += rayStep;
    
    float sdfSample = 0.0f;
    SdfBrushMaterial mat;
    SDF_RAY_MARCH_MASKED_BRUSHES(sdfSample, p, brushMask, mat, false);
    if (sdfSample < 0.0f)
    {
      float density = rayMarchVolumeDensity * sample_density(p, sdfSample, 0.1f, 0.3f, 2.0f);

      switch ((int) rayMarchLightPositionType.w)
      {
        case kLightTypePoint:
          toLightDir = normalize(rayMarchLightPositionType.xyz - p);
          break;
      }
      float lightTransmittance = light_transmittance(p, toLightDir);
      lightEnergy += density * actualStepSize * transmittance * lightTransmittance;

      transmittance *= exp(-density * actualStepSize * rayMarchAbsorption.x);

      if (transmittance < 1e-2f)
      {
        transmittance = 0.0f;
        break;
      }
    }
  }

  float3 cloudColor = lightEnergy; // TODO: * lightColor
  results.mat.color.rgb = backgroundColor * transmittance + cloudColor;
  //results.mat.color.a = saturate(1.0f - transmittance * exp(rayMarchTransmittanceCurve));
  results.mat.color.a = saturate(1.0f - transmittance);

  return results;
}

#else

struct SdfBrushMaterialDummy
{
  float4 color;
  float4 emissionHash;
  float4 metallicSmoothnessSizeTightness;
  float4 textureWeight;
};

struct RayMarchResults
{
  bool hit;
  float3 pos;
  SdfBrushMaterialDummy mat;
};

RayMarchResults ray_march_surface
(
  float3 rayOrigin, 
  float3 rayDirection, 
  out float3 normal
)
{
  RayMarchResults res;
  res.hit = false;
  res.pos = rayOrigin;
  res.mat.color = float4(0.00001f * rayDirection, 1.0f);
  res.mat.emissionHash = 0.0f;
  res.mat.metallicSmoothnessSizeTightness = 0.0f;
  res.mat.textureWeight = 0.0f;
  normal = -rayDirection;

  return res;
}

RayMarchResults ray_march_volume
(
  float3 rayOrigin, 
  float3 rayDirection, 
  float3 screenColor, 
  sampler2D ditherTexture, 
  int ditherTextureSize, 
  float2 screenPos
)
{
  RayMarchResults res;
  res.hit = false;
  res.pos = rayOrigin;
  res.mat.color = float4(0.00001f * rayDirection, 1.0f);

  return res;
}

#endif

#endif

