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

using System.Collections.Generic;

#if UNITY_EDITOR
using UnityEditor;
#endif

using Unity.Collections;
using UnityEngine;

#if MUDBUN_BURST
using Unity.Burst;
using Unity.Mathematics;
using AOT;
#endif

namespace MudBun
{
#if MUDBUN_BURST
  [BurstCompile]
#endif
  public class MudCurveSimple : MudSolid
  {
    [Header("Shape")]
  
    [SerializeField] private float m_elongation = 0.0f;
    public float Elongation { get => m_elongation; set { m_elongation = value; MarkDirty(); } }

    public Transform PointA;
    public Transform ControlPoint;
    public Transform PointB;

    [SerializeField] private float m_radiusA = 0.2f;
    public float RadiusA { get => m_radiusA; set { m_radiusA = value; MarkDirty(); } }

    [SerializeField] private float m_radiusControlPoint = -1.0f;
    public float ControlPointRadius { get => m_radiusControlPoint; set { m_radiusControlPoint = value; MarkDirty(); } }

    [SerializeField] private float m_radiusB = 0.2f;
    public float RadiusB { get => m_radiusB; set { m_radiusB = value; MarkDirty(); } }

    [SerializeField] [Range(0.0f, 1.0f)] private float m_smoothStepBlend = 0.0f;
    public float SmoothStepBlend { get =>m_smoothStepBlend; set { m_smoothStepBlend = value; MarkDirty(); } }

    [Header("Noise")]

    [SerializeField] private bool m_enableNoise = false;
    [SerializeField] private float m_noiseOffset = 0.0f;
    [SerializeField] private Vector2 m_noiseBaseOctaveSize = 0.5f * Vector2.one;
    [SerializeField] [Range(0.0f, 1.0f)] private float m_noiseThreshold = 0.45f;
    [SerializeField] [Range(0.0f, 1.0f)] private float m_noiseThresholdFade = 0.0f;
    [SerializeField] [Range(-1.0f, 1.0f)] private float m_noiseThresholdCoreBias = 0.0f;
    [SerializeField] [Range(1, 3)] private int m_noiseNumOctaves = 2;
    [SerializeField] private float m_noiseOctaveOffsetFactor = 0.5f;
    [SerializeField] private float m_noiseTwist = 0.0f;
    [SerializeField] private float m_noiseTwistOffset = 0.0f;
    public bool EnableNoise { get => m_enableNoise; set { m_enableNoise = value; MarkDirty(); } }
    public float NoiseOffset { get => m_noiseOffset; set { m_noiseOffset = value; MarkDirty(); } }
    public Vector2 NoiseBaseOctaveSize { get => m_noiseBaseOctaveSize; set { m_noiseBaseOctaveSize = value; MarkDirty(); } }
    public float NoiseThreshold { get => m_noiseThreshold; set { m_noiseThreshold = value; MarkDirty(); } }
    public float NoiseThresholdFade { get => m_noiseThresholdFade; set { m_noiseThresholdFade = value; MarkDirty(); } }
    public float NoiseThresholdCoreBias { get => m_noiseThresholdCoreBias; set { m_noiseThresholdCoreBias = value; MarkDirty(); } }
    public int NoiseNumOctaves { get => m_noiseNumOctaves; set { m_noiseNumOctaves = value; MarkDirty(); } }
    public float NoiseOctaveOffsetFactor { get => m_noiseOctaveOffsetFactor; set { m_noiseOctaveOffsetFactor = value; MarkDirty(); } }
    public float NoiseTwist { get => m_noiseTwist; set { m_noiseTwist = value; MarkDirty(); } }
    public float NoiseTwistOffset { get => m_noiseTwistOffset; set { m_noiseTwistOffset = value; MarkDirty(); } }

    public override Aabb RawBoundsRs
    {
      get
      {
        if (PointA == null || PointB == null || ControlPoint == null)
          return Aabb.Empty;

        Vector3 a = PointRs(PointA.position);
        Vector3 b = PointRs(PointB.position);
        Vector3 c = PointRs(ControlPoint.position);

        Vector3 r = Mathf.Max(m_radiusA, m_radiusB, m_radiusControlPoint) * Vector3.one;
        Aabb bounds = Aabb.Empty;
        bounds.Include(new Aabb(a - r, a + r));
        bounds.Include(new Aabb(b - r, b + r));
        bounds.Include(new Aabb(c - r, c + r));

        if (m_elongation != 0.0f)
        {
          Vector3 ab = b - a;
          Vector3 ac = c - a;
          Vector3 x = VectorUtil.NormalizeSafe(ab, VectorRs(transform.right));
          Vector3 z = VectorUtil.NormalizeSafe(Vector3.Cross(ab, ac), VectorRs(transform.forward));

          Vector3 e = m_elongation * VectorUtil.Abs(VectorRs(z));
          bounds.Min -= e;
          bounds.Max += e;
        }

        return bounds;
      }
    }

    public override void SanitizeParameters()
    {
      base.SanitizeParameters();

      Validate.NonNegative(ref m_elongation);

      Validate.NonNegative(ref m_radiusA);
      Validate.NonNegative(ref m_radiusB);

      Validate.NonNegative(ref m_noiseBaseOctaveSize);

    }

    private Transform[] m_aPoint = new Transform [] { null, null, null };
    private void Update()
    {
      if (m_aPoint[0] != PointA || m_aPoint[1] != PointB || m_aPoint[2] != ControlPoint)
        MarkDirty();

      m_aPoint[0] = PointA;
      m_aPoint[1] = PointB;
      m_aPoint[2] = ControlPoint;
      foreach (var p in m_aPoint)
      {
        if (p == null)
          return;

        if (!p.hasChanged)
          continue;

        MarkDirty();
        p.hasChanged = false;
      }
    }

    public Matrix4x4 m_basis;

    public override int FillComputeData(NativeArray<SdfBrush> aBrush, int iStart, List<Transform> aBone)
    {
      if (PointA == null || PointB == null || ControlPoint == null)
        return 0;

      Vector3 a = PointA.position;
      Vector3 b = PointB.position;
      Vector3 c = ControlPoint.position;
      Vector3 d = 0.5f * (a + b);
      Vector3 ab = b - a;
      Vector3 ac = c - a;
      Vector3 x = VectorUtil.NormalizeSafe(ab, transform.right);
      Vector3 z = VectorUtil.NormalizeSafe(Vector3.Cross(ab, ac), transform.forward);
      Vector3 y = VectorUtil.NormalizeSafe(Vector3.Cross(z, x), transform.up);
      m_basis = Matrix4x4.TRS(d, RotationRs(Quaternion.LookRotation(z, y)), Vector3.one);
      Matrix4x4 basisInv = m_basis.inverse;
      a = basisInv.MultiplyPoint(a);
      b = basisInv.MultiplyPoint(b);
      c = basisInv.MultiplyPoint(c);

      int iBrush = iStart;
      SdfBrush brush = SdfBrush.New();

      bool colinear = Mathf.Abs(Vector3.Dot(VectorUtil.NormalizeSafe(ab, Vector3.forward), VectorUtil.NormalizeSafe(ac, Vector3.forward))) > 0.99999f;

      brush.Type = (int) SdfBrush.TypeEnum.CurveSimple;
      brush.Data0 = new Vector4(a.x, a.y, a.z, m_radiusA);
      brush.Data1 = new Vector4(b.x, b.y, b.z, m_radiusB);
      brush.Data2 = new Vector4(c.x, c.y, c.z, m_enableNoise ? 1.0f : 0.0f);
      brush.Data3 = new Vector4(m_elongation, m_radiusControlPoint, m_smoothStepBlend, colinear ? 1.0f : 0.0f);

      if (aBone != null)
      {
        brush.BoneIndex = aBone.Count;
        aBone.Add(PointA);
        aBone.Add(PointB);
        aBone.Add(ControlPoint);
      }

      aBrush[iBrush++] = brush;

      if (m_enableNoise)
      {
        brush.Type = (int) SdfBrush.TypeEnum.Nop;
        brush.Data0 = new Vector4(m_noiseBaseOctaveSize.x, m_noiseBaseOctaveSize.y, m_noiseBaseOctaveSize.y, m_noiseThreshold);
        brush.Data1 = new Vector4(m_noiseOffset, 0.0f, 0.0f, m_noiseNumOctaves);
        brush.Data2 = new Vector4(m_noiseOctaveOffsetFactor, MathUtil.TwoPi * (m_noiseTwistOffset), MathUtil.TwoPi * (m_noiseTwistOffset + m_noiseTwist), 0.0f);
        brush.Data3 = new Vector4(m_noiseThresholdFade, m_noiseThresholdCoreBias, 0.0f, 0.0f);
        aBrush[iBrush++] = brush;
      }

      return iBrush - iStart;
    }

    public override void FillBrushData(ref SdfBrush brush, int iBrush)
    {
      base.FillBrushData(ref brush, iBrush);

      brush.Position = PointRs(m_basis.MultiplyPoint(Vector3.zero));
      brush.Rotation = RotationRs(m_basis.rotation);
    }

    internal override bool IsSelected()
    {
      if (base.IsSelected())
        return true;

      #if UNITY_EDITOR
      if (PointA?.gameObject != null && Selection.Contains(PointA.gameObject))
        return true;

      if (PointB?.gameObject != null && Selection.Contains(PointB.gameObject))
        return true;

      if (ControlPoint?.gameObject != null && Selection.Contains(ControlPoint.gameObject))
        return true;
      #endif

      return false;
    }

    public override void DrawSelectionGizmosRs()
    {
      base.DrawSelectionGizmosRs();

      if (PointA == null || PointB == null || ControlPoint == null)
        return;

      Vector3 a = PointRs(PointA.position);
      Vector3 b = PointRs(PointB.position);
      Vector3 c = PointRs(ControlPoint.position);

      int n = 8;
      float t = 0.0f;
      float dt = 1.0f / (n - 1);
      for (int i = 0; i < n; ++i)
      {
        GizmosUtil.DrawInvisibleSphere(VectorUtil.BezierQuad(a, b, c, t), Mathf.Lerp(m_radiusA, m_radiusB, t), Vector3.one, Quaternion.identity);
        t += dt;
      }
    }

#if MUDBUN_BURST
    [BurstCompile]
    [MonoPInvokeCallback(typeof(Sdf.SdfBrushEvalFunc))]
    [RegisterSdfBrushEvalFunc(SdfBrush.TypeEnum.CurveSimple)]
    public static unsafe float EvaluateSdf(float resDummy, ref float3 p, in float3 pRel, SdfBrush* aBrush, int iBrush)
    {
      float res = float.MaxValue;
      var b = aBrush[iBrush];
      float3 pRelAdj = pRel;

      float3 pA = new float4(b.Data0).xyz;
      float3 pB = new float4(b.Data1).xyz;
      float3 pC = new float4(b.Data2).xyz;

      float3 pRelRaw = pRelAdj;
      float elongation = b.Data3.x;
      pRelAdj.z -= math.clamp(pRelAdj.z, -elongation, elongation);

      float controlPointR = b.Data3.y;
      float smoothStepBlend = b.Data3.z;
      float r = 0.0f;

      bool colinear = b.Data3.w > 0.0f;
      float2 curRes;
      if (b.Data3.w > 0.0f) // colinear?
        Sdf.Segment(pRelAdj, pA, pB, out curRes);
      else
        Sdf.Bezier(pRelAdj, pA, pC, pB, out curRes);

      if (controlPointR < 0.0f)
      {
        float t = curRes.y;
        r = b.Data0.w + (b.Data1.w - b.Data0.w) * math.lerp(t, math.smoothstep(0.0f, 1.0f, t), smoothStepBlend);
      }
      else
      {
        if (curRes.y < 0.5f)
        {
          float t = 2.0f * curRes.y;
          r = b.Data0.w + (controlPointR - b.Data0.w) * math.lerp(t, math.smoothstep(0.0f, 1.0f, t), smoothStepBlend);
        }
        else
        {
          float t = 2.0f * (curRes.y - 0.5f);
          r = controlPointR + (b.Data1.w - controlPointR) * math.lerp(t, math.smoothstep(0.0f, 1.0f, t), smoothStepBlend);
        }
      }
      res = curRes.x - r;

      bool useNoise = (b.Data2.w > 0.0f);
      if (useNoise)
      {
        float curveLen = 0.0f;
        int precision = 16;
        float dt = 1.0f / precision;
        float t = dt;
        float3 prevPos = pA;
        for (int i = 1; i < precision; ++i, t += dt)
        {
          float3 currPos;
          MathUtil.BezierQuad(pA, pC, pB, t, out currPos);
          curveLen += math.length(currPos - prevPos);
          prevPos = currPos;
        }
        if (curRes.y < 0.0001f)
          curRes.y = math.min(0.0f, -math.dot(math.normalize(pA - pC), pRelAdj - pA) / curveLen);
        else if (curRes.y > 0.9999f)
          curRes.y = math.max(1.0f, 1.0f + math.dot(math.normalize(pB - pC), pRelAdj - pB) / curveLen);

        float3 up = math.normalize(new float3(0.0f, 1.0f, 0.0f) + 1e-3f * Sdf.Rand(pA));
        float3 front = math.normalize(math.lerp(pA - pC, pC - pB, curRes.y));
        float3 left = math.normalize(math.cross(up, front));
        up = math.cross(front, left);
        float3 closest;
        MathUtil.BezierQuad(pA, pC, pB, curRes.y, out closest);
        float3 pDelta = pRelRaw - closest;
        float3 s = new float3(curRes.y * curveLen, math.dot(pDelta, up), math.dot(pDelta, left));

        // advance to additional noise data
        b = aBrush[iBrush + 1];

        float thresholdFade = b.Data3.x;
        float thresholdCoreBias = b.Data3.y;

        // twist
        float twistA = b.Data2.y;
        float twistB = b.Data2.z;
        float twistT = math.lerp(twistA, twistB, curRes.y);
        float twistCos = math.cos(twistT);
        float twistSin = math.sin(twistT);
        s.yz = math.mul(new float2x2(twistCos, twistSin, -twistSin, twistCos), s.yz);

        float3 offset = new float4(b.Data1).xyz;
        float3 size = new float4(b.Data0).xyz;
        float threshold = b.Data0.w;
        float rDelta = math.length(pDelta);
        float coreBiasT = 1.0f - math.saturate(rDelta / math.max(MathUtil.Epsilon, r));
        threshold = math.saturate(threshold + math.sign(thresholdCoreBias) * math.abs(thresholdCoreBias) * coreBiasT);
        threshold += (1.0f - threshold) * thresholdFade * math.saturate(curRes.y);
        int numOctaves = (int) b.Data1.w;
        float octaveOffsetFactor = b.Data2.x;

        float twistSdfMult = 1.0f / (1.0f + math.saturate(math.abs(twistA - twistB))); // hack: evlauate more surrounding voxels when twisted to avoid holes
        float n = twistSdfMult * Sdf.Noise((int) SdfBrush.NoiseTypeEnum.BakedPerlin, s, float.MinValue, float.MaxValue, offset, size, threshold, numOctaves, octaveOffsetFactor, 8.0f);
        res = Sdf.IntCubic(res, n, 0.5f * r);
      }

      return res;
    }
#endif

    public override void DrawOutlineGizmosRs()
    {
      base.DrawOutlineGizmosRs();

      if (PointA != null)
      {
        GizmosUtil.DrawWireSphere(PointRs(PointA.position), m_radiusA, Vector3.one, RotationRs(PointA.rotation));
      }

      if (PointB != null)
      {
        GizmosUtil.DrawWireSphere(PointRs(PointB.position), m_radiusB, Vector3.one, RotationRs(PointB.rotation));
      }

      if (ControlPoint != null)
      {
        float da = (ControlPoint.position - PointA.position).magnitude;
        float db = (ControlPoint.position - PointB.position).magnitude;
        float r = m_radiusControlPoint >= 0.0f ? m_radiusControlPoint : Mathf.Lerp(m_radiusA, m_radiusB, da / (da + db));
        GizmosUtil.DrawWireSphere(PointRs(ControlPoint.position), r, Vector3.one, RotationRs(ControlPoint.rotation));
      }

      if (PointA != null && PointB != null && ControlPoint != null)
      {
        GizmosUtil.DrawBezierQuad(PointRs(PointA.position), PointRs(PointB.position), PointRs(ControlPoint.position));
      }
    }
  }
}

