using System.Collections;
using System.Collections.Generic;
using Godot;
using System;
using System.Threading.Tasks;



namespace Rokojori
{
  /** <summary for="class GrassPatch">
      
      <title>
        Creates a grass patch 3D model asset.
      </title>
      
      <description>
        The GrassPatch has various settings to create a different styles of grass.
        It allows to change the shapes of the blades, their number and distribution, their triangle count,
        rotation and scale, LOD levels and much more. 
        

      </description>

    </summary>  
  */

  [Tool]
  [GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Scatterer.svg") ]
  public partial class GrassPatch:Node3D
  {
    
    [ExportToolButton( "(?)  Help", Icon = "Help") ]
    public Callable openHelpButton => Callable.From( ()=> 
    { 
      #if TOOLS
      Rokojori.Tools.OnlineDocs.Open( GetType() );
      #endif
    }
    );

    /** <summary for="field output">The output where the mesh is stored, gets generated automatically.</summary>*/
    [Export]
    public MeshInstance3D output;

    /** <summary for="field seed">The seed for the randomizers</summary>*/
    [Export]
    public int seed = 1984;

    
    [ExportToolButton( "Create")]
    public Callable createButton => Callable.From( ()=> 
    { 
      CreatePatch();
    }
    );

    /** <summary for="field material">The material the grass will be assigned</summary>*/
    [Export]
    public Material material;

   
    /** <summary for="field patchSize">Patch size for x and z</summary>*/
    [ExportGroup( "Patch")]
    [Export]
    public float patchSize = 2;

    /** <summary for="field patchSizeX">Additional patch size for x</summary>*/
    [Export]
    public float patchSizeX = 0;

    /** <summary for="field patchSizeZ">Additional patch size for z</summary>*/
    [Export]
    public float patchSizeZ = 0;

    /** <summary for="field patchOffsetPosition">Patch offset in x/z</summary>*/
    [Export]
    public Vector2 patchOffsetPosition = new Vector2( 0.5f, 0.5f );

    /** <summary for="field centerPatch">If centerPatch is on, the patch will be centered automatically</summary>*/
    [Export]
    public bool centerPatch = false;

    /** <summary for="field patchScale">Scales the complete mesh with patchScale</summary>*/
    [Export]
    public float patchScale = 1.0f;

    /** <summary for="field centerPatchComputationHeightTreshold">
    If on, this determines which vertices should be counted to compute the center of the patch. 
    
    <p>
    All vertices in y that are below, will not count to compute the center.
    This can be usefull to align more to the upper more visible parts, it usually has only a small impact.
    </p>
    </summary>*/
    [Export]
    public float centerPatchComputationHeightTreshold = 0.1f;


    [ExportGroup( "Blades")]

    /** <summary for="field blades">The number of blades it will generate in x and z</summary>*/
    [Export( PropertyHint.Range, "0,100")]
    public int blades = 20;

    /** <summary for="field bladesX">Additional number of blades it will generate in x</summary>*/
    [Export( PropertyHint.Range, "0,100")]
    public int bladesX = 0;

    /** <summary for="field bladesZ">Additional number of blades it will generate in z</summary>*/
    [Export( PropertyHint.Range, "0,100")]
    public int bladesZ = 0;

    /** <summary for="field X_numBlades">This is an output/debug property, and will show how many blades were created</summary>*/
    [Export]
    public int X_numBlades;



    /** <summary for="field bladeSegments">This defines the number of segments (vertical divisions) for each blade.</summary>*/
    [ExportGroup( "Triangles")]
    [Export( PropertyHint.Range, "1,256")]
    public int bladeSegments = 3;

    /** <summary for="field createBackFaces">This generates back faces, if it is disabled the material should not use backface culling</summary>*/
    [Export]
    public bool createBackFaces = true;

    /** <summary for="field allowTrianglesOnEnds">
    If on, this allows the ends of the blades to be merged as triangles instead of quads.
    <p>
    This is usefull for creating very low poly grass blades with only one triangle.
    The width of the blade needs to be very close to zero at the start or end to be applied. 
    </p>
    </summary>*/
    [Export]
    public bool allowTrianglesOnEnds = true;

    /** <summary for="field X_numTriangles">This is an output/debug property, and will show how many triangels were created</summary>*/
    [Export]
    public int X_numTriangles;


    /** <summary for="field bladeSegmentMapping">
    Allows to use a non-linear distortion for the segmentation.
    <p>
    Normally, the blade shapes are segmented by taking an equal distance of divisions throughout the
    height of the blade.
    But since this low poly shapes won't be represented well with this approach,  
    zhis setting allows to distort the segmentation, some shapes are better represented with it.
    </p>
    </summary>*/
    [Export]
    public Curve bladeSegmentMapping = MathX.Curve( 0, 1 );

    /** <summary for="field uvSegmentColumns">
    The number of splits in U for the blades, when generating the uv coordinates.
    
    <p>
    This allows to use multiple regions for the blades, so that blades can have different material settings.
    </p>
    </summary>*/
    [ExportGroup( "Segmentation")]
    [Export]
    public int uvSegmentColumns = 1;

    /** <summary for="field uvSegmentRows">
    The number of splits in V for the blades, when generating the uv coordinates.
    
    <p>
    This allows to use multiple regions for the blades, so that blades can have different material settings.
    </p>
    </summary>*/
    [Export]
    public int uvSegmentRows = 1;

    /** <summary for="field uvSegmentWeightsClose">
    The distribution for the UV region selection. 
    
    <p>
    This will change the propability of the uv regions, so that some regions can 
    be used more often than other regions.
    </p>
    </summary>*/
    [Export]
    public Curve uvSegmentWeightsClose = MathX.Curve( 1f );


   /** <summary for="field uvSegmentWeightsFar">
    The distribution for the UV region selection for blades outside the center. 
    
    <p>
    This will change the propability of the uv regions outside the center.
    This allows to have different distribution dependening on the position.
    </p>
    </summary>*/
    [Export]
    public Curve uvSegmentWeightsFar = null;

    /** <summary for="field uvSegmentDistortion">
    Blending factor between undistorted and distorted U mapping.
    
    <p>
    This warps/pulls the strength of the close or far distribution. 
    </p>
    </summary>*/

    [Export( PropertyHint.Range, "0,1" )]
    public float uvSegmentDistortion = 0.5f;

    /** <summary for="field uvSegmentMaxRange">
    Defines how far the U mapping can distort.
    </summary>*/
    [Export( PropertyHint.Range, "0,0.5" )]
    public float uvSegmentMaxRange = 0.3f;


    /** <summary for="field bladeHeight">
    Defines the height of the blades, the curve defines the random distribution. 

    <p>
    The total y size is sum of the blade height and baldInGround. 
    </p>
    </summary>*/
    [ExportGroup( "Shape")]
    [Export]
    public Curve bladeHeight = MathX.Curve( 0.4f );

     /** <summary for="field bladeInGround">
    The amount the blade is continued below 0 in y. See also bladeHeight. 
    </summary>*/
    [Export]
    public Curve bladeInGround = MathX.Curve( 0.05f );

    /** <summary for="field bladeWidth">
    The width of the blade defined over its height. 
    </summary>*/
    [Export]
    public Curve bladeWidth = MathX.Curve( 0.05f );

    /** <summary for="field bladeWidth2">
    Optional: A second width (as randomizer) of the blade defined over its height. 

    <p>
    The randomizer chooses a blend value before the creation and will use the resulting curve as width.
    This means it will not randomly jump between the first and second width, but uses an interpolated curve.
    </p> 
    </summary>*/
    [Export]
    public Curve bladeWidth2 = null;

    [ExportGroup( "Curve")]

    [Export]
    public Curve bladeBending = MathX.Curve( 0f );

    [Export]
    public Curve bladeBending2 = null;

    [Export]
    public Curve bladeTwisting = null;

    [Export]
    public Curve bladeTwisting2 = null;

    [Export]
    public Curve rolling = MathX.Curve( 0f );

    [Export]
    public Curve rolling2 = null;


    [ExportGroup( "Offset")]

    [Export]
    public Curve positionJitter = MathX.Curve( 0.05f );

    [Export]
    public Curve positionJitterX = MathX.Curve( 0.05f );

    [Export]
    public Curve positionJitterZ = MathX.Curve( 0.05f );



    [ExportGroup( "Scale")]

    [Export]
    public Curve bladeScale = MathX.Curve( 1f );

    [Export]
    public Curve scaleByDistanceX = MathX.Curve( 1f );

    [Export]
    public Curve scaleByDistanceZ = MathX.Curve( 1f );

    [Export]
    public Curve xRemapper = null;

    [Export]
    public Curve yRemapper = null;

    [Export]
    public Curve zRemapper = null;

    [Export]
    public Curve scaleXZForY = null;

    [Export]
    public Curve scaleXForY = null;

    [Export]
    public Curve scaleZForY = null;


    [ExportGroup( "Rotation")]

    [Export]
    public Curve yawRotation = MathX.Curve( 0f, 1f );

    [Export]
    public Curve randomRotation = MathX.Curve( 0f, 20f );

    [ExportGroup( "Noise")]
    [Export]
    public Curve vertexTurbulenceAmount = MathX.Curve( 0f, 0f );

    [Export]
    public Curve vertexTurbulenceScale = MathX.Curve( 1f, 1f );

    [Export]
    public Curve vertexTurbulenceScaleX = MathX.Curve( 1f, 1f );

    [Export]
    public Curve vertexTurbulenceScaleY = MathX.Curve( 1f, 1f );

    [Export]
    public Curve vertexTurbulenceScaleZ = MathX.Curve( 1f, 1f );


    [ExportGroup( "Normals")]
    [Export]
    public Curve normalBlending = MathX.Curve( 0.5f );

    [Export]
    public Curve normalBlendingAmountOverY = MathX.Curve( 1f );

    [Export]
    public Vector3 normalBlendingDirection = Vector3.Up;



    [ExportGroup( "Filter")]
    [Export (PropertyHint.Range, "0,1")]
    public float filterTreshold = 1;

    [Export]
    public Vector2 filterScale = new Vector2( 1, 1 );

    [Export]
    public Vector2 filterOffset = new Vector2( 0, 0 );

    [Export]
    public Vector2 positionToFilter = new Vector2( 0, 0 );

    [ExportGroup("LOD Levels")]
    [Export]
    public GrassPatchLODLevel[] lodLevels = new GrassPatchLODLevel[ 0 ];

    [Export]
    public int currentLODLevel = -1;

    [Export]
    public bool generateLODMesh = false;

    [Export]
    public float customLODEdgeLength = 0;

    [Export]
    public int lodLerpSteps = 2;

    [Export]
    public Curve lowCurve = MathX.Curve( 0, 1 );

    [Export]
    public Curve highCurve = MathX.Curve( 0, 1 );

    float _maxWidth = 0;

    Vector3 _patchOffset = Vector3.Zero;

    public int ComputeNumBlades()
    {
      if ( blades == 0 )
      {
        if ( bladesX < bladesZ )
        {
          bladesX = Mathf.Max( 1, bladesX );
        }
        else 
        {
          bladesZ = Mathf.Max( 1, bladesZ ); 
        }
      }

      return ( bladesX + blades ) * ( blades + bladesZ );
    }

    public int ComputeNumTriangles()
    {
      return bladeSegments * 2 * ComputeNumBlades();
    }

    public bool isCreating = false;

    public async Task CreatePatch( string patchName = null )
    {
      isCreating = true;

      // RJLog.Log( "Create Patch:", patchName );

      if ( blades == 0 && bladesX == 0 && bladesZ == 0)
      {
        isCreating = false;
        return;
      }

      if ( blades == 0 )
      {
        if ( bladesX < bladesZ )
        {
          bladesX = Mathf.Max( 1, bladesX );
        }
        else 
        {
          bladesZ = Mathf.Max( 1, bladesZ ); 
        }
      }

      if ( lodLevels == null )
      {
        lodLevels = new GrassPatchLODLevel[]{};
      } 

      
      this.output = this.GetOrCreateChild<MeshInstance3D>( patchName );
      


      var random = new LCG();
      random.SetSeed( 1712 + seed );

      _patchOffset = new Vector3( - ( patchSizeX + patchSize ) * patchOffsetPosition.X, 0, - ( patchSizeZ + patchSize ) * patchOffsetPosition.Y );

      _maxWidth = MathX.CurveMaximum( bladeWidth );

      X_numBlades = 0;  
      
      if ( ! generateLODMesh )
      {
        var mg = CreatePatchGeometry();
        X_numTriangles = mg.indices.Count / 3;
        output.Mesh = mg.GenerateMesh();
        output.Mesh.SurfaceSetMaterial( 0, material );

        output.Name = patchName;
      } 
      else
      {
        var cachedCurrentLODLevel = currentLODLevel;

        try
        {
          var mgs = new List<MeshGeometry>();

          var numTris = 0;

          var time = this.StartAsyncTimer();
          var edgeLength = ( MathX.CurveAverage( bladeHeight ) * MathX.CurveAverage( bladeScale ) ) / bladeSegments;

          if ( customLODEdgeLength > 0 )
          {
            edgeLength = customLODEdgeLength;
          }

          this.LogInfo( "Edge Length", edgeLength );

          for ( int i = 0; i < lodLevels.Length + 1; i++ )
          {
            time = await this.WaitForAsyncTimer( time );

            var lodIndex = i - 1;
            currentLODLevel = lodIndex;

            var lodEdgeLengthScale = 1f;

            if ( lodIndex >= 0 )
            {
              lodEdgeLengthScale = lodLevels[ lodIndex ].lodEdgeLengthScale;
            }

            var lodMG = CreatePatchGeometry();
            lodMG.lodEdgeLength = edgeLength * lodEdgeLengthScale; 
            numTris += lodMG.vertices.Count;

            lodMG.ApplyTranslation( new Vector3( 0, 0, 0 ) ) ;
            mgs.Add( lodMG );            
          }

          X_numTriangles = numTris;

          LODLerpingData lerpingData = null;

          if ( lodLerpSteps > 0 )
          {
            lerpingData = new LODLerpingData();
            lerpingData.lowerLevelWeights = lowCurve;
            lerpingData.higherLevelWeights = highCurve;
            lerpingData.lerpSteps = lodLerpSteps;
          }

          output.Mesh = MeshGeometry.GenerateTrianglesLODMesh( mgs, lerpingData, false );
          output.Mesh.SurfaceSetMaterial( 0, material );

          output.Name = patchName;


        }
        catch ( System.Exception e )
        {
          this.LogError( e );
        }

        currentLODLevel = cachedCurrentLODLevel;
      }

      isCreating = false;

      
    }

    bool debugBlade = false;

    MeshGeometry CreatePatchGeometry()
    {
      var random = new LCG();
      random.SetSeed( 1712 + seed );

      var mg = new MeshGeometry();

      var cellSizeX = ( patchSizeX + patchSize ) / ( bladesX + blades );
      var cellSizeZ = ( patchSizeZ + patchSize ) / ( bladesZ + blades );


      var allBladesX = bladesX + blades;
      var allBladesZ = bladesZ + blades;

      for ( int i = 0; i < allBladesX; i++ )
      {
        var x = ( i + 0.5f ) * cellSizeX;

        for ( int j = 0; j < allBladesZ; j++ )
        {
          var z = ( j + 0.5f ) * cellSizeZ;

          random.SetSeed( i * 11223 + j *12895 + seed );
          var position = new Vector3( x, 0, z );
          var yaw = random.Sample( yawRotation );
          var rotationY = yaw * Mathf.Pi * 2f;
          var maxRotation = random.Sample( randomRotation );
          var normalBlendingAmount = random.Sample( normalBlending );
          var rotationOther = random.Next() * Mathf.DegToRad( maxRotation );
          var rotationX = random.Next() * rotationOther;
          var rotationZ = rotationOther - rotationX;
          var positionOffset = Math3D.OnCircleXZ( random.Next() * Mathf.Pi * 2f ) * random.Sample( positionJitter );
          positionOffset.X += random.Sample( positionJitterX );
          positionOffset.Z += random.Sample( positionJitterZ );
          var worldPosition = position + _patchOffset + positionOffset;

          var filterValue = Noise.PerlinXZ(  Math3D.XYasXZ( filterScale ) * worldPosition + Math3D.XYasXZ( filterOffset ) + -GlobalPosition * Math3D.XYasXZ( positionToFilter ));

          var trsf = new Transform3D( 
              new Basis( Math3D.RotateY( rotationY ) * Math3D.RotateX( rotationX ) * Math3D.RotateZ( rotationZ )), 
              worldPosition
            );

          debugBlade = X_numBlades < 10;


          var bladeMG = CreateBlade( random, worldPosition );

          if ( bladeMG.numNormals == 0 || bladeMG.numUVs == 0 )
          {
            this.LogInfo( "Invalid mg" );  
          }
          
          if ( filterValue > filterTreshold )
          {
            continue;
          }              

          var cL = Mathf.Clamp( currentLODLevel, -1, lodLevels.Length - 1 );

          if ( cL != -1 )
          {
            var lodFilterTreshold = lodLevels[ cL ].filter;

            if ( random.Next() >= lodFilterTreshold )
            {
              continue;
            }
          }
         
          bladeMG.ApplyTransform( trsf );

          MeshGeometry clonedBladeMG = null;

          if ( createBackFaces )
          {
            clonedBladeMG = bladeMG.Clone();
          }

          var yRange = bladeMG.GetRangeY();

          // bladeMG.BlendNormals( normalBlendingDirection, normalBlendingAmount );        
          bladeMG.BlendNormalsOverY( normalBlendingDirection, normalBlendingAmount, yRange.min, yRange.max, normalBlendingAmountOverY  );

          mg.Add( bladeMG );  

          if ( createBackFaces )
          {
            
            clonedBladeMG.FlipNormalDirection();  
            // clonedBladeMG.BlendNormals( normalBlendingDirection, normalBlendingAmount );   
            clonedBladeMG.BlendNormalsOverY( normalBlendingDirection, normalBlendingAmount, yRange.min, yRange.max, normalBlendingAmountOverY  );
            
            mg.Add( clonedBladeMG );       
          }          


          X_numBlades ++;
        }
      }      

      if ( mg.numNormals == 0 || mg.numUVs == 0 )
      {
        this.LogInfo( "Invalid mg" );  
      }


      if ( centerPatch )
      {
        var f = 100000;
        var h = centerPatchComputationHeightTreshold;
        var box = Box3.Create( new Vector3( -f, -f, -f ), new Vector3( f, h, f ) );
        mg.CenterMesh( true, false, true, box );
      } 

      if ( vertexTurbulenceAmount != null )
      {
        var turbulenceAmount = random.Sample( vertexTurbulenceAmount );
        var turbulenceScale  = Vector3.One * ( vertexTurbulenceScale == null ? 1 : random.Sample( vertexTurbulenceScale ) );

        if ( currentLODLevel != -1 )
        {
          turbulenceAmount *= lodLevels[ currentLODLevel ].turbulenceScale;
        }

        if ( vertexTurbulenceScaleX != null )
        {
          turbulenceScale.X *= random.Sample( vertexTurbulenceScaleX );
        }

        if ( vertexTurbulenceScaleY != null )
        {
          turbulenceScale.Y *= random.Sample( vertexTurbulenceScaleY );
        }

        if ( vertexTurbulenceScaleZ != null )
        {
          turbulenceScale.Z *= random.Sample( vertexTurbulenceScaleX );
        }

        mg.Turbulence( turbulenceAmount, turbulenceScale, Vector3.Zero );
      }

      if ( xRemapper != null )
      {
        mg.RemapX( xRemapper );
      }

      if ( yRemapper != null )
      {
        mg.RemapY( yRemapper );
      }

      if ( zRemapper != null )
      {
        mg.RemapZ( zRemapper );
      }

      if ( scaleXZForY != null )
      {
        mg.ScaleXZForY( scaleXZForY );
      }

      if ( scaleXForY != null )
      {
        mg.ScaleXForY( scaleXForY );
      }

      if ( scaleZForY != null )
      {
        mg.ScaleZForY( scaleZForY );
      }


      if ( patchScale != 1.0f )
      {
        mg.ApplyScale( patchScale );
      }

      return mg;

    }

    MeshGeometry CreateBlade( RandomEngine random, Vector3 position )
    {
      // if ( debugBlade )
      // {
      //   this.LogInfo( "Blade:", X_numBlades );
      // }

      var bladeSegments = this.bladeSegments;

      var currentLodLevel = Mathf.Clamp( currentLODLevel, -1, lodLevels.Length - 1 );
      
      var bladeScaleMultiplier = 1f;
      var bladeWidthMultiplier = 1f;

      if ( currentLodLevel != -1 )
      {
        bladeSegments        = lodLevels[ currentLodLevel ].bladeSegments;
        bladeScaleMultiplier = lodLevels[ currentLodLevel ].bladeScaleMultiplier; 
        bladeWidthMultiplier = lodLevels[ currentLodLevel].bladeWidthMultiplier; 
      }

      var bmg = new MeshGeometry();

      
      var inGround = random.Sample( bladeInGround );
      var height   = random.Sample( bladeHeight );

      var size = height + inGround;

      var distancesToCenter = new Vector2( position.X / ( patchSizeX + patchSize ), position.Z / ( patchSizeZ  + patchSize ) );
      distancesToCenter = distancesToCenter.Abs();

      var minDistance = distancesToCenter.Length();
      var scaling = scaleByDistanceX.Sample( distancesToCenter.X ) * scaleByDistanceZ.Sample( distancesToCenter.Y );
      scaling *= random.Sample( bladeScale );
      scaling *= bladeScaleMultiplier;

      size *= scaling;
      var firstIsTriangle = false;  
      var lastIsTriangle  = false;

      var bladeWidthLerp = random.Next();
      var bladeBendLerp  = random.Next();

      var uvMin = new Vector2( 0, 0 );
      var uvMax = new Vector2( 1, 1 );

      var uvSegments = uvSegmentRows * uvSegmentColumns;

      if ( uvSegments > 1 )
      {
        var index = random.IntegerExclusive( uvSegments );

        if ( uvSegmentWeightsClose != null )
        {        
          var weightsClose = MathX.GetCurveWeights( uvSegmentWeightsClose, uvSegments );

          if ( uvSegmentWeightsFar != null )
          {
            var weightsFar   = MathX.GetCurveWeights( uvSegmentWeightsFar, uvSegments ); 
            var distanceFade = minDistance;

            for ( int i = 0; i < uvSegments; i++ )
            {
              weightsClose[ i ] += distanceFade * ( weightsFar[ i ] - weightsClose[ i ] );
            }
          }

          index = random.IndexFromUnnormalizedWeights( weightsClose );    
        }   
        
        var x = index % uvSegmentColumns;
        var y = Mathf.FloorToInt( index / uvSegmentColumns );

        var xSize = 1f / uvSegmentColumns;
        var ySize = 1f / uvSegmentRows;

        uvMin.X = x * xSize;
        uvMin.Y = y * ySize;

        uvMax = uvMin + new Vector2( xSize, ySize );
      }

      var endingTriangles = allowTrianglesOnEnds;

      if ( currentLodLevel != -1 )
      {
        var useTris = lodLevels[ currentLodLevel ].allowTrianglesOnEnd;

        endingTriangles = TrilleanLogic.ToBool( useTris, endingTriangles );        
      }
     

      for ( int i = 0; i <= bladeSegments; i++ )
      {
        var t = (float)i / bladeSegments;
        t = bladeSegmentMapping.Sample( t );

        var v = 1f - t;

        var y = size * t - inGround;
        var bw2 = bladeWidth2 == null ? bladeWidth : bladeWidth2;
        var bb2 = bladeBending2 == null ? bladeBending : bladeBending2; 

        var width = Mathf.Lerp( bladeWidth.Sample( v ), bw2.Sample( v ), bladeWidthLerp );
        width *= scaling * bladeWidthMultiplier;

        
        // var bending = Mathf.Lerp( bladeBending.Sample( v ), bb2.Sample( v ), bladeBendLerp ) * Vector3.Forward.Z * scaling;
        // var bendingNormalAngle = Mathf.LerpAngle( MathX.CurveAngle( bladeBending, v ), MathX.CurveAngle( bb2, v ), bladeBendLerp );
        // var bendingNormal = Math3D.NormalAngle( bendingNormalAngle );      

        var bending = 0;
        var bendingNormal = Vector3.Back;

        if ( endingTriangles && width < 0.005f && ( i == 0 || ( i == bladeSegments - 1 )) )
        {
          bmg.vertices.Add( new Vector3( 0, y, bending ) );
          bmg.normals.Add( bendingNormal );
          bmg.uvs.Add( MapUV( new Vector2( 0.5f, v ), uvMin, uvMax ) );

          if ( i == 0 ){ firstIsTriangle = true; }
          if ( i == bladeSegments - 1 ){ lastIsTriangle = true; }
        } 
        else
        {
          bmg.vertices.Add( new Vector3( -width, y, bending ) );
          bmg.vertices.Add( new Vector3(  width, y, bending ) );

          bmg.normals.Add( bendingNormal );
          bmg.normals.Add( bendingNormal );

          var uvCoord = width / _maxWidth;

          uvCoord = Mathf.Lerp( uvCoord, 1f, uvSegmentDistortion );

          uvCoord *= uvSegmentMaxRange;
          var uvL = MapUV( new Vector2( -uvCoord + 0.5f, v ), uvMin, uvMax );
          var uvR = MapUV( new Vector2(  uvCoord + 0.5f, v ), uvMin, uvMax );

          bmg.uvs.Add( uvL );
          bmg.uvs.Add( uvR );

          // if ( debugBlade )
          // {
          //   this.LogInfo( i, ">>", uvL, uvR );
          // }


        }
      }

      
      for ( int i = 0; i < bladeSegments; i++ )
      {
        var index = i * 2;

        if ( firstIsTriangle && index != 0 )
        {
          index --;
        }
        
        if ( i == 0 && firstIsTriangle )
        {
          bmg.AddTriangle( index + 0, index + 1, index + 2 );
        }
        else if ( i == ( bladeSegments - 1 ) && lastIsTriangle )
        {
          bmg.AddTriangle( index + 1, index + 0, index + 2 );
        }
        else 
        {
          bmg.AddQuad( index + 0, index + 1, index + 2, index + 3 );
        }
      }

      var yStart = size * 0f - inGround;
      var yEnd   = size * 1f - inGround;

      var startPosition = new Vector3( 0, yStart, 0 );
      var endPosition   = new Vector3( 0, yEnd, 0 );

      var inputList = new List<Vector3>(){ startPosition, endPosition };
      var inputSpline = new SplineCurveCreator().Create( inputList );
      inputSpline.AutoOrientateByTangents( Vector3.Back );
      
      var outputList = new List<Vector3>();

      var numPoints = Mathf.Max( 2, bladeSegments * 2 );

      for ( int i = 0; i < numPoints ; i++ )
      {
        var t = (float)i / ( numPoints - 1f );
        var y = size * t - inGround;
        var bb2 = bladeBending2 == null ? bladeBending : bladeBending2; 
        var v = 1f - t;
        var bending = Mathf.Lerp( bladeBending.Sample( v ), bb2.Sample( v ), bladeBendLerp ) * Vector3.Forward.Z * scaling;

        var p = new Vector3( 0, y, bending );

        outputList.Add( p );
      } 

      var outputSpline = new SplineCurveCreator().Create( outputList );
      

      var outputPoints = outputSpline.points;

      if ( bladeTwisting != null )
      {     
        for ( int i = 0; i < outputPoints.Count; i++ )
        {
          var t = (float)i / ( numPoints - 1f );
          var v = 1f - t;

          var bt2 = bladeTwisting2 == null ? bladeTwisting : bladeTwisting2; 
          var tw = Mathf.Lerp( bladeTwisting.Sample( v ), bt2.Sample( v ), bladeBendLerp );
          var p = outputPoints[ i ];
          p.twist = tw;
        }

      }

      outputSpline.AutoOrientateByTangents( Vector3.Back );

      var rollingMix = random.Next();

      var otherRolling = rolling2 == null ? rolling : rolling2;

      for ( int i = 0; i < outputPoints.Count - 1; i++ )
      {
        var t = i / (float)( outputPoints.Count - 2 );
        var rotation  = rolling.Sample( t );
        var rotation2 = otherRolling.Sample( t );
        var mixedRotation = Mathf.Lerp( rotation, rotation2, rollingMix );

        RollSpline( outputPoints, i, mixedRotation );
      }

      var modifier = new SplinesDeformModifier();
      modifier.sourceSplines = new SplineCurve[]{ inputSpline };
      modifier.targetSplines = new SplineCurve[]{ outputSpline };
      modifier.settings = new SplinesDeformerSettings();

      bmg = modifier.Modify( bmg );


      return bmg;
    }

    void RollSpline( List<SplineCurvePoint> points, int start, float amount )
    {
      var pivot = points[ start ].position;
      var rotation = Math3D.RotateX( MathX.DegreesToRadians * amount / (float) points.Count ).Normalized();

      for ( int i = start + 1; i < points.Count; i++ )
      {        
        points[ i ].RotateAround( rotation, pivot );
      }
    }

    Vector2 MapUV( Vector2 uv, Vector2 min, Vector2 max )
    {
      uv = Math2D.Clamp01( uv );
      return Math2D.Map( uv, Vector2.Zero, Vector2.One, min, max );
    }
  }
}