using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Generator
{

	[TestClass]
	public class CodeGen
	{
		private void AddTree( List<SyntaxTree> syntaxTree, string path )
		{
			var parseOptions = CSharpParseOptions.Default.WithLanguageVersion( LanguageVersion.Default );
			var code = System.IO.File.ReadAllText( path );
			var tree = CSharpSyntaxTree.ParseText( text: code, options: parseOptions, path: path, encoding: System.Text.Encoding.UTF8 );
			syntaxTree.Add( tree );
		}

		CSharpCompilation Build( string assemblyName, params string[] files )
		{
			List<SyntaxTree> SyntaxTree = new List<SyntaxTree>();

			foreach ( var file in files )
				AddTree( SyntaxTree, $"data/codegen/{file}" );

			var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary )
									.WithConcurrentBuild( true )
									.WithOptimizationLevel( OptimizationLevel.Debug )
									.WithGeneralDiagnosticOption( ReportDiagnostic.Info )
									.WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu )
									.WithAllowUnsafe( false );

			var refs = new List<MetadataReference>();

			var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location );
			refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) );
			refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) );

			refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) );
			refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); // Sandbox.System

			CSharpCompilation compiler = CSharpCompilation.Create( $"{assemblyName}.dll", SyntaxTree, refs, optn );

			var processor = new Sandbox.Generator.Processor();
			processor.AddonName = assemblyName;
			processor.PackageAssetResolver = ( p ) => $"/{p}/model_mock.mdl";
			processor.Run( compiler );

			compiler = processor.Compilation;

			foreach ( var tree in compiler.SyntaxTrees )
			{
				System.Console.WriteLine( tree.FilePath );
				System.Console.WriteLine( tree.GetText().ToString() );
			}

			return compiler;
		}

		CSharpCompilation BuildString( string contents, string assemblyName = "assembly_name" )
		{
			List<SyntaxTree> SyntaxTree = new List<SyntaxTree>();

			{
				var parseOptions = CSharpParseOptions.Default.WithLanguageVersion( LanguageVersion.Default );
				var code = contents;
				var tree = CSharpSyntaxTree.ParseText( text: code, options: parseOptions, path: "Program.cs", encoding: System.Text.Encoding.UTF8 );
				SyntaxTree.Add( tree );
			}

			var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary )
									.WithConcurrentBuild( true )
									.WithOptimizationLevel( OptimizationLevel.Debug )
									.WithGeneralDiagnosticOption( ReportDiagnostic.Info )
									.WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu )
									.WithAllowUnsafe( false );

			var refs = new List<MetadataReference>();

			var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location );
			refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) );
			refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) );

			refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) );
			refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); // Sandbox.System

			CSharpCompilation compiler = CSharpCompilation.Create( $"{assemblyName}.dll", SyntaxTree, refs, optn );

			var processor = new Sandbox.Generator.Processor();
			processor.AddonName = assemblyName;
			processor.PackageAssetResolver = ( p ) => $"/{p}/model_mock.mdl";
			processor.Run( compiler );

			compiler = processor.Compilation;

			foreach ( var tree in compiler.SyntaxTrees )
			{
				System.Console.WriteLine( tree.FilePath );
				System.Console.WriteLine( tree.GetText().ToString() );
			}

			return compiler;
		}

		[TestMethod]
		[DataRow( "TestVar.cs" )]
		[DataRow( "TestRpc.cs" )]
		[DataRow( "TestReplicate.cs" )]
		[DataRow( "TestStackOverflow.cs" )]
		[DataRow( "TestRpc.Entity.cs" )]
		[DataRow( "TestReplicate.Entity.cs" )]
		public void DoCodeGen( string filename )
		{
			Build( "do_code_gen", filename );
		}


		[TestMethod]
		public void AdditionalFiles()
		{
			List<SyntaxTree> SyntaxTree = new List<SyntaxTree>();

			var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary )
									.WithConcurrentBuild( true )
									.WithOptimizationLevel( OptimizationLevel.Debug )
									.WithGeneralDiagnosticOption( ReportDiagnostic.Info )
									.WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu )
									.WithAllowUnsafe( false );

			var refs = new List<MetadataReference>();

			var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location );
			refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) );
			refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) );

			refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) );
			refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) );

			CSharpCompilation compiler = CSharpCompilation.Create( $"poopy.dll", SyntaxTree, refs, optn );

			// Process Razor files using RazorProcessor before running the Processor
			foreach ( var file in System.IO.Directory.EnumerateFiles( "data/codegen/", "*.razor" ) )
			{
				var razorText = System.IO.File.ReadAllText( file );
				var generatedCode = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, file );
				var razorTree = CSharpSyntaxTree.ParseText( generatedCode, path: $"_gen_{System.IO.Path.GetFileName( file )}.cs", encoding: System.Text.Encoding.UTF8 );
				compiler = compiler.AddSyntaxTrees( razorTree );
			}

			var processor = new Sandbox.Generator.Processor();
			processor.AddonName = $"poopy";
			processor.Run( compiler );

			compiler = processor.Compilation;

			foreach ( var tree in compiler.SyntaxTrees )
			{
				System.Console.WriteLine( tree.GetText().ToString() );
			}
		}

		/// <summary>
		/// Note that our aim here is to prevent 99% of accidental overflows. There are still going to be ways
		/// to overflow and force quit.. but this should all stop the accidental stuff.
		/// </summary>
		//	[TestMethod]
		public void StackOverflowPrevention()
		{
			var compiler = Build( "do_code_gen", "TestStackOverflow.cs" );

			// actually compile
			using var stream = new MemoryStream();
			var result = compiler.Emit( stream );
			Assert.IsTrue( result.Success, "Failed to compile!" );

			// Load it
			var assemblyBytes = stream.ToArray();
			var asm = System.Reflection.Assembly.Load( assemblyBytes );

			// Call RecurseForever
			var TestStackOverflow = asm.GetType( "TestStackOverflow" );


			// Should successfully throw an exception due to stack overflowing
			var RecurseForever = TestStackOverflow.GetMethod( "RecurseForever" );
			Assert.ThrowsException<TargetInvocationException>( () => RecurseForever.Invoke( null, null ) );

			var RecurseForeverInline = TestStackOverflow.GetMethod( "RecurseForeverInline" );
			Assert.ThrowsException<TargetInvocationException>( () => RecurseForeverInline.Invoke( null, null ) );
		}

		[TestMethod]
		public void TestCloudAssets()
		{
			var compiler = Build( "do_code_gen", "CloudAssetTest.cs" );

			Assert.AreEqual( 2, compiler.SyntaxTrees.Count(), "Should have the original syntaxtree plus the generated one" );

			var added = compiler.SyntaxTrees.First( x => x.FilePath.Contains( "_gen__AddedCode.cs" ) );
			Assert.IsTrue( added.GetText().ToString().Contains( "[assembly:Sandbox.Cloud.Asset( " ), "Outputted text should contain the asset attribute" );


		}

		[TestMethod]
		public void TestWrapCall()
		{
			var compiler = Build( "do_code_gen", "TestWrapCall.cs" );
			var tree = compiler.SyntaxTrees.First();

			System.Console.WriteLine( tree.GetText().ToString() );

			Assert.IsTrue( tree.GetText().ToString().Contains( "WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod {Resume = () => {},Object = null,MethodIdentity = -1168963981,MethodName = \"TestWrappedStaticCall\",TypeName = \"TestWrapCall\",IsStatic = true,Attributes = __m_1168963981__Attrs}, arga );" ), "Generated code should wrap static method call" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => {},Object = this,MethodIdentity = -446800946,MethodName = \"TestWrappedInstanceCall\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_446800946__Attrs}, arga );" ), "Generated code should wrap instance method call" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod {Resume = () => {},Object = null,MethodIdentity = 1638661065,MethodName = \"TestWrappedStaticCallNoArg\",TypeName = \"TestWrapCall\",IsStatic = true,Attributes = __1638661065__Attrs} );" ), "Generated code should wrap static method call with no arg" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => {},Object = this,MethodIdentity = -1769572979,MethodName = \"TestWrappedInstanceCallNoArg\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_1769572979__Attrs} );" ), "Generated code should wrap instance method call with no arg" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "public void ExpressionBodiedBroadcast() => OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => Log.Info( \"Test.\" ),Object = this,MethodIdentity = 1201362747,MethodName = \"ExpressionBodiedBroadcast\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __1201362747__Attrs} );" ), "Generated code should wrap expression bodied method" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "Object = this,MethodIdentity = -1316352073,MethodName = \"TestWrappedInstanceCallReturnType\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_1316352073__Attrs}, arga );" ), "Generated code should wrap instance method call with return type" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "return WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod<Task<bool>> {Resume = async () =>" ), "Generated code should wrap async Task method call" );
		}

		[TestMethod]
		public void TestPreprocessorWrappedCall()
		{
			var compiler = Build( "do_code_gen", "TestPreprocessorWrapCall.cs" );
			var tree = compiler.SyntaxTrees.First();
			var treeText = tree.GetText().ToString();

			Assert.IsTrue( treeText.Contains( "{\r\n#if true\r\n\t\tif ( true )\r\n\t\t{\r\n\t\t\t// Hello there!\r\n\t\t}\r\n#endif\r\n\t}" ), "Maintain preprocessor directive in body" );
			Assert.IsFalse( treeText.Contains( $");}}#endif" ), "No trailing trivia outside of body" );
		}

		[TestMethod]
		public void TestWrapGet()
		{
			var compiler = Build( "do_code_gen", "TestWrapGet.cs" );
			var tree = compiler.SyntaxTrees.First();
			System.Console.WriteLine( tree.GetText().ToString() );

			Assert.IsTrue( tree.GetText().ToString().Contains( "return (bool)WrapGet.OnWrapGetStatic(new global::Sandbox.WrappedPropertyGet<bool> { Value = value, Object = null, IsStatic = true, TypeName = \"TestWrapGet\", PropertyName = \"StaticProperty\", MemberIdent = 2112852062, Attributes = __StaticProperty__Attrs });" ), "Generated code should wrap static property get accessor" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "return (bool)OnWrapGet(new global::Sandbox.WrappedPropertyGet<bool> { Value = value, Object = this, IsStatic = false, TypeName = \"TestWrapGet\", PropertyName = \"InstanceProperty\", MemberIdent = 1816497229, Attributes = __InstanceProperty__Attrs });" ), "Generated code should wrap instance property get accessor" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "_repback__InstanceProperty= true;" ), "Generated code should copy initializer" );
		}

		[TestMethod]
		public void TestWrapSet()
		{
			var compiler = Build( "do_code_gen", "TestWrapSet.cs" );
			var tree = compiler.SyntaxTrees.First();
			System.Console.WriteLine( tree.GetText().ToString() );

			Assert.IsTrue( tree.GetText().ToString().Contains( "WrapSet.OnWrapSetStatic(new global::Sandbox.WrappedPropertySet<bool> { Value = value, Object = null, Setter = (v) =>" ), "Generated code should wrap static property set accessor" );
			Assert.IsTrue( tree.GetText().ToString().Contains( "OnWrapSet(new global::Sandbox.WrappedPropertySet<bool> { Value = value, Object = this, Setter = (v) =>" ), "Generated code should wrap instance property set accessor" );
		}

		[TestMethod]
		public void CodeGenFullyQualified()
		{
			var compiler = BuildString( """
		        namespace CodeGenFullyQualified;
		        
		        [Sandbox.CodeGenerator(Sandbox.CodeGeneratorFlags.WrapPropertyGet | Sandbox.CodeGeneratorFlags.Static | Sandbox.CodeGeneratorFlags.Instance, "CodeGenFullyQualified.TestCodeGenAttribute.GetWrapper")]
		        [System.AttributeUsage(System.AttributeTargets.Property)]
		        public class TestCodeGenAttribute : System.Attribute
		        {
		        	internal static T GetWrapper<T>(Sandbox.WrappedPropertyGet<T> property)
		        	{
		        		return property.Value;
		        	}
		        }
		        
		        public class Program
		        {
		            [TestCodeGen] public bool TestProperty { get; set; }
		        
		            public static void Main()
		            {
		            }
		        }
		        """ );

			var tree = compiler.SyntaxTrees.First();
			Assert.IsTrue( compiler.GetDiagnostics().Length == 0, "Should be no compile errors" );
		}

		[TestMethod]
		public void StringTokenReplacement()
		{
			var compiler = BuildString( """"
				using Sandbox;
				
				public static class Program
				{
					public static void Main()
					{

						RenderAttributes ra = new ();

						ra.Set( "PoopSock", Vector3.One ); 

					}
				}

				"""" );

			var tree = compiler.SyntaxTrees.First();
			Assert.IsTrue( tree.GetText().ToString().Contains( "\"PoopSock\",1590150592U" ), "Should have replaced string token" );

		}

		[TestMethod]
		public void ParameterDescriptionAttribute()
		{
			var compiler = Build( "do_code_gen", "Descriptions.cs" );
			var tree = compiler.SyntaxTrees.First();

			Assert.IsTrue( tree.GetText().ToString().Contains( "[DescriptionAttribute( \"This parameter has a description!\" )]int baz" ) );
		}


		[TestMethod]
		public void ReturnsDescriptionAttribute()
		{
			var compiler = Build( "do_code_gen", "Descriptions.cs" );
			var tree = compiler.SyntaxTrees.First();

			Assert.IsTrue( tree.GetText().ToString().Contains( "[return:DescriptionAttribute( \"Here's a description of the return value!\" )]" ) );
		}
	}
}
