using System.Collections;
using System.Collections.Generic;
using Godot;
using System.Text;
using System;

namespace Rokojori
{
  public static class Math3D
  {
    public static float LookingAtEachOtherAngle( Vector3 lookDirectionA, Vector3 lookDirectionB )
    {
      return Dot( lookDirectionA, lookDirectionB );
    }

    public static bool LookingAtEachOther( Vector3 lookDirectionA, Vector3 lookDirectionB )
    {
      return LookingAtEachOtherAngle( lookDirectionA, lookDirectionB ) > 0;
    }

    public static bool LookingTowards( Vector3 from, Vector3 fromDirection, Vector3 to )
    {
      return LookingAtEachOther( fromDirection, to - from );
    }


    public static Transform3D TRS( Vector3 translation, Quaternion rotation, Vector3 scale )
    {
      var trsf = new Transform3D( new Basis( rotation ), translation );
      return trsf.ScaledLocal( scale );
    }

    public static Transform3D TRS( Vector3 translation, Vector4 rotation, Vector3 scale )
    {
      var quaternion = new Quaternion( rotation.X, rotation.Y, rotation.Z, rotation.W );

      return TRS( translation, quaternion.Normalized(), scale );
    }


    public static Transform3D TRS( Pose pose, Vector3 scale )
    {
      return TRS( pose.position, pose.rotation, scale );
    }

    public static Vector4 QuaternionToVector4( Quaternion rotation )
    {
      return new Vector4( rotation.X, rotation.Y, rotation.Z, rotation.W );
    }

    public static Vector3 Clamp01( Vector3 v )
    {
      return new Vector3(
        MathX.Clamp01( v.X ),
        MathX.Clamp01( v.Y ),
        MathX.Clamp01( v.Z )
      );
    }

    public static Vector3 OnCircleXZ( float radians, float size = 1 )
    {
      var x = Mathf.Cos( radians ) * size;
      var z = Mathf.Sin( radians ) * size;

      return new Vector3( x, 0, z );
    }

    public static Vector3 OnCircleXY( float radians, float size = 1 )
    {
      var x = Mathf.Cos( radians ) * size;
      var y = Mathf.Sin( radians ) * size;

      return new Vector3( x, y, 0 );
    }

    public static Vector3 XYasXZ( Vector2 v )
    {
      return new Vector3( v.X, 0, v.Y );
    }

    public static float Dot( Vector3 a, Vector3 b )
    {
      return a.Dot( b );
    }

    public static Vector3 Cross( Vector3 a, Vector3 b )
    {
      return a.Cross( b );
    }

    public static Vector3 Fract( Vector3 a )
    {
      return new Vector3( MathX.Fract( a.X ), MathX.Fract( a.Y ), MathX.Fract( a.Z ) );
    }

    public static bool IsExactlyZero( Vector3 v )
    {
      return v.X == 0 && v.Y == 0 && v.Z == 0;
    } 

    public static Vector3 ComputeNormalFast( Vector3 a, Vector3 b, Vector3 c )
    {
      return Math3D.Cross( c - b , a - b  ).Normalized();
    }

    public static Vector3 ComputeNormal( Vector3 a, Vector3 b, Vector3 c )
    {
      var cross = Math3D.Cross( c - b , a - b  );

      if ( Math3D.IsExactlyZero( cross ) )
      {
        cross = Math3D.Cross( b - c , a - c  );
      }

      if ( Math3D.IsExactlyZero( cross ) )
      {
        cross = Math3D.Cross( c - a , b - a  );
      }

      return cross.Normalized();
    }


    public static Vector3 SmoothStep( Vector3 a, Vector3 b, Vector3 t )
    {
      var x = MathX.Smoothstep( a.X, b.X, t.X );
      var y = MathX.Smoothstep( a.Y, b.Y, t.Y );
      var z = MathX.Smoothstep( a.Z, b.Z, t.Z ); 

      return new Vector3( x, y, z );
    }

    public static Vector3 Center( params Vector3[] points )
    {
      var center = Vector3.Zero;

      if ( points.Length == 0 )
      {
        return center;
      }

      for ( int i = 0; i < points.Length; i++ )
      {
        center += points[ i ];
      }

      return center / points.Length;
    }

    public static Vector3 Center<N>( List<N> points ) where N:Node3D
    {
      var center = Vector3.Zero;

      if ( points.Count == 0 )
      {
        return center;
      }

      for ( int i = 0; i < points.Count; i++ )
      {
        center += points[ i ].GlobalPosition;
      }

      return center / points.Count;
    }

    public static Vector3 Center( List<Vector3> points )
    {
      var center = Vector3.Zero;

      if ( points.Count == 0 )
      {
        return center;
      }

      for ( int i = 0; i < points.Count; i++ )
      {
        center += points[ i ];
      }

      return center / points.Count;
    }

    public static Vector3 GetClosest( Vector3 from, params Vector3[] points )
    {
      var distance = float.MaxValue;
      var index = -1;

      for ( int i = 0; i < points.Length; i++ )
      {
        var d = from.DistanceSquaredTo( points[ i ] );

        if ( d < distance )
        {
          index = i;
          distance = d;
        }
      }

      return points[ index ];
    }

    public static Vector3 GetClosest( Vector3 from, List<Vector3> points )
    {
      var distance = float.MaxValue;
      var index = -1;

      for ( int i = 0; i < points.Count; i++ )
      {
        var d = from.DistanceSquaredTo( points[ i ] );

        if ( d < distance )
        {
          index = i;
          distance = d;
        }
      }

      return points[ index ];
    }


    public static Vector3 LerpUnclamped( Vector3 a, Vector3 b, float amount )
    {
      return a + amount * ( b - a );
    }

    public static Vector3 LerpClamped( Vector3 a, Vector3 b, float amount )
    {
      return LerpUnclamped( a, b, MathX.Clamp01( amount ) );
    }

    public static Quaternion GetGlobalQuaternion( this Node3D node )
    {
      return GetGlobalRotationFrom( node );
    }

    public static Quaternion GetGlobalRotationFrom( Node3D node )
    {
      var quaternion = node.GlobalBasis.GetRotationQuaternion();

      return quaternion;
    } 

    public static Vector3 MinGlobalPosition( Node3D a, Node3D b )
    {
      return a.GlobalPosition.Min( b.GlobalPosition );
    }

    public static Vector3 MaxGlobalPosition( Node3D a, Node3D b )
    {
      return a.GlobalPosition.Max( b.GlobalPosition );
    }

    public static Vector3 MinLocalPosition( Node3D a, Node3D b )
    {
      return a.Position.Min( b.Position );
    }

    public static Vector3 MaxLocalPosition( Node3D a, Node3D b )
    {
      return a.Position.Max( b.Position );
    }

    public static Vector3 SnapRounded( Vector3 v, Vector3 snapping )
    {
      v.X = MathX.SnapRounded( v.X, snapping.X );
      v.Y = MathX.SnapRounded( v.Y, snapping.Y );
      v.Z = MathX.SnapRounded( v.Z, snapping.Z );

      return v;
    }

     public static Vector3 SnapRoundedXZ( Vector3 v, float snapX, float snapZ )
    {
      v.X = MathX.SnapRounded( v.X, snapX );
      v.Z = MathX.SnapRounded( v.Z, snapZ );

      return v;
    }

    public static Vector3 SnapCeiled( Vector3 v, Vector3 snapping )
    {
      v.X = MathX.SnapCeiled( v.X, snapping.X );
      v.Y = MathX.SnapCeiled( v.Y, snapping.Y );
      v.Z = MathX.SnapCeiled( v.Z, snapping.Z );

      return v;
    }

    public static Vector3 SnapFloored( Vector3 v, Vector3 snapping )
    {
      v.X = MathX.SnapFloored( v.X, snapping.X );
      v.Y = MathX.SnapFloored( v.Y, snapping.Y );
      v.Z = MathX.SnapFloored( v.Z, snapping.Z );

      return v;
    }

    public static Basis AlignUp( Basis basis, Vector3 upDirection )
    {
      basis.Y = upDirection;
      basis.X = - basis.Z.Cross( upDirection );
      return basis.Orthonormalized();
    }

    public static Quaternion AlignUp( Quaternion rotation, Vector3 upDirection )
    {
      var basis = new Basis( rotation );
      var aligned = AlignUp( basis, upDirection );
      return aligned.GetRotationQuaternion();
    }


    public static Quaternion AlignUp( Vector3 upDirection, Quaternion? q = null )
    {
      var quaternion = q == null ? Quaternion.Identity : (Quaternion) q;
      return AlignUp( quaternion, upDirection );
    }

    public static Quaternion AlignUpFromDirections( Vector3 upFrom, Vector3 upTo )
    {
      var fromRotation = AlignUp( upFrom );

      return AlignUp( fromRotation, upTo );
    }

    public static float AngleXY( Vector3 direction )
    {
      return Mathf.Atan2( direction.Y, direction.X );
    }

    public static float AngleXZ( Vector3 direction )
    {
      return Mathf.Atan2( direction.Z, direction.X );
    }

    public static Vector3 NormalAngle( float angle )
    {
      var y = Mathf.Sin( angle );
      var z = Mathf.Cos( angle );

      return new Vector3( 0, y, z );
    }

   


    public static Vector3 Lerp( Vector3 a, Vector3 b, float lerp )
    {
      return a.Lerp( b, lerp );
    }

    public static Vector3 LerpGlobalPosition( Node3D a, Node3D b, float lerp )
    {
      return a.GlobalPosition.Lerp( b.GlobalPosition, lerp );
    }

    public static Quaternion LookRotation( Vector3 direction, bool useModelFront = false )
    { 
      if ( direction.Normalized() == Vector3.Up )
      {
        return LookRotation( direction, Vector3.Back, useModelFront );
      }

      return LookRotation( direction, Vector3.Up, useModelFront );
    }

    public static Quaternion LookRotation( Vector3 direction, Vector3 up, bool useModelFront = false )
    {
      if ( direction.Length() == 0 )
      {
        return Quaternion.Identity;
      }
      
      var t = new Transform3D();
      t.Basis = Basis.Identity;
      t.Origin = Vector3.Zero;    

      t = t.LookingAt( direction, up, useModelFront );

      return t.Basis.GetRotationQuaternion();
    }

    public static Vector3 LerpComponents( Vector3 a, Vector3 b, Vector3 t )
    {
      return new Vector3( Mathf.Lerp( a.X, b.X, t.X ), Mathf.Lerp( a.Y, b.Y, t.Y ), Mathf.Lerp( a.Z, b.Z, t.Z ) );
    }

    public static Vector4 AsVector4( this Quaternion q)
    {
      return new Vector4( q.X, q.Y, q.Z, q.W );
    }

    public static Quaternion RotateX( float radians )
    {
      if ( radians == 0 ) { return Quaternion.Identity; }

      return Quaternion.FromEuler( new Vector3( radians, 0, 0 ) );
    }

    public static Quaternion RotateXDegrees( float degrees )
    {
      return RotateX( Mathf.DegToRad( degrees ) );
    }

    public static Quaternion RotateY( float radians )
    {
      if ( radians == 0 ) { return Quaternion.Identity; }

      return Quaternion.FromEuler( new Vector3( 0, radians, 0 ) );
    }

    public static Quaternion RotateYDegrees( float degrees )
    {
      return RotateY( Mathf.DegToRad( degrees ) );
    }

    public static Quaternion RotateZ( float radians )
    {
      if ( radians == 0 ) { return Quaternion.Identity; }
      
      return Quaternion.FromEuler( new Vector3( 0, 0, radians ) );
    }

    public static Quaternion RotateZDegrees( float degrees )
    {
      return RotateZ( Mathf.DegToRad( degrees ) );
    }

    public static Quaternion YawPitchRotation( float yaw, float pitch )
    {
      return RotateYDegrees( yaw ) * RotateXDegrees( pitch );
    }

    public static float GlobalYaw( Vector3 direction )
    {
      return Mathf.Atan2( direction.Z, direction.X );
    }

    public static float GlobalPitch( Vector3 direction )
    {
      var xz = new Vector2( direction.X, direction.Z ).Length();
      var y  = direction.Y; 

      return Mathf.Atan2( y, xz );
    }

    public static void SetGlobalQuaternion( this Node3D node, Quaternion quaternion )
    {
      var localScale = node.Scale;
      node.GlobalBasis = new Basis( quaternion );
      node.Scale = localScale;
    }

    public static void SetLocalQuaternion( this Node3D node, Quaternion quaternion )
    {
      var localScale = node.Scale;
      node.Basis = new Basis( quaternion );
      node.Scale = localScale;
    }

    public static void SetGlobalRotationTo( Node3D node, Quaternion quaternion )
    {
      var forward = quaternion * Vector3.Forward;
      var up      = quaternion * Vector3.Up;

      node.LookAt( node.GlobalPosition + forward, up );
    }

    public static void LookTowards( this Node3D node, Vector3 forwardDirection, Vector3 upDirection, Quaternion rotation )
    {
      //forwardDirection = rotation * forwardDirection;
      //upDirection      = rotation * upDirection;

      node.LookTowards( forwardDirection, upDirection );  
      node.SetGlobalQuaternion( node.GetGlobalQuaternion() * rotation );
    }

    public static void LookTowards( this Node3D node, Vector3 forward, Vector3 up, Vector3? up2 = null )
    {
      if ( forward == up )
      {
        up = up2 == null ? Vector3.Back : (Vector3)up2;
      }

      node.LookAt( forward + node.GlobalPosition, up );
    }


    public static Quaternion GetDifference( this Quaternion q, Quaternion other )
    {
      return GetQuaternionDifference( q, other );
    }

    public static Quaternion GetQuaternionDifference( Quaternion a, Quaternion b )
    {
      return b.Inverse() * a;
    } 

    public static Quaternion GetQuaternionFraction( Quaternion q, float fraction )
    {
      return Quaternion.Identity.Slerp( q, fraction );
    }

    public static Vector3 GetGlobalOffset( this Node3D node, Vector3 direction )
    {
      return direction.X * node.GlobalRight() + 
             direction.Y * node.GlobalUp() +
             direction.Z * node.GlobalForward() ;
    }

    public static Vector3 GlobalForward( this Node3D node )
    {
      return GetGlobalForward( node );
    }

    public static Vector3 GetGlobalForward( Node3D node )
    {
      return -node.GlobalBasis.Z;
    }

    public static Vector3 GetGlobalScale( Node3D node )
    {
      return node.GlobalTransform.Basis.Scale;
    }

    public static float GetGlobalUniScale( Node3D node )
    {
      var scale3 = GetGlobalScale( node );
      return MathX.Max( scale3.X, scale3.Y, scale3.Z );
    }

    public static Vector3 GlobalUp( this Node3D node )
    {
      return GetGlobalUp( node );
    }

    public static Vector3 GetGlobalUp( Node3D node )
    {
      return node.GlobalBasis.Y;
    }

     public static Vector3 GlobalRight( this Node3D node )
    {
      return GetGlobalRight( node );
    }

    public static Vector3 GetGlobalRight( Node3D node )
    {
      return node.GlobalBasis.X;
    }

    public static Vector3 GetYPlaneForward( Node3D node )
    {
      var forward = GetGlobalForward( node );

      forward.Y = 0;

      return forward.Normalized();
    }

    public static Vector3 GetYPlaneRight( Node3D node )
    {
      var right = GetGlobalRight( node );

      right.Y = 0;

      return right.Normalized();
    }

    public static void SetGlobalX( this Node3D node, float x )
    {
      var gp = node.GlobalPosition;

      gp.X = x;

      node.GlobalPosition = gp;
    }

    public static void SetGlobalY( this Node3D node, float y )
    {
      var gp = node.GlobalPosition;

      gp.Y = y;

      node.GlobalPosition = gp;
    }

    public static void SetGlobalZ( this Node3D node, float z )
    {
      var gp = node.GlobalPosition;

      gp.Z = z;

      node.GlobalPosition = gp;
    }

    public static Vector3 Average( List<Vector3> vectors )
    {
      var average = Vector3.Zero;

      vectors.ForEach( v => average += v );

      if ( average == Vector3.Zero )
      {
        return vectors[ 0 ];
      }

      return ( average / vectors.Count ).Normalized();
    }

    public static Vector3 BlendNormals( Vector3 a, Vector3 b, float amount )
    {
      if ( amount <= 0 )
      {
        return a;
      }

      if ( amount >= 1 )
      {
        return b;
      }

      var n = a.Lerp( b, amount );
      var length = n.Length();

      if ( length > 0 )
      {
        return n / length;
      }

      return amount <= 0.5 ? a : b;
    }

    public static Aabb? GetWorldBounds( this Node3D node, bool onlyVisible = true )
    {
      return GetWorldBoundsFrom( node, onlyVisible );
    }

    public static Aabb? GetWorldBoundsFrom( Node3D node, bool onlyVisible = true )
    {
      Aabb? worldBounds = null;

      Nodes.ForEach<VisualInstance3D>( node,
        ( vi )=>
        {
          if ( onlyVisible && ! vi.IsVisibleInTree() )
          {
            return;
          }

          var nBounds = vi.GetAabb();

          nBounds.Size *= GetGlobalUniScale( vi );
          nBounds.Position += vi.GlobalPosition;
          nBounds.End += vi.GlobalPosition;

          worldBounds = worldBounds == null ? nBounds : ( ((Aabb)worldBounds).Merge( nBounds ) );
        }
      );

      return worldBounds;
    }
  }

}