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



namespace Rokojori
{
  public class MeshGeometry
  {
    public string name = "";
    public List<Vector3> vertices = new List<Vector3>();
    public List<int> indices = new List<int>();

    public List<Vector3> normals;
    public List<Vector2> uvs;
    public List<Vector2> uv2s;
    public List<Color> colors;

    public int numTriangles => indices.Count / 3;
    

    public static MeshGeometry From( MeshInstance3D meshInstance3D )
    {
      return From( (ArrayMesh)meshInstance3D.Mesh, meshInstance3D.GlobalTransform );
    }

    public static MeshGeometry From( ArrayMesh mesh, Transform3D? trsf = null, int index = 0 )
    {
      var mg = new MeshGeometry();

      var arrays = mesh.SurfaceGetArrays( index );

      var vertices = arrays[ (int) Mesh.ArrayType.Vertex ];

      if ( Variant.Type.Nil != vertices.VariantType  )
      {
        mg.vertices = new List<Vector3>( vertices.AsVector3Array() );
      }

      var normals = arrays[ (int) Mesh.ArrayType.Normal ];

      if ( Variant.Type.Nil != normals.VariantType  )
      {
        mg.normals = new List<Vector3>( normals.AsVector3Array() );
      }

      var uvs = arrays[ (int) Mesh.ArrayType.TexUV ];

      if ( Variant.Type.Nil != uvs.VariantType  )
      {
        mg.uvs = new List<Vector2>( uvs.AsVector2Array() );
      }

      var uv2s = arrays[ (int) Mesh.ArrayType.TexUV2 ];

      if ( Variant.Type.Nil != uv2s.VariantType  )
      {
        mg.uv2s = new List<Vector2>( uv2s.AsVector2Array() );
      }

      var colors = arrays[ (int) Mesh.ArrayType.Color ];

      if ( Variant.Type.Nil != colors.VariantType  )
      {
        mg.colors = new List<Color>( colors.AsColorArray() );
      }

      var indices = arrays[ (int) Mesh.ArrayType.Index ];

      if ( Variant.Type.Nil != indices.VariantType  )
      {
        mg.indices = new List<int>( indices.AsInt32Array() );
      }

      if ( trsf != null )
      {
        mg.ApplyTransform( (Transform3D)trsf );
      }

      return mg;
    }

    public void ApplyTransform( Vector3 translation, Quaternion rotation, Vector3 scale, int start = 0, int length = -1 )
    {
      ApplyTransform( Math3D.TRS( translation, rotation, scale ), start, length );
    }

    public void ApplyTranslation( Vector3 translation, int start = 0, int length = -1 )
    {
      if ( start < 0 )
      {
        start = vertices.Count + start;
      }

      length = length < 0 ? ( vertices.Count - start ) : length;

      var end = start + length;
      
      for ( int i = start; i < end; i++ )
      {
        vertices[ i ] = vertices[ i ] + translation;
      }
    }


   public void ApplyUVTransform( Transform2D transform, int start = 0, int length = -1 )
    {
      if ( start < 0 )
      {
        start = vertices.Count + start;
      }

      length = length < 0 ? ( vertices.Count - start ) : length;

      var end = start + length;

      for ( int i = start; i < end; i++ )
      {
        uvs[ i ] = transform * uvs[ i ];
      }
    }


    public void ApplyTransform( Transform3D trsf, int start = 0, int length = -1 )
    {
      if ( start < 0 )
      {
        start = vertices.Count + start;
      }

      length = length < 0 ? ( vertices.Count - start ) : length;

      var end = start + length;

      for ( int i = start; i < end; i++ )
      {
        vertices[ i ] = trsf * vertices[ i ];
        normals[ i ] = trsf.Basis.GetRotationQuaternion() * normals[ i ];
      }
    }

    public void ApplyTransformWithPivot( Transform3D trsf, Vector3 pivot, int start = 0, int length = -1 )
    {
      if ( start < 0 )
      {
        start = vertices.Count + start;
      }

      length = length < 0 ? ( vertices.Count - start ) : length;

      var end = start + length;

      for ( int i = start; i < end; i++ )
      {
        vertices[ i ] = ( trsf * ( vertices[ i ] - pivot ) ) + pivot;
        normals[ i ] = trsf.Basis.GetRotationQuaternion() * normals[ i ];
      }
    }

    public void NormalsLookAt( Vector3 point, float amount )
    {
      for ( int i = 0; i < normals.Count; i++ )
      {
        var direction = ( vertices[ i ] - point ).Normalized();
        normals[ i ] = Math3D.BlendNormals( normals[ i ], direction, amount );
      }
    }

    public void BlendNormals( Vector3 direction, float amount )
    {
      if ( amount <= 0 )
      {
        return;
      }

      if ( amount >= 1 )
      {
        for ( int i = 0; i < normals.Count; i++ )
        {
          normals[ i ] = direction;
        }
        
        return;
      }

      for ( int i = 0; i < normals.Count; i++ )
      {
        normals[ i ] = Math3D.BlendNormals( normals[ i ], direction, amount );
      }
    }

    public Range GetRangeY()
    {
      var minY = float.MaxValue;
      var maxY = -float.MaxValue;

      for ( int i = 0; i < vertices.Count; i++ )
      {
        minY = Mathf.Min( minY, vertices[ i ].Y );
        maxY = Mathf.Max( maxY, vertices[ i ].Y );
      } 

      return new Range( minY, maxY );
    }

    public void BlendNormalsOverY( Vector3 direction, float amount, float startY, float endY, Curve curve )
    {
      if ( amount <= 0 )
      {
        return;
      }

      for ( int i = 0; i < normals.Count; i++ )
      {
        var yPositionAmount = MathX.RemapClamped( vertices[ i ].Y, startY, endY, 0, 1 );
        var blendAmount = curve.Sample( yPositionAmount ) * amount; 
        blendAmount = MathX.Clamp01( blendAmount );
        normals[ i ] = Math3D.BlendNormals( normals[ i ], direction, blendAmount );
      }
    }

    public void Offset( Vector3 offset )
    {
      for ( int i = 0; i < vertices.Count; i++ )
      {
        vertices[ i ] = vertices[ i ] + offset;
      }
    }

    public void ForEachTriangle( Action<int,int,int,int> callback )
    {
      var index = 0;

      for ( int i = 0; i < indices.Count; i += 3 )
      {
        var a = indices[ i + 0 ];
        var b = indices[ i + 1 ];
        var c = indices[ i + 2 ];

        callback( index, a, b, c );

        index ++;
      }
    }

    static int GetUVIndex( int u, int v, int segments )
    {
      return u * ( segments + 1 ) + v;
    }

   
    public static MeshGeometry CreateFromUVFunction( Func<Vector2,Pose> uv, int uSegments, int vSegments, bool fullUVQuads = false )
    {
      var mg = new MeshGeometry();

      if ( fullUVQuads )
      {
        var uv00 = new Vector2( 0, 0 );
        var uv10 = new Vector2( 1, 0 );
        var uv01 = new Vector2( 0, 1 );
        var uv11 = new Vector2( 1, 1 );

        for ( int i = 0; i < uSegments; i++ )
        {
          var u0 = (float)i / uSegments;
          var u1 = (float)(i + 1 ) / uSegments; 

          for ( int j = 0; j < vSegments; j++ )
          {
            var v0 = (float)j / vSegments;
            var v1 = (float)(j + 1 ) / vSegments; 

            var p00 = uv( new Vector2( u0, v0 ) );
            var p10 = uv( new Vector2( u1, v0 ) );
            var p01 = uv( new Vector2( u0, v1 ) );
            var p11 = uv( new Vector2( u1, v1 ) );
            var n = ( p00.up + p10.up + p01.up + p11.up ) / 4f;

            mg.AddQuad(
              p00.position, p10.position, p11.position, p01.position,
              n, n, n, n,
              uv00, uv10, uv11, uv01
            );
          } 
        }

        return mg;
      }
      
      for ( int i = 0; i <= uSegments; i++ )
      {
        var uI = (float)i / uSegments;

        for ( int j = 0; j <= vSegments; j++ )
        {
          var vJ = (float)j / vSegments;

          var uvIJ = new Vector2( uI, vJ );

          var pose = uv( uvIJ );

          mg.vertices.Add( pose.position );
          mg.normals.Add( pose.up );
          mg.uvs.Add( uvIJ );
        }
      }


      for ( int i = 0; i < uSegments; i++ )
      {
        var u0 = i;
        var u1 = i + 1;

        for ( int j = 0; j < vSegments; j++ )
        {
          var v0 = j;
          var v1 = j + 1;

          var a = GetUVIndex( u0, v0, vSegments );
          var b = GetUVIndex( u1, v0, vSegments );
          var c = GetUVIndex( u1, v1, vSegments );
          var d = GetUVIndex( u0, v1, vSegments );

          mg.AddTriangle( a, b, d );
          mg.AddTriangle( b, c, d );

          // mg.AddTriangle( d, b, a );
          // mg.AddTriangle( d, c, b );
        }
      }

      return mg;

    }


    public MeshGeometry UniqueTriangles()
    {
      var mg = new MeshGeometry();

      mg.Initialize( true, true, colors != null, uv2s != null );

      if ( colors != null )
      {
        mg.colors = new List<Color>();
      }

      if ( uv2s != null )
      {
        mg.uv2s = new List<Vector2>();
      }      

      ForEachTriangle(
        ( index, a, b, c )=>
        {
          var vA = vertices[ a ];
          var vB = vertices[ b ];
          var vC = vertices[ c ];

          var uvA = uvs[ a ];
          var uvB = uvs[ b ];
          var uvC = uvs[ c ];

          var nA = normals[ a ];
          var nB = normals[ b ];
          var nC = normals[ c ];

          mg.AddTriangle( 
            vA, vB, vC,
            nA, nB, nC,
            uvA, uvB, uvC
          );

          if ( colors != null )
          {
            Lists.Add( mg.colors, colors[ a ], colors[ b ], colors[ c ] );
          }

          if ( uv2s != null )
          {
            Lists.Add( mg.uv2s, uv2s[ a ], uv2s[ b ], uv2s[ c ] );
          }

        }
      );

      return mg;
    }

    public void SetTriangleUVs( int triangleIndex, Vector2 uv )
    {
      var i = triangleIndex * 3;

      var a = indices[ i + 0 ];
      var b = indices[ i + 1 ];
      var c = indices[ i + 2 ];

      uvs[ a ] = uv;
      uvs[ b ] = uv;
      uvs[ c ] = uv;
    }

    public void SetTriangleNormal( int triangleIndex, Vector3 normal )
    {
      var i = triangleIndex * 3;

      var a = indices[ i + 0 ];
      var b = indices[ i + 1 ];
      var c = indices[ i + 2 ];

      normals[ a ] = normal;
      normals[ b ] = normal;
      normals[ c ] = normal;
    }

    public void SetTriangleColor( int triangleIndex, Color color )
    {
      var i = triangleIndex * 3;

      var a = indices[ i + 0 ];
      var b = indices[ i + 1 ];
      var c = indices[ i + 2 ];

      colors[ a ] = color;
      colors[ b ] = color;
      colors[ c ] = color;
    }

    public MeshGeometry( bool normals = true, bool uvs = true, bool colors = false, bool uvs2 = false )
    {
      Initialize( normals, uvs, colors, uvs2 );
    } 

    public MeshGeometry( string name, bool normals = true, bool uvs = true, bool colors = false, bool uvs2 = false )
    {
      this.name = name;
      Initialize( normals, uvs, colors, uvs2 );
    }

    public static MeshGeometry BillboardQuad( float size = 1 )
    {
      var hs = size / 2f;

      var mg = new MeshGeometry();
      mg.AddQuad(
        new Vector3( -hs, -hs, 0 ), new Vector3( hs, -hs, 0 ),  
        new Vector3(  hs,  hs, 0 ), new Vector3( -hs,  hs, 0 ),

        Vector3.Forward, Vector3.Forward, Vector3.Forward, Vector3.Forward, 

        new Vector2( 0, 0 ), new Vector2( 1, 0 ),  
        new Vector2( 1, 1 ), new Vector2( 0,  1 )
      );

      return mg;
    }

    public void Initialize( bool normals = true, bool uvs = true, bool colors = false, bool uvs2 = false )
    {      
      this.normals = normals ? new List<Vector3>() : null;
      this.uvs = uvs ? new List<Vector2>() : null;
      this.uv2s = uvs2 ? new List<Vector2>() : null;
      this.colors = colors ? new List<Color>() : null;      
    }

    public Vector3 GetRawNormal( int triangleIndex )
    { 
      var va = vertices[ indices[ triangleIndex * 3 ] ];
      var vb = vertices[ indices[ triangleIndex * 3 + 1] ]; 
      var vc = vertices[ indices[ triangleIndex * 3 + 2 ] ]; 

      return Math3D.ComputeNormal( va, vb, vc );
    }

    public void ComputeNormals()
    {
      var dl = new MapList<int,Vector3>();

      ForEachTriangle(
        ( t, va, vb, vc)=>
        {
          var normal = GetRawNormal( t );

          dl.Add( va, normal );
          dl.Add( vb, normal );
          dl.Add( vc, normal );
        }
      );

      foreach ( var e in dl )
      {
        var normalIndex = e.Key;
        var normal = Math3D.Average( e.Value ); 

        normals[ normalIndex ] = normal; 
      }
    }

    public MeshGeometry Clone()
    {
      var mg = new MeshGeometry();

      mg.vertices = Lists.Clone( vertices );
      mg.indices  = Lists.Clone( indices );
      mg.normals  = Lists.Clone( normals );

      mg.uvs      = Lists.Clone( uvs );
      mg.uv2s     = Lists.Clone( uv2s );
      mg.colors   = Lists.Clone( colors );

      return mg;
    }

    public void FlipNormalDirection()
    {
      for ( int i = 0; i < normals.Count; i++ )
      {
        normals[ i ] = -normals[ i ];
      }

      for ( int i = 0 ; i < indices.Count; i += 3 )
      {
        var b = indices[ i ];
        indices[ i ] = indices[ i + 2 ];
        indices[ i + 2 ] = b;
      }
      
    }



    public void Add( MeshGeometry sourceGeometry, Transform3D? optionalTransform = null )
    {
      var mappedIndices = new Dictionary<int,int>();

      var rotation = optionalTransform == null ? Quaternion.Identity : ( (Transform3D)optionalTransform ).Basis.GetRotationQuaternion();
      var transform = optionalTransform == null ? Transform3D.Identity : (Transform3D)optionalTransform;

      for ( int i = 0; i < sourceGeometry.indices.Count; i++ )
      {
        var sourceIndex = sourceGeometry.indices[ i ];

        if ( ! mappedIndices.ContainsKey( sourceIndex ) )
        {
          var newIndex = vertices.Count;

          if ( sourceIndex >= sourceGeometry.vertices.Count || sourceIndex < 0 )
          {
            RJLog.Log( "Out of range:", i, ">>", sourceIndex, sourceGeometry.vertices.Count, sourceGeometry.indices );
          }

          var v = sourceGeometry.vertices[ sourceIndex ]; 

          if ( optionalTransform == null )
          {
            vertices.Add( v );
          }
          else
          {
            vertices.Add( transform * v );
          }

          

          if ( normals != null && sourceGeometry.normals != null)
          {
            if ( optionalTransform == null )
            {
              normals.Add( sourceGeometry.normals[ sourceIndex ] );
            }
            else
            {
              normals.Add( rotation * sourceGeometry.normals[ sourceIndex ] );
            }
            
          }

          if ( uvs != null && sourceGeometry.uvs != null)
          {
            uvs.Add( sourceGeometry.uvs[ sourceIndex ] );
          }

          if ( colors != null && sourceGeometry.colors != null)
          {
            colors.Add( sourceGeometry.colors[ sourceIndex ] );
          }

          if ( uv2s != null && sourceGeometry.uv2s != null)
          {
            uv2s.Add( sourceGeometry.uv2s[ sourceIndex ] );
          }


          mappedIndices[ sourceIndex ] = newIndex;
        }

        indices.Add( mappedIndices[ sourceIndex ] );
      }
    }


    public void AddTriangle( int a, int b, int c, bool flip = false )    
    {
      if ( flip )
      {
        Lists.Add( indices, c, b, a );
      }
      else
      {
        Lists.Add( indices, a, b, c );
      }
      
    }


    public void AddTriangle( 
      Vector3 va, Vector3 vb, Vector3 vc, 
      Vector3 na, Vector3 nb, Vector3 nc,
      Vector2 uva, Vector2 uvb, Vector2 uvc
    )
    {
      var index = vertices.Count;

      Lists.Add( vertices, va, vb, vc );
      Lists.Add( normals, na, nb, nc );
      Lists.Add( uvs, uva, uvb, uvc );
      Lists.Add( indices, index, index + 1, index + 2 );
    }

   

    public void AddTriangle( 
      Vector3 va, Vector3 vb, Vector3 vc, 
      Vector2 uva, Vector2 uvb, Vector2 uvc
    )
    {
      var n = Vector3.Up;

      AddTriangle( va, vb, vc, 
                   n, n, n,
                   uva, uvb, uvc );
    }

    

    public void AddTriangle( Vector3 va, Vector3 vb, Vector3 vc )    
    {
      var n  = Vector3.Up;
      var uv = Vector2.Zero;

      AddTriangle( va, vb, vc,  n, n, n, uv, uv, uv );
    }

    public void AddQuad(
      Vector3 va, Vector3 vb, Vector3 vc, Vector3 vd,
      Vector3 na, Vector3 nb, Vector3 nc, Vector3 nd, 
      Vector2 uva, Vector2 uvb, Vector2 uvc, Vector2 uvd
    )
    {
      
      var index = vertices.Count;

      Lists.Add( vertices, va, vb, vc, vd );
      Lists.Add( normals, na, nb, nc, nd );
      Lists.Add( uvs, uva, uvb, uvc, uvd );

      Lists.Add( indices, index, index + 1, index + 2 );
      Lists.Add( indices, index + 2, index + 3, index );
    }

    public void AddQuad( Quaternion rotation, float size, Box2 rectangle )
    {
      AddQuad( rotation, size, rectangle.min, rectangle.max );
    }

   
    public void DuplicateRange( int verticesStart, int verticesLength, int indexStart, int indicesLength )
    {
      for ( int i = verticesStart; i < verticesStart + verticesLength; i++ )
      {
        var v = vertices[ i ];
        var n = normals[ i ];
        var uv = uvs[ i ];

        vertices.Add( v ) ;
        normals.Add( n );
        uvs.Add( uv );
      }

      for ( int i = indexStart; i < indexStart + indicesLength; i++ )
      {
        indices.Add( indices[ i ] );
      }
    }

    public void AddQuad( Quaternion rotation, float size, Vector2 uv00, Vector2 uv11 )
    {
      var l = size * 0.5f;

      var normal = Vector3.Back * rotation;
      var points = new List<Vector3>
      {
        new Vector3(  l, -l, 0 ), new Vector3( -l, -l, 0 ),
        new Vector3( -l,  l, 0 ), new Vector3(  l,  l, 0 )
      };

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

      var uv10 = new Vector2( uv11.X, uv00.Y );
      var uv01 = new Vector2( uv00.X, uv11.Y );

      AddQuad(
        points[ 0 ], points[ 1 ], points[ 2 ], points[ 3 ],
        normal, normal, normal, normal,
        uv10, uv00, uv01, uv11
      );

    }

    public void AddQuad( int lt, int rt, int lb, int rb, bool flip = false )    
    {
      if ( flip )
      {
        AddQuad( rt, lt, rb, lb, false );
      }
      else
      {
        Lists.Add( indices, lb, rt, lt, rt, lb, rb );
      }
     
    }


    public void AddQuad(
      Vector3 va, Vector3 vb, Vector3 vc, Vector3 vd,
      Vector2 uva, Vector2 uvb, Vector2 uvc, Vector2 uvd
    )
    {

      AddTriangle( va, vb, vc, uva, uvb, uvc );
      AddTriangle( vc, vd, va, uvc, uvd, uva );
    }

    public void AddQuad(  Vector3 va, Vector3 vb, Vector3 vc, Vector3 vd  )
    {
      AddQuad(
        va, vb, vc, vd,
        new Vector2( 0, 0 ),
        new Vector2( 0, 1 ),
        new Vector2( 1, 1 ),
        new Vector2( 1, 0 )
      );
    }

    public void AddQuad( Vector3 offset, float size  )
    {
      var center = new Vector3( 0.5f, 0.5f, 0 );

      /*
      
      [-0.5, -0.5]  [0.5, -0.5 ]  
      [-0.5,  0.5]  [0.5,  0.5 ]  

      */

      AddQuad(        
        new Vector3( -0.5f, -0.5f, 0 ) * size + offset ,
        new Vector3( -0.5f,  0.5f, 0 ) * size + offset ,
        new Vector3(  0.5f,  0.5f, 0 ) * size + offset ,
        new Vector3(  0.5f, -0.5f, 0 ) * size + offset 
      );
    } 

    public void AddConvex2( Convex2 convex )
    {
      var points = convex.points;
      var tris = points.Count - 2;

      for ( int i = 0; i < tris; i++ )
      {
        var p0 = Math3D.XYasXZ( points[ 0 ] );
        var p1 = Math3D.XYasXZ( points[ i + 1 ] );
        var p2 = Math3D.XYasXZ( points[ i + 2 ] );

        AddTriangle( p0, p1, p2 );
      }
    }

    public void CenterMesh( bool onX = true, bool onY = true, bool onZ = true, Box3 centerCalcluationBox = null )
    {
      var offset = new Vector3( 0, 0, 0 );
      var numPoints = 0;

      for ( int i = 0; i < vertices.Count; i++ )
      {
        if ( centerCalcluationBox != null && ! centerCalcluationBox.ContainsPoint( vertices[ i ] ) )
        {
          continue;
        }

        offset += vertices[ i ];
        numPoints ++;
      }

      offset /= numPoints;
      
      if ( ! onX )
      {
        offset.X = 0;
      } 

      if ( ! onY )
      {
        offset.Y = 0;
      }

      if ( ! onZ )
      {
        offset.Z = 0;
      }


      ApplyTranslation( - offset );


    }
    
  

    public ArrayMesh GenerateMesh( Mesh.PrimitiveType type = Mesh.PrimitiveType.Triangles, ArrayMesh arrayMesh = null )
    {
      if ( arrayMesh == null )
      {
        arrayMesh = new ArrayMesh();
      }

      var surfaceArray = new Godot.Collections.Array();
      surfaceArray.Resize( (int) Mesh.ArrayType.Max );

      if ( vertices != null && vertices.Count != 0 )
      {
        surfaceArray[ (int) Mesh.ArrayType.Vertex ] = vertices.ToArray();
      }

      if ( normals != null && normals.Count != 0 )
      {
        surfaceArray[ (int) Mesh.ArrayType.Normal ] = normals.ToArray();
      }

      if ( uvs != null && uvs.Count != 0 )
      {
        surfaceArray[ (int) Mesh.ArrayType.TexUV ] = uvs.ToArray();
      }

      if ( uv2s != null && uv2s.Count != 0 )
      {
        surfaceArray[ (int) Mesh.ArrayType.TexUV2 ] = uv2s.ToArray();
      }

      if ( colors != null && colors.Count != 0 )
      {
        surfaceArray[ (int) Mesh.ArrayType.Color ] = colors.ToArray();
      }

      if ( indices != null && indices.Count != 0 )
      {
        surfaceArray[ (int) Mesh.ArrayType.Index ] = indices.ToArray();
      }

      arrayMesh.AddSurfaceFromArrays( Mesh.PrimitiveType.Triangles, surfaceArray );

      return arrayMesh;
    }

  }
}