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

#ifndef MUDBUN_BONE_FUNCS
#define MUDBUN_BONE_FUNCS

#include "BrushDefs.cginc"
#include "BrushFuncs.cginc"

#define kAutoRiggingNew     (0)
#define kAutoRiggingDefault (1)

int autoRiggingAlgorithm = kAutoRiggingNew;

float4 normalize_bone_weight(float4 boneWeight)
{
  return saturate(boneWeight / comp_sum(boneWeight) - 0.01f);
}

// reference: sdf_brush_apply
float bone_weight_t(SdfBrush brush, float distA, float distB)
{
  float t = 0.0f;

  switch (brush.op)
  {
    case kSdfUnionCubic:
    case kSdfUnionChamfer:
      t = dist_blend_weight(distA, distB, 2.5f);
      break;

    case kSdfSubtractCubic:
    case kSdfSubtractChamfer:
      t = 1.0f - saturate(2.0f * distB / max(kEpsilon, brush.blend));
      break;

    case kSdfIntersectCubic:
    case kSdfIntersectChamfer:
      t = 1.0f - saturate(-2.0f * distB / max(kEpsilon, brush.blend));
      break;

    case kSdfPipe:
      t = saturate(-distB / max(kEpsilon, brush.blend));
      break;

    case kSdfEngrave:
      t = 1.0f - saturate(abs(distB) / max(kEpsilon, brush.blend));
      break;

    /*
    case kSdfDye:
      t = 1.0f - saturate(max(0.0f, distB) / max(kEpsilon, brush.blend));
      break;
    */
    default:
      if (is_sdf_dye(brush.op))
      {
        t = 1.0f - saturate(max(0.0f, distB) / max(kEpsilon, brush.blend));
      }
      break;
  }

  return t;
}

int blend_bone_weights(float brushRes, SdfBrush brush, int brushBoneIndex, inout float4 boneRes, inout int4 boneIndex, inout float4 boneWeight)
{
  int iBone = -1;

  switch (autoRiggingAlgorithm)
  {
    case kAutoRiggingNew:
    {
      boneWeight *= 
        float4
        (
          1.0f - bone_weight_t(brush, boneRes.x, brushRes), 
          1.0f - bone_weight_t(brush, boneRes.y, brushRes), 
          1.0f - bone_weight_t(brush, boneRes.z, brushRes), 
          1.0f - bone_weight_t(brush, boneRes.w, brushRes)
        );

      float minBoneRes = min(boneRes.x, min(boneRes.y, min(boneRes.z, boneRes.w)));
      float brushWeight = bone_weight_t(brush, minBoneRes, brushRes);

      // this could probably be vectorized, but this is for off-line compute jobs so it's not that important
      if (brushWeight > boneWeight.x)
      {
        boneWeight.xyzw = float4(brushWeight, boneWeight.xyz);
        boneRes.xyzw = float4(brushRes, boneRes.xyz);
        boneIndex.xyzw = float4(brushBoneIndex, boneIndex.xyz);
        iBone = 0;
      }
      else if (brushWeight > boneWeight.y)
      {
        boneWeight.yzw = float3(brushWeight, boneWeight.yz);
        boneRes.yzw = float3(brushRes, boneRes.yz);
        boneIndex.yzw = float3(brushBoneIndex, boneIndex.yz);
        iBone = 1;
      }
      else if (brushWeight > boneWeight.z)
      {
        boneWeight.zw = float2(brushWeight, boneWeight.z);
        boneRes.zw = float2(brushRes, boneRes.z);
        boneIndex.zw = float2(brushBoneIndex, boneIndex.z);
        iBone = 2;
      }
      else if (brushWeight > boneWeight.w)
      {
        boneWeight.w = brushWeight;
        boneRes.w = brushRes;
        boneIndex.w = brushBoneIndex;
        iBone = 3;
      }
      else
      {
        return -1;
      }
    }
    break;

    case kAutoRiggingDefault:
    {
      // this could probably be vectorized, but this is for off-line compute jobs so it's not that important
      if (brushRes < boneRes.x)
      {
        boneRes.xyzw = float4(brushRes, boneRes.xyz);
        boneIndex.xyzw = float4(brushBoneIndex, boneIndex.xyz);
        iBone = 0;
      }
      else if (brushRes < boneRes.y)
      {
        boneRes.yzw = float3(brushRes, boneRes.yz);
        boneIndex.yzw = float3(brushBoneIndex, boneIndex.yz);
        iBone = 1;
      }
      else if (brushRes < boneRes.z)
      {
        boneRes.zw = float2(brushRes, boneRes.z);
        boneIndex.zw = float2(brushBoneIndex, boneIndex.z);
        iBone = 2;
      }
      else if (brushRes < boneRes.w)
      {
        boneRes.w = brushRes;
        boneIndex.w = brushBoneIndex;
        iBone = 3;
      }
      else
      {
        return -1;
      }

      boneWeight = 1.0f / max(kEpsilon, boneRes);
    }
    break;
  }

  // TODO: variable tightness?
  //boneWeight = pow(boneWeight, 0.25f);

  boneWeight = normalize_bone_weight(boneWeight);
  boneWeight = step(0.02f, boneWeight) * boneWeight; // trim tiny weights
  boneWeight = normalize_bone_weight(boneWeight);

  return iBone;
}

#include "../Customization/CustomBone.cginc"

void sdf_apply_brush_bone_weights(float3 p, SdfBrush b, float brushRes, inout float4 boneRes, inout int4 boneIndex, inout float4 boneWeight)
{
  float3 pRel = quat_rot(quat_inv(b.rotation), p - b.position);

  switch (b.type)
  {
    case kSdfCurveSimple:
    {
      float2 curveRes = sdf_bezier(p, b.data0.xyz, b.data2.xyz, b.data1.xyz);
      float brushRes = curveRes.x;
      float resA = brushRes;
      float resB = brushRes;
      float resC = brushRes;
      int brushBoneIndexA = b.boneIndex;
      int brushBoneIndexB = b.boneIndex + 1;
      int brushBoneIndexC = b.boneIndex + 2;
      int iBoneA = blend_bone_weights(resA, b, brushBoneIndexA, boneRes, boneIndex, boneWeight);
      int iBoneB = blend_bone_weights(resB, b, brushBoneIndexB, boneRes, boneIndex, boneWeight);
      int iBoneC = blend_bone_weights(resC, b, brushBoneIndexC, boneRes, boneIndex, boneWeight);
      float aBoneWeight[4] = { boneWeight.x, boneWeight.y, boneWeight.z, boneWeight.w };
      if (iBoneA >= 0 && iBoneB >= 0)
      {
        float t = bone_weight_t(b, resA, resB);
        float boneWeightAB = aBoneWeight[iBoneA] + aBoneWeight[iBoneB];
        aBoneWeight[iBoneA] += (1.0f - t) * boneWeightAB;
        aBoneWeight[iBoneB] += t * boneWeightAB;
      }
      if (iBoneB >= 0 && iBoneC >= 0)
      {
        float t = bone_weight_t(b, resB, resC);
        float boneWeightBC = aBoneWeight[iBoneB] + aBoneWeight[iBoneC];
        aBoneWeight[iBoneB] += (1.0f - t) * boneWeightBC;
        aBoneWeight[iBoneC] += t * boneWeightBC;
      }
      boneWeight = float4(aBoneWeight[0], aBoneWeight[1], aBoneWeight[2], aBoneWeight[3]);

      break;
    }

    case kSdfCurveFull:
    {
      int numPoints = int(b.data0.x) - 2;
      bool useNoise = false;//(b.data0.z > 0.0f);
      for (int i = 0; i < numPoints; ++i)
      {
        int iBrush = b.index + (useNoise ? 3 : 2) + i;
        float3 pointPos = aBrush[iBrush].data0.xyz;

        /*
        float maxSegDist = 0.0f;
        if (i > 0)
        {
          float3 prevPointPos = aBrush[iBrush - 1].data0.xyz;
          maxSegDist = max(maxSegDist, length(prevPointPos - pointPos));
        }
        if (i < numPoints - 1)
        {
          float3 nextPointPos = aBrush[iBrush - 1].data0.xyz;
          maxSegDist = max(maxSegDist, length(nextPointPos - pointPos));
        }

        float pDist = length(pRel - pointPos);
        if (maxSegDist > 0.0f && pDist > maxSegDist)
          continue;
        */

        float pointRes = sdf_sphere(p - aBrush[iBrush].data0.xyz, aBrush[iBrush].data0.w);
        int pointBoneIndex = b.boneIndex + i;
        blend_bone_weights(pointRes, b, pointBoneIndex, boneRes, boneIndex, boneWeight);
      }
      break;
    }

    case kSdfBox:
    case kSdfSphere:
    case kSdfCylinder:
    case kSdfTorus:
    case kSdfSolidAngle:
    case kSdfParticle:
    case kSdfParticleSystem:
    case kSdfNoiseVolume:
    {
      blend_bone_weights(brushRes, b, b.boneIndex, boneRes, boneIndex, boneWeight);
      break;
    }

    case kSdfNoOp:
      break;

    default:
    {
      apply_custom_brush_bone_weights(p, pRel, b, brushRes, boneRes, boneIndex, boneWeight);
      break;
    }
  }
}

void compute_brush_bone_weights(float3 p, out int4 boneIndex, out float4 boneWeight)
{
  boneIndex = -1;
  boneWeight = 1e-6f;

  float res = kInfinity;
  float4 boneRes = kInfinity;
  for (int iBrush = 0; iBrush < numBrushes; ++iBrush)
  {
    res = sdf_brush(res, p, aBrush[iBrush]);

    // not sure why Metal doesn't like this check...
    #if !defined(SHADER_API_METAL)
      if ((aBrush[iBrush].flags & kSdfBrushFlagsCountAsBone) == 0)
        continue;
    #endif

    sdf_apply_brush_bone_weights(p, aBrush[iBrush], res, boneRes, boneIndex, boneWeight);
  }
}

#endif

