using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Scripting;
using RobotNet.Script.Shares;
using System.Collections.Immutable;
using System.Reflection;
using System.Text;

namespace RobotNet.ScriptManager.Helpers;

public static class ScriptConfiguration
{
    public static readonly string ScriptStorePath;
    public static readonly string DllsPath;
    public static readonly ScriptOptions ScriptOptions;
    public static readonly ImmutableArray<MetadataReference> MetadataReferences;
    public static readonly ImmutableArray<string> UsingNamespaces;
    public static readonly string UsingNamespacesScript;
    public static readonly string DeveloptGlobalsScript;
    public static readonly string RuntimeGlobalsScript;

    static ScriptConfiguration()
    {
        ScriptStorePath = "scripts";
        DllsPath = "dlls";
        UsingNamespaces = ["System", "System.Collections.Generic", "System.Linq.Expressions", "System.Threading", "System.Threading.Tasks", "System.Runtime.CompilerServices", "RobotNet.Script"];
        UsingNamespacesScript = string.Join(Environment.NewLine, UsingNamespaces.Select(ns => $"using {ns};"));

        List<MetadataReference> metadataRefs = [];
        var currentDirectory = Directory.GetCurrentDirectory();
        if (Directory.Exists(DllsPath))
        {
            foreach (var dll in Directory.GetFiles(DllsPath, "*.dll"))
            {
                metadataRefs.Add(MetadataReference.CreateFromFile(Path.Combine(currentDirectory, dll), properties: MetadataReferenceProperties.Assembly));
            }
        }

        MetadataReferences = [.. metadataRefs];
        var options = ScriptOptions.Default;
        options.MetadataReferences.Clear();
        ScriptOptions = options.AddReferences(MetadataReferences).AddImports(UsingNamespaces).WithEmitDebugInformation(false);

        DeveloptGlobalsScript = BuildDeveloptGlobalsScript();
        RuntimeGlobalsScript = BuildRuntimeGlobalsScript();
    }

    public static string GetScriptCode() => ReadAllTextFromFolder(ScriptStorePath);

    private static string ReadAllTextFromFolder(string path)
    {
        var dirInfo = new DirectoryInfo(path);

        var code = string.Join(Environment.NewLine, [.. dirInfo.GetFiles("*.cs").Select(f => File.ReadAllText(f.FullName))]);
        var after = string.Join(Environment.NewLine, dirInfo.GetDirectories().Select(dir => ReadAllTextFromFolder(dir.FullName)).ToArray());
        return string.Join(Environment.NewLine, code, after);
    }

    private static string BuildRuntimeGlobalsScript()
    {
        var type = typeof(IRobotNetGlobals);
        var sb = new StringBuilder();

        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
        {
            sb.AppendLine($@"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name}
{{
    get => ({CSharpSyntaxHelper.ToTypeString(field.FieldType)})robotnet[""{field.Name}""];
    set => robotnet[""{field.Name}""] = value;
}}");
        }

        foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (prop.GetIndexParameters().Length == 0)
            {
                var hasGetter = prop.GetGetMethod() != null;
                var hasSetter = prop.GetSetMethod() != null;
                if (hasGetter || hasSetter)
                {
                    var propBuilder = new StringBuilder();
                    propBuilder.AppendLine($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} {prop.Name}");
                    propBuilder.AppendLine("{");
                    if (hasGetter)
                        propBuilder.AppendLine($@"    get => ((Func<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""get_{prop.Name}""])();");

                    if (hasSetter)
                        propBuilder.AppendLine($@"    set => ((Action<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""set_{prop.Name}""])(value);");
                    propBuilder.AppendLine("}");
                    sb.AppendLine(propBuilder.ToString());
                }
            }
            else
            {
                // Handle indexers (properties with parameters)
                var indexParams = prop.GetIndexParameters();
                var paramDecl = string.Join(", ", indexParams.Select(p => $"{CSharpSyntaxHelper.ToTypeString(p.ParameterType)} {p.Name}"));
                var paramNames = string.Join(", ", indexParams.Select(p => p.Name));
                var getterDelegateType = $"Func<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>";
                var setterDelegateType = $"Action<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>";

                var propBuilder = new StringBuilder();
                propBuilder.AppendLine($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} this[{paramDecl}]");
                propBuilder.AppendLine("{");
                if (prop.GetGetMethod() != null)
                    propBuilder.AppendLine($@"    get => (({getterDelegateType})robotnet[""get_{prop.Name}_indexer""])({paramNames});");
                if (prop.GetSetMethod() != null)
                    propBuilder.AppendLine($@"    set => (({setterDelegateType})robotnet[""set_{prop.Name}_indexer""])({(string.IsNullOrEmpty(paramNames) ? "value" : paramNames + ", value")});");
                propBuilder.AppendLine("}");
                sb.AppendLine(propBuilder.ToString());
            }
        }
        foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
        {
            if (!method.IsSpecialName)
            {
                var parameters = method.GetParameters();
                var parametersStr = string.Join(", ", parameters.Select(ToParameterString));
                var args = string.Join(", ", parameters.Select(p => p.Name));
                var returnType = CSharpSyntaxHelper.ToTypeString(method.ReturnType);


                var paramTypes = string.Join(",", parameters.Select(p => p.ParameterType.FullName));
                var methodKey = $"{method.Name}({paramTypes})";

                var methodType = method.ReturnType == typeof(void)
                    ? (parameters.Length == 0 ? "Action" : $"Action<{string.Join(", ", parameters.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)))}>") 
                    : $"Func<{string.Join(", ", parameters.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(method.ReturnType)]))}>";

                sb.AppendLine($@"{returnType} {method.Name}({parametersStr}) => (({methodType})robotnet[""{methodKey}""]){(string.IsNullOrEmpty(args) ? "()" : $"({args})")};");
            }
        }

        var code = sb.ToString();

        return sb.ToString();
    }

    private static string BuildDeveloptGlobalsScript()
    {
        var sb = new StringBuilder();
        var glovalType = typeof(IRobotNetGlobals);

        var properties = glovalType.GetProperties();
        foreach (var property in properties)
        {
            var propStr = "";
            if (property.CanRead)
            {
                propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ get; {(property.CanWrite ? "set; " : "")}}}";
            }
            else if (property.CanWrite)
            {
                propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ set => throw new System.NotImplementedException(); }}";
            }
            if(string.IsNullOrEmpty(propStr)) continue;
            sb.AppendLine(propStr);
        }

        var fields = glovalType.GetFields();
        foreach (var field in fields)
        {
            sb.AppendLine($"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name};");
        }

        var methods = glovalType.GetMethods();
        foreach (var method in methods)
        {
            if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue;

            var notImplementedMethod = $"{CSharpSyntaxHelper.ToTypeString(method.ReturnType)} {method.Name}({string.Join(',', method.GetParameters().Select(ToParameterString))}) => throw new System.NotImplementedException();";
            sb.AppendLine(notImplementedMethod);
        }

        return sb.ToString();
    }

    private static string ToParameterString(ParameterInfo parameter)
    {
        var modifier = "";
        if (parameter.IsDefined(typeof(ParamArrayAttribute), false))
            modifier = "params ";
        else if (parameter.IsIn && parameter.ParameterType.IsByRef && !parameter.IsOut)
            modifier = "in ";
        else if (parameter.IsOut)
            modifier = "out ";
        else if (parameter.ParameterType.IsByRef)
            modifier = "ref ";

        var typeString = CSharpSyntaxHelper.ToTypeString(
            parameter.ParameterType.IsByRef
                ? parameter.ParameterType.GetElementType()!
                : parameter.ParameterType
        );

        var defaultValue = "";
        if (parameter.HasDefaultValue)
        {
            if (parameter.DefaultValue != null)
            {
                if (parameter.ParameterType.IsEnum)
                {
                    defaultValue = $" = {CSharpSyntaxHelper.ToTypeString(parameter.ParameterType)}.{parameter.DefaultValue}";
                }
                else if (parameter.DefaultValue is string)
                {
                    defaultValue = $" = \"{parameter.DefaultValue}\"";
                }
                else if (parameter.DefaultValue is bool b)
                {
                    defaultValue = $" = {b.ToString().ToLower()}";
                }
                else
                {
                    defaultValue = $" = {parameter.DefaultValue}";
                }
            }
            else
            {
                defaultValue = " = null";
            }
        }

        return $"{modifier}{typeString} {parameter.Name}{defaultValue}";
    }
    
}
