using Godot;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;

namespace Rokojori
{ 
  [Tool]
  [GlobalClass]
  public partial class CSShaderClassGenerator:Node
  { 
    [ExportToolButton( "Generate")]
    public Callable generateButton => Callable.From(
      ()=>
      {
        Generate();
      }
    );


    [Export]
    public Shader[] shaders;

    [ExportGroup( "Common Base Material")]
    [Export]
    public bool createCommonBase = false;

    [Export]
    public string baseMaterialName = "";

    [Export]
    public string baseMaterialDirectoryPath = "";

    [Export]
    public string baseMaterialParentClass = "CustomMaterial";

    [ExportGroup( "Variants")]
    
    [Export]
    public bool includeInGeneration = true;

    [Export]
    public CSShaderVariant[] variants = [];

    
    
    public static readonly string ShaderName = "${ShaderName}";
    public static readonly string CustomMaterialBase = "${CustomMaterialBase}";
    public static readonly string ShaderResourcePath = "${ShaderResourcePath}";
    public static readonly string BaseOnly = "${BaseOnly}";
    public static readonly string ShaderStaticPropertyNames = "${ShaderStaticPropertyNames}";
    public static readonly string ShaderInstancePropertiesDeclarations = "${ShaderInstancePropertiesDeclarations}";
    public static readonly string ShaderInstancePropertiesInitializers = "${ShaderInstancePropertiesInitializers}";
    public static readonly string MaterialPresets = "${MaterialPresets}";
    

    public void Generate()
    {
      var shaderMap = new Dictionary<Shader, System.Tuple<string, List<UniformMember>>>();
      var combined = new Dictionary<string,int>();
      var infoToMember = new Dictionary<string,UniformMember>();
      var resourcePaths = new Dictionary<Shader,string>();

      var shaders = this.shaders;

      shaders.ForEach( s => { resourcePaths[ s ] = s.ResourcePath; } );

      if ( variants.Length > 0 )
      {
        var variantShaders = new List<Shader>();

        shaders.ForEach(
          s =>
          {
            variants.ForEach(
              v =>
              {
                var resourcePath = s.ResourcePath;
                var shaderCode = v.GetVariant( s.Code );
                var savePath = v.GetSavingPath( s.ResourcePath );

                var shader = new Shader();
                shader.Code = shaderCode;
                shader.ResourcePath = savePath;

                ResourceSaver.Save( shader, savePath );
                resourcePaths[ shader ] = savePath;
                // this.LogInfo( "Saving:", resourcePath, ">>", savePath );
                // this.LogInfo( shaderCode );

                variantShaders.Add( shader );

              }
            );
          }
        );

        var updatedList = shaders.ToList();
        updatedList.Add( variantShaders );

        shaders = updatedList.ToArray();
      }

      shaders.ForEach( 
        s => 
        {            
          var resourcePath = resourcePaths[ s ];
          var members = Shaders.GetUniformMembers( s );

          shaderMap[ s ] = new System.Tuple<string, List<UniformMember>>( resourcePath, members );
          
          members.ForEach(
            ( m )=>
            {
              var info = m.ToString();

              // this.LogInfo( resourcePath, info );
              if ( ! combined.ContainsKey( info ) )
              {
                combined[ info ] = 1;
                infoToMember[ info ] = m;
              }
              else
              {
                combined[ info ] = combined[ info ] + 1;
              }
            }
          );
          
        }
      );

      var customMaterialBase = "CustomMaterial";
      var commonUniforms = new List<UniformMember>();

      if ( createCommonBase )
      {
        customMaterialBase = baseMaterialName + "Material";

       
        // var info = new List<string>();

        foreach ( var kv in combined )
        { 
          var onAll = kv.Value == shaders.Length;
          var smallKey = kv.Key.Substring( 0, kv.Key.IndexOf( "(" ) );

          // info.Add( smallKey + ": " + ( onAll ? "all" : ( kv.Value ) ) ); 

          if ( onAll )
          {
            commonUniforms.Add( infoToMember[ kv.Key ] );
          }
         
        }   

        // this.LogInfo( info.Join( "\n" ) );


        var baseMaterialShaderPath = RegexUtility.JoinPaths( 
          [ ProjectSettings.GlobalizePath( baseMaterialDirectoryPath ), baseMaterialName + ".gdshader" ] 
        );

        Generate( baseMaterialShaderPath, commonUniforms, [], baseMaterialParentClass, true );
      }

      
      shaders.ForEach(
        s => 
        {
          var resourcePath = shaderMap[ s ].Item1;
          var members      = shaderMap[ s ].Item2;

          Generate( resourcePath, members, commonUniforms, customMaterialBase );
        }
      );
    }

    public void Generate( string resourcePath, List<UniformMember> members, List<UniformMember> inheritedMembers, string customMaterialBase = "CustomMaterial", bool baseOnly = false )
    {
      // var resourcePath = shaderFile.ResourcePath;
      // var shader = ResourceLoader.Load<Shader>( resourcePath );
      var resourceParentPath = RegexUtility.ParentPath( resourcePath );
      var absoluteFilePath = FilePath.Absolute( ProjectSettings.GlobalizePath( resourcePath ) );
      var absoluteParentPath = absoluteFilePath.CreateAbsoluteParent();

      RJLog.Log( resourcePath, resourceParentPath, absoluteFilePath.absolutePath );

      var shaderName = absoluteFilePath.fileName;

      // var members = Shaders.GetUniformMembers( shader );

      var templatePath = ProjectSettings.GlobalizePath( "res://addons/rokojori_action_library/Runtime/Shading/Tools/CSShaderClassGenerator/CSShaderClassTemplate.txt" );

      var template = FilesSync.LoadUTF8( templatePath );



      var staticProps = new List<string>();
      var instanceProps = new List<string>();
      var instanceInitializers = new List<string>();

      members.ForEach(
        ( m )=>
        {
          var type = m.GetPropertyNameType();

          var csType = type;

          if ( type == "Texture2D" )
          {
            type = "Sampler2D";
          }


          var name = m.name;
          var csName = name.ToCamelCase();

          var declaration = "public static readonly ${type}PropertyName ${csName} = ${type}PropertyName.Create( \"${name}\" );";
          var instance =    "public readonly CustomMaterialProperty<${csType}> ${csName};";

          var init = "${csName} = new CustomMaterialProperty<${csType}>( this, ${ShaderName}Shader.${csName} );";
          

          if ( type == "Float" )
          {
            csType = "float";
          }
          else if ( type == "Int" )
          {
            csType = "int";
          }
          else if ( type == "Bool" )
          {
            csType = "bool";
          }
          

          var map = new StringMap();
          map[ "${type}" ] = type;
          map[ "${csType}"] = csType;
          map[ "${name}" ] = name;
          map[ "${csName}" ] = csName;
          map[ "${ShaderName}"] = shaderName;
          

          staticProps.Add( map.ReplaceAll( declaration ) );

          if ( inheritedMembers.Find( im => m.Equals( im ) ) == null )
          {
            instanceProps.Add( map.ReplaceAll( instance ) );
            instanceInitializers.Add( map.ReplaceAll( init ) );
          } 


        }
      );

      var materials = FilesSync.GetFiles( absoluteParentPath.fullPath, 
        ( f ) =>
        {
          return f.HasFileExtension( "material" );
        }
      ); 

      var presets = new List<string>();

      materials.ForEach( m => 
        {
          var materialName = m.fileName;
          var shortName = materialName;

          if ( materialName.ToLower().StartsWith( shaderName.ToLower() ) )
          {
            shortName = shortName.Substring( shaderName.Length );
          }

          var declaration = 
          "public static readonly CachedResource<${ShaderName}Material> ${csName} = CustomMaterial.Cached<${ShaderName}Material>(\n" +
          "      \"${materialPath}\"\n" +
          "    );";


          var mMap = new StringMap();
          mMap[ "${materialPath}" ] = resourceParentPath + "/" + materialName + ".material";
          mMap[ "${ShaderName}" ]   = shaderName;

          var csName = RegexUtility.ToValidCSName( shortName ).ToPascalCase();

          if ( System.Char.IsDigit( csName[ 0 ] ) )
          {
            csName = "_" + csName;
          }

          mMap[ "${csName}" ]   = csName;

          

          presets.Add( mMap.ReplaceAll( declaration ) );
        }
      );


      var variables = new StringMap();
      variables[ CSShaderClassGenerator.ShaderName ] = shaderName;
      variables[ CSShaderClassGenerator.CustomMaterialBase ] = customMaterialBase;
      variables[ CSShaderClassGenerator.ShaderResourcePath ] = resourcePath;
      variables[ CSShaderClassGenerator.BaseOnly ] = baseOnly ? "" : "//";

      variables[ CSShaderClassGenerator.ShaderStaticPropertyNames ] = Lists.Join( staticProps, "\n    " );
      variables[ CSShaderClassGenerator.ShaderInstancePropertiesDeclarations ] = Lists.Join( instanceProps, "\n    " );
      variables[ CSShaderClassGenerator.ShaderInstancePropertiesInitializers ] = Lists.Join( instanceInitializers, "\n      " );

      variables[ CSShaderClassGenerator.MaterialPresets ] = Lists.Join( presets, "\n      " );

      var shaderClass = variables.ReplaceAll( template );

      var outputPath = absoluteFilePath.AddToFileName( "Material" ).WithExtension( "cs" );
      
  

      FilesSync.SaveUTF8( outputPath.fullPath, shaderClass );

      this.LogInfo( "\nGenerated >> ", outputPath.fullPath, "\n" + shaderClass );


    }
  }
}