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


namespace Rokojori
{

  public class Triangle3
  {   
    public Vector3 a;
    public Vector3 b;
    public Vector3 c;

    public Triangle3( Vector3 a, Vector3 b, Vector3 c )
    {
      this.a = a;
      this.b = b;
      this.c = c;

      _needsUpdate = true;
    }

    public void SetFrom( MeshGeometry mg, int a, int b, int c )
    {
      this.a = mg.vertices[ a ];
      this.b = mg.vertices[ b ];
      this.c = mg.vertices[ c ];

      _needsUpdate = true;
    }

    public static Triangle3 CreateFrom( MeshGeometry mg, int a, int b, int c )
    {
      var t = new Triangle3( 
        mg.vertices[ a ],
        mg.vertices[ b ],
        mg.vertices[ c ]
      );

      return t;
    }

    public static Triangle3 CreateFrom( MeshGeometry mg, int triangleIndex )
    {
      var triangleOffset = triangleIndex;

      var t = new Triangle3( 
        mg.vertices[ triangleOffset ],
        mg.vertices[ triangleOffset + 1],
        mg.vertices[ triangleOffset + 2 ]
      );

      return t;
    }

    public void SetFrom( MeshGeometry mg, int triangleIndex )
    {
      var triangleOffset = triangleIndex;

      SetFrom( mg, triangleOffset, triangleOffset + 1, triangleOffset + 2 );
    }

    public float perimeter
    {
      get
      {
        var sideA = ( b - a ).Length();
        var sideB = ( c - b ).Length();
        var sideC = ( a - c ).Length();
      
        return sideA + sideB + sideC;
      }
    }

    public float semiperimeter => perimeter * 0.5f;

    Vector3 _center;
    Sphere _boundingSphere;
    Plane3 _plane;
    Vector3 _normal;

    bool _needsUpdate = false;

    public void NeedsUpdate()
    {
      _needsUpdate = true;
    }


    public Vector3 center
    {
      get 
      {
        Update();
        return _center;
      }
    }

    public Sphere boundingSphere 
    {
      get 
      {      
        Update();
        return _boundingSphere;
      }
    }

    void Update()
    {
      if ( ! _needsUpdate )
      {
        return;
      }

      var m = Math3D.Center( a, b, c );
      var radius = Mathf.Sqrt( MathX.Min( ( m - a ).LengthSquared(), ( m - b ).LengthSquared(), ( m  -c ).LengthSquared() ) );
      _boundingSphere = new Sphere( m, radius );

      _plane = new Plane3(); 
      _plane.SetFromCoplanarPoints( a, b, c );

      _normal = ( b - a ).Cross( c - a ).Normalized(); 

      _center = ( a + b + c ) / 3f;

      _needsUpdate = false;
    }

    public static float ComputeTriangleArea( Vector3 a, Vector3 b, Vector3 c )
    {
      var sideA = ( b - a ).Length();
      var sideB = ( c - b ).Length();
      var sideC = ( a - c ).Length();
    

      var perimeter = sideA + sideB + sideC;
      var semiperimeter = 0.5f * perimeter;
      var areaValue = Mathf.Sqrt( semiperimeter * ( semiperimeter - sideA ) 
                                                * ( semiperimeter - sideB ) 
                                                * ( semiperimeter - sideC )) ;

      return areaValue;   
    }

    public float area => ComputeTriangleArea( a, b, c );
   
    public bool Intersects( Line3 line )
    {
      var planePoint = _plane.IntersectLine( line );

      if ( planePoint == null )
      {
        return false;
      }

      return InsideTriangle( (Vector3) planePoint, a, b, c );
    } 

    public Vector3? GetIntersection( Line3 line )
    {
      Update();

      var planePoint = _plane.IntersectLine( line );

      if ( planePoint == null )
      {
        return null;
      }

      if ( ! InsideTriangle( (Vector3) planePoint, a, b, c ) )
      {
        return null;
      }

      return planePoint;
    }

    public bool PointInside( Vector3 p )
    {
      return InsideTriangle( p, a, b, c );
    } 
  
    public Vector3 ClosestPoint( Vector3 p )
    {
      Update();

      var planePoint = _plane.ClosestPointTo( p );

      if ( PointInside( planePoint ) )
      {
        return planePoint;
      }

      return ClosestPointToEdges( p );

    }

    public Vector3? GetPointOnPlaneInsideTriangle( Vector3 p )
    {
      Update();

      var planePoint = _plane.ClosestPointTo( p );

      if ( PointInside( planePoint ) )
      {
        return planePoint;
      }

      return null;
    }

    Vector3[] closestPoints = new Vector3[3];

    public Vector3 ClosestPointToEdges( Vector3 p )
    {
      closestPoints[ 0 ] = Line3.ClosestPointOf( p, a, b );
      closestPoints[ 1 ] = Line3.ClosestPointOf( p, b, c );
      closestPoints[ 2 ] = Line3.ClosestPointOf( p, c, a );

      return Math3D.GetClosest( p, closestPoints );
    }

    public Vector3 ClosestPoint( Line3 line )
    {
      var intersection = GetIntersection( line );

      if ( intersection != null )
      {
        return (Vector3) intersection;
      }

      var ab = new Line3( a, b ).ClosestPointTo( line );
      var bc = new Line3( b, c ).ClosestPointTo( line );
      var ca = new Line3( c, a ).ClosestPointTo( line );

      return line.ClosestPointTo( ab, bc, ca );
    }

    public float GetHeightOfPoint( int i )
    {
      var p = GetPoint( i );

      var b = GetPoint( ( i + 1 ) % 3 );
      var a = GetPoint( ( i + 2 ) % 3 );

      var ab = Line3.ClosestPointOf( p, a, b );

      return ( p - ab ).Length();
    }

    public float GetArea( int i = 0 )
    {
      var p = GetPoint( i );

      var b = GetPoint( ( i + 1 ) % 3 );
      var a = GetPoint( ( i + 2 ) % 3 );

      var ab = Line3.ClosestPointOf( p, a, b );

      var h =  ( p - ab ).Length();

      return ( ( a - b ).Length() * h ) / 2f;
    }

    public LerpCurve3 GetOutline()
    {
      return LerpCurve3.FromPoints( GetPoint( 0 ), GetPoint( 1 ), GetPoint( 2 ) );
    }

    public Vector3 GetPoint( int i )
    {
      if ( i == 0 )
      {
        return a;
      }
      else if ( i == 1 )
      {
        return b;
      }
      else if ( i == 2 )
      {
        return c;
      }

      return a;
    }

    void SetPointsToLine( Line3 line, int index )
    {
      line.Set( GetPoint( index ), GetPoint( ( index + 1 ) % 3 ) );
    }

    public Line3 GetIntersectionLine( Triangle3 other )
    {     
      var ownSphere   = boundingSphere;
      var otherSphere = other.boundingSphere;

      if ( ! ownSphere.IntersectsSphere( otherSphere ) )
      {
        return null;
      }

      var line = new Line3();

      var intersections = new List<Vector3>();

      for ( int i = 0; i < 3; i++ )
      {
        SetPointsToLine( line, i );
        var intersection = other.GetIntersection( line );        

        if ( intersection != null )
        {
          intersections.Add( (Vector3) intersection );
        }
      }

      if ( intersections.Count == 2 )
      {
        line.Set( intersections[ 0 ], intersections[ 1 ] );
        return line;
      }

      for ( int i = 0; i < 3; i++ )
      {
        other.SetPointsToLine( line, i );
        var intersection = GetIntersection( line );        

        if ( intersection != null )
        {
          intersections.Add( (Vector3) intersection );
        }
      }

      if ( intersections.Count == 2 )
      {
        line.Set( intersections[ 0 ], intersections[ 1 ] );
        return line;
      }

      return null;


    }

    public bool Intersects( Triangle3 other )
    {
      var ownSphere   = boundingSphere;
      var otherSphere = other.boundingSphere;

      if ( ! ownSphere.IntersectsSphere( otherSphere ) )
      {
        return false;
      }

      var line = new Line3();
      
      for ( int i = 0; i < 3; i++ )
      {
        if ( PointInside( other.GetPoint( i ) ) )
        {
          return true;
        }

        if ( other.PointInside( GetPoint( i ) ) )
        {
          return true;
        }

        SetPointsToLine( line, i );

        if ( other.Intersects( line ) )
        {
          return true;
        }

        other.SetPointsToLine( line, i );

        if ( Intersects( line ) )
        {
          return true;
        }
      }

      return false;
    }

    public void Rotate( Quaternion q, Vector3? pivot = null )
    {
      if ( pivot != null )
      {
        Translate( -( (Vector3)pivot ));
      }

      a = a * q;
      b = b * q;
      c = c * q;

      if ( pivot != null )
      {
        Translate( ( (Vector3)pivot ));
      }
    }

    public Triangle3 Clone()
    {
      return new Triangle3( a, b, c );
    }

    public Vector3 normal 
    {
      get
      {
        Update();
        return _normal;
      }

    }
    
    public Quaternion GetXZAlignmentRotation()
    {
      return Math3D.AlignUp( normal );
    }

    public void ScaleFromCenter( float scale )
    {
      var cnt = center;
      a = ( a - cnt ) * scale + cnt; 
      b = ( b - cnt ) * scale + cnt; 
      c = ( c - cnt ) * scale + cnt;       
    }

    public Triangle3 Shrink( float distance )
    {
      var n = normal;

      var t = Offset( distance );

      if ( t == null || Math3D.LookingAtEachOther( t.normal, n ) )
      {
        return null;
      }

      return t;
      
    }

    public Triangle3 Offset( float distance )
    {
      var rotationXZ = GetXZAlignmentRotation();
      var clone = Clone();
      var originalCenter = center;

      clone.Translate( - originalCenter );
      clone.Rotate( rotationXZ );
      

      var t2 = Triangle2.AsXZ( clone );

      var shrinked = t2.Shrink( distance );

      if ( shrinked == null )
      {
        return null;
      }

      clone = XYasXZ( shrinked );
      clone.Rotate( rotationXZ.Inverse() );
      clone.Translate( originalCenter );

      return clone;
    }

    public void Translate( Vector3 translation )
    {
      a += translation;
      b += translation;
      c += translation;
    }

    public static Triangle3 XYasXZ( Triangle2 t )
    {
      return new Triangle3( Math3D.XYasXZ( t.a ), Math3D.XYasXZ( t.b ), Math3D.XYasXZ( t.c ) );
    }


    
    public static bool InsideTriangle( Vector3 point, Vector3 a, Vector3 b, Vector3 c ) 
    {
      var bary = GetBaryCentricCoordinate( point, a, b, c );

      if ( bary == null )
      {
        return false;
      }

      var values = (Vector3) bary;

      var insideV = Range.Contains( values.Y, 0, 1 );
      var insideU = Range.Contains( values.Z, 0, 1 );
      var insideX = Range.Contains( values.X, 0, 1 );

      var result = insideX && insideV && insideU;
      return result;

    }

    public static Vector3? GetBaryCentricCoordinate( Vector3 point, Vector3 a, Vector3 b, Vector3 c ) 
    {
  		var _v0 = c - a;
  		var _v1 = b - a;
  		var _v2 = point - a;

  		var dot00 = Math3D.Dot( _v0, _v0 );
  		var dot01 = Math3D.Dot( _v0, _v1 );
  		var dot02 = Math3D.Dot( _v0, _v2 );
  		var dot11 = Math3D.Dot( _v1, _v1 );
  		var dot12 = Math3D.Dot( _v1, _v2 );

  		var denom = ( dot00 * dot11 - dot01 * dot01 );

  		if ( denom == 0 ) 
      {
  			return null; 
  		}

  		var invDenom = 1f / denom;

  		var u = ( dot11 * dot02 - dot01 * dot12 ) * invDenom;
  		var v = ( dot00 * dot12 - dot01 * dot02 ) * invDenom;

  		return new Vector3( 1f - u - v, v, u );
  	}

   
  }

}