using System;
using UnityEngine;
namespace Framework.Support {
  /// A cubic bezier curve based on 4 points
  public struct CubicBezierCurve {
    /// Point the curve will start at
    public Vector3 P0;
    /// Point towards which the curve leaves the starting point
    public Vector3 P1;
    /// Point away from which the curve reaches the ending point
    public Vector3 P2;
    /// Point the curve ends at
    public Vector3 P3;
    /// 
    ///   Initializes a new cubic bezier curve from the provided support points
    /// 
    /// Point the curve will start at
    /// 
    ///   Point in whose direction the curve will leave the starting point
    /// 
    /// 
    ///   Point from whose direction the curve will reach the ending point
    /// 
    /// Point the curve will end at
    public CubicBezierCurve(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3) {
      this.P0 = p0;
      this.P1 = p1;
      this.P2 = p2;
      this.P3 = p3;
    }
    /// Approximates the length of the bezier curve
    /// The approximate length of the berzier curve
    public float ApproximateLength() {
      const int SegmentCount = 10;
      float approximateLength = 0.0f;
      // Break the curve into line segments and sum the length of those line segments.
      // I couldn't find any code that actually calculates the length based on the input
      // parameters to the bezier formula.
      Vector3 previous;
      Vector3 current = this.P0;
      for(int index = 1; index < SegmentCount; ++index) {
        previous = current;
        current = Interpolate(
          this.P0, this.P1, this.P2, this.P3, (float)index / (float)SegmentCount
        );
        approximateLength += (current - previous).magnitude;
      }
      return approximateLength + (this.P3 - current).magnitude;
    }
#if false
    internal static float MaybeGetLength(Vector3 b0, Vector3 b1, Vector3 b2, Vector3 b3) {
      Vector3 p0 = (b0 - b1);
      Vector3 p1 = (b2 - b1);
      Vector3 p2;
      Vector3 p3 = (b3 - b2);
      float l0 = p0.magnitude;
      float l1 = p1.magnitude;
      float l3 = p3.magnitude;
      if(l0 > 0.0f) {
        p0 /= l0;
      }
      if(l1 > 0.0f) {
        p1 /= l1;
      }
      if(l3 > 0.0f) {
        p3 /= l3;
      }
      p2 = -p1;
      float a = Mathf.Abs(Vector3.Dot(p0, p1)) + Mathf.Abs(Vector3.Dot(p2, p3));
      if((a > 1.98f) || ((l0 + l1 + l3) < ((4.0f - a) * 8.0f))) {
        return l0 + l1 + l3;
      }
      Vector3 bl0 = b0;
      Vector3 bl1 = (b0 + b1) * 0.5f;
      Vector3 mid = (b1 + b2) * 0.5f;
      Vector3 bl2 = (bl1 + mid) * 0.5f;
      Vector3 br3 = b3;
      Vector3 br2 = (b2 + b3) * 0.5f;
      Vector3 br1 = (br2 + mid) * 0.5f;
      Vector3 br0 = (br1 + bl2) * 0.5f;
      Vector3 bl3 = br0;
      return MaybeGetLength(bl0, bl1, bl2, bl3) + MaybeGetLength(br0, br1, br2, br3);
    }
#endif
    /// Calculates the point at the specified interpolation time
    /// Time at which the point will be calculated
    /// The interpolated point at the specified time
    public Vector3 GetPointAtTime(float t) {
      return Interpolate(this.P0, this.P1, this.P2, this.P3, t);
    }
    /// 
    ///   Calculates a point on the berzier curve at the specified distance
    ///   (following the curve) from the start
    /// 
    /// Distance from the start of the curve
    /// The interpolated location
    /// 
    ///   
    ///     This is a very rough approximation since the velocity along the curve
    ///     can be less than one (eg. at a tight bend), so using this method does not
    ///     guarantee equal distance traveled for the resulting point when linearly
    ///     increasing the distance.
    ///   
    ///   
    ///     If you need a more accurate version of this method, consider splitting
    ///     the curve into line segments, caching the length of those line segments
    ///     and interpolating the points along the curve this way. Or finding an
    ///     equation that calculates T from distance (I couldn't find one.)
    ///   
    /// 
    public Vector3 GetPointAtDistance(float distanceFromStart) {
      return Interpolate(
        this.P0, this.P1, this.P2, this.P3, distanceFromStart / ApproximateLength()
      );
    }
    /// Interpolates a point along a cubic bezier curve
    /// Location from which the curve starts
    /// Point towards which the direction of the curve starts out
    /// 
    ///   Point from which towards the end the direction of the curve ends
    /// 
    /// Location at which the curve ends
    /// Interpolation interval in the range 0.0 .. 1.0
    /// The interpolated position
    public static Vector3 Interpolate(
      Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t
    ) {
      return
        (float)Math.Pow(1 - t, 3) * p0 +
        3 * (float)Math.Pow(1 - t, 2) * t * p1 +
        3 * (1 - t) * (float)Math.Pow(t, 2) * p2 +
        (float)Math.Pow(t, 3) * p3;
    }
  }
} // namespace Framework.Support