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



namespace Rokojori
{
  [Tool]
  [GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Scatterer.svg") ]
  public partial class Scatterer:Node3D
  {
    [ExportGroup("Update Behaviour")]
    [Export]
    public bool update = false;
    [Export]
    public bool updateAlways = false;

    [ExportGroup("Spawing")]
    [Export]
    public bool removeDiscarded = false;
    [Export]
    public Node3D[] streamSources = new Node3D[ 0 ];
    [Export]
    public float streamCullDistance = 100;
    
    [Export]
    public float streamFadeRange = 20;

    [Export]
    public int maxPoints = 1000000;


    [ExportGroup("Clean Up")]
    [Export]
    public Node3D[] containersToClearNodes = new Node3D[ 0 ];
    [Export]
    public bool clearContainers = false;
    [Export]
    public bool clearCache = false;

    

    [ExportGroup("Read Only")]
    [Export]
    public int X_createdPoints = 0;
    [Export]
    public int X_instantiatedPoints = 0;
    [Export]
    public int X_reusedPoints = 0;
    [Export]
    public int X_remappedPoints = 0;    


    List<Vector3> _streamPositions = new List<Vector3>();


    public override void _Process( double delta )
    {
      if ( clearCache )
      {
        clearCache = false;
        ClearCache();
      }
      
      if ( clearContainers )
      {
        clearContainers = false;
        ClearContainers(); 
      }


      if ( ! ( update || updateAlways ) )
      {
        return;
      }

      update = false;

      ScatterAndInstantiatePoints();
    }

    public void ClearContainers()
    {      
      Arrays.ForEach( containersToClearNodes,
        c => 
        { 
          Nodes.RemoveAndDeleteChildren( c );
        }
      );
    }

    class InstantiatedScatterPoint
    {
      public string hash;
      public ScatterPoint scatterPoint;
      public Node3D output;
    }

    Dictionary<string,InstantiatedScatterPoint> _instantiatedPoints = new Dictionary<string, InstantiatedScatterPoint>();
    MapList<Mesh,MassRenderer> _instancedMassRenderers = new MapList<Mesh, MassRenderer>();
  
    Dictionary<Scatterer,int> _scattererIDs = new Dictionary<Scatterer, int>();

    public enum PositionMode
    {
      Local_As_Global,
      Local_Plus_ScattererGlobal
    }

    [Export]
    public PositionMode positionMode;

    public void ClearCache()
    {
      _instantiatedPoints.Clear();
      _scattererIDs.Clear();
      
      foreach ( var mmr in _instancedMassRenderers )
      {
        mmr.Value.ForEach( mr => Nodes.RemoveAndDelete( mr ) );
      }

      _instancedMassRenderers.Clear();
      ClearContainers();
    }

    int GetScattererID( Scatterer s )
    {
      if ( ! _scattererIDs.ContainsKey( s ) )
      {
        _scattererIDs[ s ] = _scattererIDs.Count;
      }

      return _scattererIDs[ s ]; 
    }

    string GetScatterPointHash( ScatterPoint p )
    {
      var scatterID = GetScattererID( p.creator ) + ":" + p.creatorID;

      return scatterID;
    }

    public void ScatterAndInstantiatePoints()
    {
      if ( _instantiatedPoints.Count == 0 )
      {
        ClearContainers();
      }

      foreach ( var mmr in _instancedMassRenderers )
      {
        var list = mmr.Value;

        for ( int i = list.Count - 1; i >=0 ; i-- )
        {
          list[ i ] = Nodes.EnsureValid( list[ i ] );

          if ( list[ i ] == null )
          {
            list.RemoveAt( i );
          }
          else
          {
            list[ i ].Clear();
          }
        }         
      }

      var points = Scatter( new List<ScatterPoint>(), this );

      X_createdPoints = points.Count;
      X_instantiatedPoints = 0;
      X_reusedPoints = 0;
      X_remappedPoints = 0;

      var usedPoints = new HashSet<string>();

      points.RemoveAll( p => ! p.visible );

      points.ForEach( p =>       
        { 
          var hash = GetScatterPointHash( p );

          if ( p.instanced )
          {
            AddInstanced( p );
            usedPoints.Add( hash );
            return;
          }

         

          if ( ! CanReuse( hash, p ) )
          {
            return;
          }

          var existing = _instantiatedPoints[ hash ];           
          existing.scatterPoint = p;
          existing.scatterPoint.UpdateInstantiated( existing.output );

          usedPoints.Add( hash );

          X_reusedPoints ++;
          return;
          
        }
      );

      var unused = new MapList<PackedScene,InstantiatedScatterPoint>();

      foreach ( var vk in _instantiatedPoints )
      {
        var ip = vk.Value;

        if ( usedPoints.Contains( ip.hash ) )
        {
          continue;
        }

        unused.Add( ip.scatterPoint.scene, ip );
      }

      points.ForEach( p =>       
        {
          var hash = GetScatterPointHash( p );

          if ( usedPoints.Contains( hash ) )
          {
            return;                      
          }          

          if ( unused.ContainsKey( p.scene ) )
          {
            var unusedPoint = unused[ p.scene ].Find( ip => ip.scatterPoint.CanBeReusedBy( p ) );
            
            if ( unusedPoint != null )
            {
              unused.Remove( p.scene, unusedPoint );
              unusedPoint.scatterPoint = p;
              unusedPoint.scatterPoint.UpdateInstantiated( unusedPoint.output );

              usedPoints.Add( hash );

              X_remappedPoints ++;
              return;
            }
          }

          var instantiatedOutput = p.Instantiate();

          if ( instantiatedOutput == null )
          {
            return;
          }

          var ip = new InstantiatedScatterPoint();
          ip.hash = hash;
          ip.output = instantiatedOutput;
          ip.scatterPoint = p;

          if ( _instantiatedPoints.ContainsKey( hash ) )
          {
            Nodes.RemoveAndDelete( _instantiatedPoints[ hash ].output );
          }
          
          _instantiatedPoints[ hash ] = ip;
          usedPoints.Add( hash );

          X_instantiatedPoints++;
        }
      );


      Dictionaries.RemoveAll(
        _instantiatedPoints, ( k, v ) =>
        {
          if ( usedPoints.Contains( k ) )
          {
            return false;
          }

          Nodes.RemoveAndDelete( v.output );

          return true;
        }
      );

      foreach ( var mmr in _instancedMassRenderers )
      {
        mmr.Value.ForEach( mr => mr.Create() );
      }


    }

    MapList<PackedScene,Transformable<SingleMaterialMesh>> _sceneMeshes = new MapList<PackedScene, Transformable<SingleMaterialMesh>>();


    public void AddInstanced( ScatterPoint sp )
    {
      var packedScene = sp.scene;
      
      if ( ! _sceneMeshes.ContainsKey( packedScene ) )
      {
        var node = sp.scene.Instantiate<Node>();
        var meshesWithMaterials = MeshExtractor.ExtractMeshesInHierarchy( node );

        _sceneMeshes[ packedScene ] = meshesWithMaterials;
      }

      var meshes = _sceneMeshes[ packedScene ];

      meshes.ForEach(
        ( m )=>
        {
          var massRenderer = GetMassRenderer( sp, m.item );
          var transform = sp.globalTransform3D;
          massRenderer.QueueObject( transform );
        }
      );
    }

    MassRenderer GetMassRenderer( ScatterPoint sp, SingleMaterialMesh mwm )
    {
      if ( ! _instancedMassRenderers.ContainsKey( mwm.mesh ) )
      {
        var massRenderer = new List<MassRenderer>(); 
       
        _instancedMassRenderers[ mwm.mesh ] = massRenderer;
      } 

      var mms =_instancedMassRenderers[ mwm.mesh ];

      var mm = mms.Find( m => m.materialOveride == mwm.material );

      if ( mm == null )
      {
        var c = sp.parent.CreateChild<MassRenderer>( "MassRenderer" );

        c.materialOveride = mwm.material; 
        c.mesh = mwm.mesh;
        
        _instancedMassRenderers.Add( mwm.mesh, c );

        return c;
      } 

      return mm;
    }

    bool CanReuse( string hash, ScatterPoint sp )
    {
      if ( ! _instantiatedPoints.ContainsKey( hash ) )
      {
        return false;
      }

      var existing = _instantiatedPoints[ hash ];

      var canBeReused = existing.scatterPoint.CanBeReusedBy( sp );
      
      return canBeReused;
    }

    public List<ScatterPoint> Scatter( List<ScatterPoint> points, Scatterer root = null )
    {
      if ( root == null )
      {
        root = this;
      }

      var returnedPoints = _Scatter( points, root );

      if ( removeDiscarded )
      {
        returnedPoints.RemoveAll( p => ! p.visible );
      }

      if ( root.streamSources != null && root.streamSources.Length > 0 )
      {
        var fadeDistance = root.streamCullDistance - root.streamFadeRange;

        returnedPoints.RemoveAll(
          ( p ) =>
          {
            for ( int s = 0; s < root.streamSources.Length; s++ )
            {
              var distance = ( p.globalPosition - root.streamSources[ s ].GlobalPosition ).Length();

              if ( distance < fadeDistance )
              {
                return false;
              }

              var treshold = MathX.RemapClamped( distance, fadeDistance, root.streamCullDistance, 1, 0 );

              var result = Noise.Perlin( p.globalPosition ) > treshold;

              if ( ! result )
              {
                return false; 
              }
              
            }

            return true;
          }
        );
      }

      if ( returnedPoints.Count > root.maxPoints )
      {
        returnedPoints.RemoveRange( root.maxPoints, returnedPoints.Count - root.maxPoints );
      }

      return returnedPoints;
    }

    protected bool IsChildEnabled( Scatterer s, bool childrenNeedToBeVisible, bool childrenNeedToBeProcessing )
    {
      var enabled = true;

      if ( childrenNeedToBeVisible )
      {
        enabled = enabled && s.Visible;
      }

      if ( childrenNeedToBeProcessing )
      {
        enabled = enabled && ( s.ProcessMode != ProcessModeEnum.Disabled );
      }

      return enabled;
    }

    protected virtual List<ScatterPoint> _Scatter( List<ScatterPoint> points, Scatterer root )
    {
      return points;
    }
  }
}