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

namespace Rokojori
{

  

  public enum PointInPathResult
  {
    INSIDE,
    OUTSIDE,
    ERROR
  }

  

  public class Path2
  {
    float POINT_TO_CLOSE_DISTANCE = 0.0001f;
    float ANGLE_TO_SIMILAR_TRESHOLD = 0.00001f / 180f;
    int NUM_POINT_IN_PATH_TEST_RETRIES = 5;

    List<Vector2> _points = new List<Vector2>();

    public List<Vector2> points => _points;
    

    public Path2( List<Vector2> points )
    {
      this._points = points;
    }

    public override string ToString()
    {    
      return "Rokojori.Path2 with " + points.Count + " points" ; 
    }


    public Path2( Vector2[] points )
    {
      this._points = new List<Vector2>( points );
    }

    public void ApplyTransform( Transform2D transform2D )
    {
      for ( int i = 0; i < _points.Count; i++ )
      {
        _points[ i ] = transform2D * _points[ i ]; 
      }      
    }

    public void MirrorX( float offset = 0 )
    {
      for ( int i = 0; i < _points.Count; i++ )
      {
        var p = _points[ i ];
        p.X -= offset;
        p.X = - _points[ i ].X;
        p.X += offset;

        _points[ i ] = p;
      } 
    }

    public int numPoints => _points.Count;
    public bool empty => _points.Count == 0;

    public bool cacheProperties = true;
    
    Vector2? min;
    Vector2? max;
    bool? clockwise;

    public void ClearCachedProperties()
    {
      min = null;
      max = null;
      clockwise = null;
    }

    public void SetWindingDirection( bool clockwise )
    {
      if ( isClockwise == clockwise )
      {
        return;
      }

      _points.Reverse();

      this.clockwise = clockwise;
    }

    public bool isClockwise
    {
      get 
      {      
      
        if ( cacheProperties )
        {
          if ( clockwise == null )
          {
            clockwise = Geometry2D.IsPolygonClockwise( points.ToArray() );
          }

          return (bool) clockwise;
        }

        return Geometry2D.IsPolygonClockwise( points.ToArray() );

      }
    }

    public Shape2 ToShape2()
    {
      var s = new Shape2();
      s.paths = new List<Path2>();
      s.paths.Add( this );
      return s;
    }


    public Vector2 leftTop
    {
      get 
      {
        if ( cacheProperties && min != null )
        {
          return (Vector2) min;
        }
        
        var _min = _points[ 0 ];
        
        for ( int i = 1; i < _points.Count; i++ )
        {
          var point = _points[ i ];
          _min.X = Mathf.Min( point.X, _min.X );
          _min.Y = Mathf.Min( point.Y, _min.Y );
        } 

        min = _min;

        return (Vector2) min;

      }
    }

    public Vector2 rightBottom
    {
      get 
      {

         if ( cacheProperties && max != null )
        {
          return (Vector2) max;
        }

        var _max = _points[ 0 ];
        
        for ( int i = 1; i < _points.Count; i++ )
        {
          var point = _points[ i ];
          _max.X = Mathf.Max( point.X, _max.X );
          _max.Y = Mathf.Max( point.Y, _max.Y );
        }

        max = _max;
        
        return (Vector2) max;

      }
    }

    public Vector2 center => ( leftTop + rightBottom ) / 2;

    public void AddToNavigationPolygon( NavigationPolygon polygon )
    { 
      var convexPolygons = Geometry2D.DecomposePolygonInConvex( points.ToArray() );

      for ( int i = 0; i < convexPolygons.Count; i++ )
      {
        var vertices = convexPolygons[ i ];
        polygon.SetVertices( vertices );
        polygon.AddPolygon( CreateIndices( vertices.Length ) );
      }
      
    }

    public static int[] CreateIndices( int amount )
    {
      var indices = new int[ amount ];

      for ( int i = 0; i < amount; i++ )
      {
        indices[ i ] = i;
      }

      return indices;
    } 

    public static Path2 AsXZFrom( Curve3D curve3D, bool closed, int resolution = 20 )
    {
      var points = new List<Vector2>();

      var numPoints = resolution;

      var normalizer = 1f / resolution;

      for ( int i = 0; i < numPoints; i++ )
      {
        points.Add( Math2D.XZ( curve3D.Samplef( i * normalizer ) ) );
      }

      if ( closed )
      {
        points.Add( points[ 0 ] );
      }

      return new Path2( points );
    }

    public static Path2 AsXZFrom( Curve3 curve3, bool closed, int resolution = 20 )
    {
      var points = new List<Vector2>();

      var numPoints = resolution;

      var normalizer = 1f / resolution;

      for ( int i = 0; i < numPoints; i++ )
      {
        points.Add( Math2D.XZ( curve3.PositionAt( i * normalizer ) ) );
      }

      if ( closed )
      {
        points.Add( points[ 0 ] );
      }

      return new Path2( points );
    }

    public bool PointInPath( Vector2 p, bool fast = true, bool checkBoundingBox = true )
    {
      if ( fast )
      {
        return PointInPathFast( p, checkBoundingBox );
      }
      else
      {
        return PointInPathReliable( p, checkBoundingBox );
      }
    }

    public bool PointInPathFast( Vector2 point, bool checkBoundingBox = true )
    { 
      var min = this.leftTop;
      var max = this.rightBottom;

      if ( checkBoundingBox )
      {        
        if ( ! Box2.ContainsPoint( min, max, point ) )
        {
          return false;
        }
      }

      return CheckPointInPathFast( point, max + new Vector2( 122.133544f, 129.45423f ) );
    }

    public Path2 Clone()
    {     
      return new Path2( new List<Vector2>( _points ) );
    }

    public bool PointInPathReliable( Vector2 point, bool checkBoundingBox = true )
    {
      var min = this.leftTop;
      var max = this.rightBottom;

      if ( checkBoundingBox )
      {        
        if ( ! Box2.ContainsPoint( min, max, point ) )
        {
          return false;
        }

      }

      var endPoint = max + new Vector2( 0.1234235f, 0.4211322f );     
      var result = CheckPointInPath( point, endPoint );

      if ( PointInPathResult.ERROR != result )
      { 
        return PointInPathResult.INSIDE == result;
      }
      
      var rightTop = new Vector2( max.X, min.Y );
      endPoint = rightTop + new Vector2( 0.03234235f, -0.90211322f );   
      result = CheckPointInPath( point, endPoint );
      
      if ( PointInPathResult.ERROR != result )
      {
        return PointInPathResult.INSIDE == result;
      }
      
      var centerTop = new Vector2( ( min.X + max.X ) * 0.5f , min.Y );
      endPoint = centerTop + new Vector2( 0.013234235f, -0.90211322f );   
      result = CheckPointInPath( point, endPoint );
      
      if ( PointInPathResult.ERROR != result )
      {
        return PointInPathResult.INSIDE == result;
      }
     
      var rightMiddle = new Vector2( max.X , ( min.Y + max.Y ) * 0.5f);
      endPoint = rightMiddle + new Vector2( 0.013234235f, 0.00211322f );   
      result = CheckPointInPath( point, endPoint );      

      if ( PointInPathResult.ERROR != result )
      {
        return PointInPathResult.INSIDE == result;
      }

      endPoint = leftTop + new Vector2( -0.01053535f, -0.41005465f );   
      result = CheckPointInPath( point, endPoint );

      if ( PointInPathResult.ERROR != result )
      {
        return PointInPathResult.INSIDE == result;
      }

      for ( int i = 0; i < NUM_POINT_IN_PATH_TEST_RETRIES; i++ )
      {
        var randomX = GodotRandom.Get().Next();
        var randomY = GodotRandom.Get().Next();

        var randomPoint = max + new Vector2( randomX, randomY );     
        result = CheckPointInPath( point, endPoint );

        if ( PointInPathResult.ERROR != result )
        {
          return PointInPathResult.INSIDE == result;
        }
      } 

      return false; 
    }

    public bool CheckPointInPathFast( Vector2 point, Vector2 endPoint )
    {
      var pointLine = new Line2( point, endPoint );

      var iterationLine = new Line2();
            
      var intersections = 0;

      for ( int i = 0; i < _points.Count; i++ )
      {
        var start = _points[ i ];

        var nextIndex = ( i == _points.Count - 1 ) ? 0 : ( i + 1 );
        var end   = _points[ nextIndex ];     
        

        iterationLine.start = start;        
        iterationLine.end   = end;

        var intersects = pointLine.IntersectsWith( iterationLine );

        if ( intersects )
        {
          intersections ++;
        }
        
      }

      
      return intersections % 2 != 0;
    }

    public List<Vector3> AsTriangleFan( bool close = false )
    {
      var list = new List<Vector3>();

      var c = center;
      var end = close ? points.Count : ( points.Count - 1 );

      for ( int i = 0; i < end; i++ )
      {
        var n = ( i + 1 ) % points.Count; 

        var v0 = points[ i ];
        var v1 = points[ n ];
        var v2 = center;

        list.Add( Math3D.XYasXZ( v0 ) );
        list.Add( Math3D.XYasXZ( v1 ) );
        list.Add( Math3D.XYasXZ( v2 ) );

      }

      return list;
    }
    
    public void Reverse()
    {
      _points.Reverse();
      ClearCachedProperties();
    }

    public void PositionJitter( float max, Vector2 scale, Vector2 offset, RandomEngine random = null )
    {
      if ( random == null )
      {
        random = GodotRandom.Get();
      }

      for ( int i = 0; i < _points.Count; i++ )
      {
        _points[ i ] = Noise.PerlinOffset( _points[ i ], max, scale, offset, random );
      }
    }


    public string ToSVGPath()
    {
      var pathCommands = new List<string>();

      pathCommands.Add( "M " + SVGPathCommand.SVGCoordinate( points[ 0 ] ) );

      for ( int i = 1; i < points.Count; i++ )
      {
        pathCommands.Add( "L " + SVGPathCommand.SVGCoordinate( points[ i ] ) );
      }

      return Lists.Join( pathCommands, " ");
    }

    public MeshGeometry CreateMeshGeometry()
    {
      var geometry = new MeshGeometry();
      geometry.Initialize();
      
      var convexShapes = Convex2.PathToConvexList( this );

      for ( int i = 0; i < convexShapes.Count; i++ )
      {
        geometry.AddConvex2( convexShapes[ i ] );
      }

      return geometry;
    }

    PointInPathResult CheckPointInPath( Vector2 point, Vector2 endPoint )
    {
      var pointLine = new Line2( point, endPoint );
      var pointAngle = pointLine.angle;
      var pointAngle2 = pointLine.reverseAngle;

      var iterationLine = new Line2();
      
      var intersections = 0;

      for ( int i = 0; i < _points.Count; i++ )
      {
        var start = _points[ i ];

        if ( pointLine.DistanceToPoint( start ) < POINT_TO_CLOSE_DISTANCE )
        {
          return PointInPathResult.ERROR;
        } 

        var nextIndex = ( i == _points.Count - 1 ) ? 0 : ( i + 1 );
        var end   = _points[ nextIndex ];     
        

        iterationLine.start = _points[ i ];        
        iterationLine.end = _points[ nextIndex ];

        var iterationAngle = iterationLine.angle;
        var angleDifference = AbsoluteDeltaAngleFromRadiansToDegrees( pointAngle, iterationAngle );

        if ( angleDifference < ANGLE_TO_SIMILAR_TRESHOLD )
        {
          return PointInPathResult.ERROR;
        } 

        angleDifference = AbsoluteDeltaAngleFromRadiansToDegrees( pointAngle2, iterationAngle );

        if ( angleDifference < ANGLE_TO_SIMILAR_TRESHOLD )
        {
          return PointInPathResult.ERROR;
        } 


        var intersects = pointLine.IntersectsWith( iterationLine );

        if ( intersects )
        {
          intersections ++;
        }
        
      }

      if ( intersections % 2 != 0 )
      {
        return PointInPathResult.INSIDE;
      }

      return PointInPathResult.OUTSIDE;

    }

    public static Path2 Circle( Vector2 center, float radius = 1, int resolution = 36 )
    {
      var points = new List<Vector2>();

      for ( int i = 0; i < resolution; i++ )
      {
        var t = i / ( float ) ( resolution );
        var phase = t * Mathf.Pi * 2;
        var x = Mathf.Cos( phase ) * radius;
        var y = Mathf.Sin( phase ) * radius;

        points.Add( new Vector2( x, y ) + center );
      }

      return new Path2( points );
    }
    

    public static float AbsoluteDeltaAngleFromRadiansToDegrees( float rA, float rB )
    {
      var dA = Mathf.RadToDeg( rA );
      var dB = Mathf.RadToDeg( rB );

      return MathX.AbsoluteDeltaAngle( dA, dB );
    }


    public static Shape2 Union( Path2 a, Path2 b )
    {
      var paths = Geometry2D.MergePolygons( a._points.ToArray(), b._points.ToArray() );

      var s = new Shape2();

      for ( int i = 0; i < paths.Count; i++ )
      {
        s.paths.Add( new Path2( paths[ i ] ) );
      }

      return s;
    }

    const double DefaultClipperLibraryScale = 10e8;

    public static Path2 ToLinearXZPath( Path3D path3D )
    {
      var curve = path3D.Curve;

      var points = new List<Vector2>();

      for ( int i = 0; i < curve.PointCount; i++ )
      {
        var point = path3D.ToGlobal( curve.GetPointPosition( i ) );

        points.Add( Math2D.XZ( point ) );
      }

      return new Path2( points );

    }

    public static List<ClipperLib.IntPoint> ToClipperPath( Path2 path, double scale = Path2.DefaultClipperLibraryScale )
    {
      return Lists.Map( 
        path.points, p => 
        {
          var x = p.X * scale;
          var y = p.Y * scale;

          return new ClipperLib.IntPoint( x, y );
        }
      );
    }

    public static Path2 FromClipperPath( List<ClipperLib.IntPoint> clipperPath, double scale = Path2.DefaultClipperLibraryScale )
    {
      var points = Lists.Map( 
        clipperPath, p => 
        {
          var x = p.X / scale;
          var y = p.Y / scale;

          return new Vector2( (float)x, (float)y );
        }
      );

      return new Path2( points );
    }

    public static Shape2 Boolean( Path2 a, Path2 b, Geometry2D.PolyBooleanOperation booleanOperation, bool simplify = true )
    {
      // RJLog.Log( "Using Clipper Library" );

      var clipperPathA = ToClipperPath( a );
      var clipperPathB = ToClipperPath( b );

      var resultPaths = new List<List<ClipperLib.IntPoint>>();

      var type = ClipperLib.ClipType.ctUnion;

      if ( Geometry2D.PolyBooleanOperation.Difference == booleanOperation )
      {
        type = ClipperLib.ClipType.ctDifference;
      }
      else if ( Geometry2D.PolyBooleanOperation.Intersection == booleanOperation )
      {
        type = ClipperLib.ClipType.ctIntersection;
      }
      else if ( Geometry2D.PolyBooleanOperation.Xor == booleanOperation )
      {
        type = ClipperLib.ClipType.ctXor;
      }

			var clipper = new ClipperLib.Clipper();
			clipper.AddPath( clipperPathA, ClipperLib.PolyType.ptSubject, true); 
			clipper.AddPath( clipperPathB, ClipperLib.PolyType.ptClip, true); 
			clipper.Execute( type, resultPaths );

      if ( simplify )
      {
        resultPaths = ClipperLib.Clipper.SimplifyPolygons( resultPaths );
      }
      

      var s = new Shape2();

      resultPaths.ForEach(
        ( r ) =>
        {
          s.paths.Add( FromClipperPath( r ) );
        }
      );

      return s;
    }


  } 
}