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


namespace Rokojori.PointClouds
{
  [Tool]
  [GlobalClass]
  public partial class PointCloudGenerator:Node3D
  { 
    [Export]
    public MeshInstance3D mesh;

    [Export]
    public PointCloudSampler.SampleMode sampleMode = PointCloudSampler.SampleMode.Center;

    [Export]
    public float samplingPixelDensity = 0.01f;

    [Export]
    public float outputPixelDensity = 0.5f;

    [Export]

    public int numPoints = 0;   

    [Export]
    public int squareTexture;

    [Export]
    public Material material;

    [Export]
    public bool createOutputs = false;

    [Export]
    public MeshInstance3D[] outputs; 

    [ExportGroup("Lerping")]
    [Export]
    public Curve lowerLerp;

    [Export]
    public Curve higherLerp;

    [Export]
    public int lerpSteps;

    [ExportGroup("")]

    [Export]
    public LODNode lod;


    [Export]
    bool working = false;



    [Export]
    public string[] outputInfos = [];

    [ExportToolButton( "Generate")]
    public Callable GenerateButton => Callable.From( 
      () => 
      { 
        GeneratePointCloud(); 
      } 
    );

    public async void GeneratePointCloud()
    {
      if ( working )
      {
        return;
      } 

      working = true;
      numPoints = -1;
      this.LogInfo( "Generating Point Cloud" );

        
      var sampler = new PointCloudSampler();

      if ( samplingPixelDensity > 0 )
      {
        sampler.densityResolution = samplingPixelDensity;
      }

      sampler.materials = Materials.GetAll<Material>( mesh );
      sampler.albedoTexture = sampler.materials.Map( m => Sampler2DPropertyName.albedoTexture );      
      sampler.albedo = sampler.materials.Map( m => ColorPropertyName.albedo );    ;

      var cloud = await sampler.SampleFromMesh( sampleMode, (ArrayMesh) mesh.Mesh, this );

      this.LogInfo( "Sampled mesh" );

      var time = Async.GetTimeMS();
      // cloud.ComputeBoundingBox();

      time = Async.PrintAndUpdateMS( time );

      var longestCM = cloud.boundingBox.size.MaxDimension() * 100f;
      var nextP2CM = MathX.NextPowerOfTwo( longestCM );
      var rootSize = nextP2CM / 100f;
      var ocMin = cloud.boundingBox.center - Vector3.One * rootSize;
      var ocMax = cloud.boundingBox.center + Vector3.One * rootSize;
      var depth = Mathf.CeilToInt( MathX.Exponent( 2, rootSize/ samplingPixelDensity ) );

      var ocTree = new PointCloudOcTree( ocMin, ocMax, rootSize, depth );

      var minSize = ocTree.GetSizeForLevel( depth );

      this.LogInfo( "OcTree MinDepth", minSize._CM() );
      
      this.RequestNextFrame();
      time = Async.PrintAndUpdateMS( time, "Inserting to octree"  );
      ocTree.Insert( cloud.points );
      time = Async.PrintAndUpdateMS( time, "Inserting to octree Done"  );
      var level = ocTree.GetLevelForSize( outputPixelDensity );

       this.LogInfo( "Combining" );
      this.RequestNextFrame();
      time = Async.PrintAndUpdateMS( time, "Combining"  );
      
      ocTree.normalSeperation = false;
      ocTree.CombineUntilLevel( level );
      
      time = Async.PrintAndUpdateMS( time, "Combining Done"  );

      this.LogInfo( "Combining level", level, "Max Level", depth );
      


      this.LogInfo( "Cloud Points:", cloud.points.Count );

      if ( outputs != null )
      {
        outputs = [];
      }



      this.DestroyChildren();


      var levelMap = ocTree.GetLevelMap();

      var levelInstances = new List<MeshInstance3D>();

      var lodBuilder = LODBuilder.ByCameraDistance();

      lodBuilder.SetMaterial( material );

      // lodBuilder.Add( 0, mesh.Mesh, mesh.GetSurfaceOverrideMaterial( 0 ) );

      var meshGeometries = new List<MeshGeometry>();

      var infos = new List<string>();

      for ( int i = level + 1; i < depth; i++ )
      {
        var filledLevels = levelMap[ i ].Filter( l => l.values != null );
        var points = new List<Point>();

        filledLevels.ForEach( 
          l => 
          {
            points.AddRange( l.values );
          }
        );

        var pc = new PointCloud();
        pc.points = points;
        pc.ComputeCenter();
        pc.SortByDistanceToCenter();

        var levelSize = ocTree.GetSizeForLevel( i );
        var distance = Cameras.ComputePixelDistanceForSizeVertical( 60, levelSize, new Vector2( 1920, 1080 ) ) * 4;

        // this.LogInfo( 
        //   "Level", i, 
        //   "Points", pc.points.Count, 
        //   "Size", RegexUtility._CM( levelSize ),
        //   "Distance", RegexUtility._M( distance )
        // );

        var mg = pc.GenerateMeshGeometry( levelSize/2f );
        meshGeometries.Add( mg );


        infos.Add( 
          RJLog.Stringify(
          "Level", i, 
          "Points", pc.points.Count, 
          "Size", levelSize._CM(),
          "Vertices", mg.numVertices,
          "Normals", mg.numNormals,
          "UVs", mg.numUV2s,
          "Colors", mg.numColors
          )
        );

        var t = MathX.Map( i, level + 1, depth - 1, 1, 0 );
        mg.lodEdgeLength = levelSize;
        // mg.ApplyTranslation( new Vector3( 100 * t, 0, 0 ) );

        var mesh = pc.GenerateMesh( levelSize / 2f );

        lodBuilder.Add( levelSize, mesh );

        if ( createOutputs )
        {
          var meshInstance = this.CreateChild<MeshInstance3D>();
          meshInstance.Mesh = mesh;
          meshInstance.MaterialOverride = material;

          levelInstances.Add( meshInstance );
        }
      }

      if ( createOutputs )
      {
        outputs = levelInstances.ToArray(); 
      }

      outputInfos = infos.ToArray();

      var lodMesh = this.CreateChild<MeshInstance3D>( "LOD Mesh" );
      meshGeometries.Reverse();
      var mainGeometry = meshGeometries[ 0 ];
      var lerpingData = new LODLerpingData();
      lerpingData.lowerLevelWeights = lowerLerp;
      lerpingData.higherLevelWeights = higherLerp;
      lerpingData.lerpSteps = lerpSteps;
      lodMesh.Mesh = MeshGeometry.GeneratePointsLODMesh( meshGeometries, lerpingData, false );

      lodMesh.SetSurfaceOverrideMaterial( 0, material );

      lod = lodBuilder.Create( this, "LOD" );
      lod.lod0 = lod.CreateChild<MeshInstance3D>();
      lod.lod0.Mesh = mesh.Mesh;
      lod.lod0.MaterialOverride = mesh.GetSurfaceOverrideMaterial( 0 );
      lod.distanceScale = 4;


      // cloud.SortByDistanceToCenter();

      // numPoints = cloud.points.Count;
      // squareTexture = (int)Mathf.Pow( numPoints, 0.5f );
      // output.Mesh = cloud.GenerateMesh();

      working = false;

    }
  }
}