using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Barotrauma.LuaCs.Services;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
// ReSharper disable ConditionIsAlwaysTrueOrFalse

[assembly: InternalsVisibleTo("CompiledAssembly")]

namespace Barotrauma.LuaCs.Services;

/// <summary>
/// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution.
/// [IMPORTANT] Only supports 1 in-memory compiled assembly at a time. Use more instances if you need more.
/// [IMPORTANT] All file assemblies required for the compilation of syntax trees should be loaded first.
/// </summary>
public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
{
    // public
    public string FriendlyName { get; set; }
    // ReSharper disable MemberCanBePrivate.Global
    public Assembly CompiledAssembly { get; private set; }
    public byte[] CompiledAssemblyImage { get; private set; }
    // ReSharper restore MemberCanBePrivate.Global 
    // internal
    private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new();       // path-folder, resolver
    protected bool IsResolving;   //this is to avoid circular dependency lookup.
    private IAssemblyManagementService _assemblyManager;
    public bool IsTemplateMode { get; set; }
    public bool IsDisposed { get; private set; }
    
    public MemoryFileAssemblyContextLoader(IAssemblyManagementService assemblyManager) : base(isCollectible: true)
    {
        this._assemblyManager = assemblyManager;
        this.IsDisposed = false;
        base.Unloading += OnUnload;
    }
    

    /// <summary>
    /// Try to load the list of disk-file assemblies.
    /// </summary>
    /// <param name="assemblyFilePaths">Operation success or failure reason.</param>
    public AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable<string> assemblyFilePaths)
    {
        if (assemblyFilePaths is null)
            throw new ArgumentNullException(
                $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(LoadFromFiles)}() | The supplied filepath list is null.");
        
        foreach (string filepath in assemblyFilePaths)
        {
            // path verification
            if (filepath.IsNullOrWhiteSpace())
                continue;
            string sanitizedFilePath = System.IO.Path.GetFullPath(filepath.CleanUpPath());
            string directoryKey = System.IO.Path.GetDirectoryName(sanitizedFilePath);

            if (directoryKey is null)
                return AssemblyLoadingSuccessState.BadFilePath;

            // setup dep resolver if not available
            if (!_dependencyResolvers.ContainsKey(directoryKey) || _dependencyResolvers[directoryKey] is null)
            {
                _dependencyResolvers[directoryKey] = new AssemblyDependencyResolver(sanitizedFilePath); // supply the first assembly to be loaded
            }
            
            // try loading the assemblies
            try
            {
                LoadFromAssemblyPath(sanitizedFilePath);
            }
            // on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected.
            catch (ArgumentNullException ane)
            {
                ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ane.Message} | {ane.StackTrace}");
                return AssemblyLoadingSuccessState.BadFilePath;
            }
            catch (ArgumentException ae)
            {
                ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ae.Message} | {ae.StackTrace}");
                return AssemblyLoadingSuccessState.BadFilePath;
            }
            catch (FileLoadException fle)
            {
                ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fle.Message} | {fle.StackTrace}");
                return AssemblyLoadingSuccessState.CannotLoadFile;
            }
            catch (FileNotFoundException fnfe)
            {
                ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fnfe.Message} | {fnfe.StackTrace}");
                return AssemblyLoadingSuccessState.NoAssemblyFound;
            }
            catch (BadImageFormatException bife)
            {
                ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {bife.Message} | {bife.StackTrace}");
                return AssemblyLoadingSuccessState.InvalidAssembly;
            }
            catch (Exception e)
            {
#if SERVER
                LuaCsLogger.LogError($"Unable to load dependency assembly file at {filepath.CleanUpPath()} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
                LuaCsLogger.ShowErrorOverlay($"Unable to load dependency assembly file at {filepath} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
                return AssemblyLoadingSuccessState.ACLLoadFailure;
            }
        }

        return AssemblyLoadingSuccessState.Success;
    }


    /// <summary>
    /// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
    /// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
    /// AssemblyManager class. 
    /// </summary>
    /// <param name="assemblyName">Name of the assembly. Must be supplied for in-memory assemblies.</param>
    /// <param name="syntaxTrees">Syntax trees to compile into the assembly.</param>
    /// <param name="externMetadataReferences">Metadata to be used for compilation.
    /// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
    /// images not managed by the AssemblyManager class.</param>
    /// <param name="compilationOptions">CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
    /// <param name="compilationMessages">Will contain any diagnostic messages for compilation failure.</param>
    /// <param name="externFileAssemblyReferences">Additional assemblies located in the FileSystem to build metadata references from.
    /// Assemblies here will have duplicates by the same name that are currently loaded filtered out.</param>
    /// <returns>Success state of the operation.</returns>
    /// <exception cref="ArgumentNullException">Throws exception if any of the required arguments are null.</exception>
    public AssemblyLoadingSuccessState CompileAndLoadScriptAssembly(
        [NotNull] string assemblyName,
        [NotNull] IEnumerable<SyntaxTree> syntaxTrees,
        IEnumerable<MetadataReference> externMetadataReferences,
        [NotNull] CSharpCompilationOptions compilationOptions,
        out string compilationMessages,
        IEnumerable<Assembly> externFileAssemblyReferences = null)
    {
        compilationMessages = "";

        if (this.CompiledAssembly is not null)
        {
            return AssemblyLoadingSuccessState.AlreadyLoaded;
        }

        var externAssemblyRefs = externFileAssemblyReferences is not null ? externFileAssemblyReferences.ToImmutableList() : ImmutableList<Assembly>.Empty;
        var externAssemblyNames = externAssemblyRefs.Any() ? externAssemblyRefs
                .Where(a => a.FullName is not null)
                .Select(a => a.FullName).ToImmutableHashSet() 
            : ImmutableHashSet<string>.Empty;

        // verifications
        if (assemblyName.IsNullOrWhiteSpace())
            throw new ArgumentNullException(
                $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied assembly name is null!");

        if (syntaxTrees is null)
            throw new ArgumentNullException(
                $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied syntax tree is null!");
        
        // add external references
        List<MetadataReference> metadataReferences = new();
        if (externMetadataReferences is not null)
            metadataReferences.AddRange(externMetadataReferences);

        // build metadata refs from default where not an in-memory compiled assembly and not the same assembly as supplied.
        metadataReferences.AddRange(AssemblyLoadContext.Default.Assemblies
            .Where(a =>
            {
                if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
                    return false;
                if (a.FullName is null)
                    return true;
                return !externAssemblyNames.Contains(a.FullName);    // exclude duplicates
            })
            .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
            .Union(externAssemblyRefs   // add custom supplied assemblies
                .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
                .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
            ).ToList());

        ImmutableList<AssemblyManager.LoadedACL> loadedAcls = _assemblyManager.GetAllLoadedACLs().ToImmutableList();
        if (loadedAcls.Any())
        {
            // build metadata refs from ACL assemblies from files/disk.
            foreach (AssemblyManager.LoadedACL loadedAcl in loadedAcls)
            {
                if(loadedAcl?.Acl is null || loadedAcl.Acl.IsTemplateMode || loadedAcl.Acl.IsDisposed)
                    continue;
                metadataReferences.AddRange(loadedAcl.Acl.Assemblies
                    .Where(a =>
                    {
                        if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
                            return false;
                        if (a.FullName is null)
                            return true;
                        return !externAssemblyNames.Contains(a.FullName);    // exclude duplicates
                    })
                    .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
                    .Union(externAssemblyRefs   // add custom supplied assemblies
                        .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
                        .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
                    ).ToList());
            }
        
            // build metadata refs from in-memory images
            foreach (var loadedAcl in loadedAcls)
            {
                if (loadedAcl?.Acl?.CompiledAssemblyImage is null || loadedAcl.Acl.CompiledAssemblyImage.Length == 0)
                    continue;
                metadataReferences.Add(MetadataReference.CreateFromImage(loadedAcl.Acl.CompiledAssemblyImage));
            }
        }

        // Change inaccessible options to allow public access to restricted members
        var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
        topLevelBinderFlagsProperty?.SetValue(compilationOptions, (uint)1 << 22);
        
        // begin compilation 
        using var memoryCompilation = new MemoryStream();
        // compile, emit
        var result = CSharpCompilation.Create(assemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(memoryCompilation);
        // check for errors
        if (!result.Success)
        {
            IEnumerable<Diagnostic> failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error);
            foreach (Diagnostic diagnostic in failures)
            {
                compilationMessages += $"\n{diagnostic}";
            }

            return AssemblyLoadingSuccessState.InvalidAssembly;
        }

        // read compiled assembly from memory stream into an in-memory assembly & image
        memoryCompilation.Seek(0, SeekOrigin.Begin);   // reset
        try
        {
            CompiledAssembly = LoadFromStream(memoryCompilation);
            CompiledAssemblyImage = memoryCompilation.ToArray();
        }
        catch (Exception e)
        {
#if SERVER
            LuaCsLogger.LogError($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
            LuaCsLogger.ShowErrorOverlay($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
            return AssemblyLoadingSuccessState.CannotLoadFromStream;
        }

        return AssemblyLoadingSuccessState.Success;
    }
    
    [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")]
    protected override Assembly Load(AssemblyName assemblyName)
    {
        if (IsResolving)
            return null;    //circular resolution fast exit.

        try
        {
            IsResolving = true;

            // resolve self collection
            Assembly ass = this.Assemblies.FirstOrDefault(a =>
                a.FullName is not null && a.FullName.Equals(assemblyName.FullName), null);
            if (ass is not null)
                return ass;

            // resolve to local folders
            foreach (KeyValuePair<string,AssemblyDependencyResolver> pair in _dependencyResolvers)
            {
                var asspath = pair.Value.ResolveAssemblyToPath(assemblyName);
                if (asspath is null)
                    continue;
                ass = LoadFromAssemblyPath(asspath);
                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                if (ass is not null)
                    return ass;
            }

            //try resolve against other loaded alcs
            ImmutableList<AssemblyManager.LoadedACL> list;
            try
            {
                list = _assemblyManager.UnsafeGetAllLoadedACLs();
            }
            catch
            {
                list = ImmutableList<AssemblyManager.LoadedACL>.Empty;
            }

            if (!list.IsEmpty)
            {
                foreach (var loadedAcL in list)
                {
                    if (loadedAcL.Acl is null || loadedAcL.Acl.IsTemplateMode || loadedAcL.Acl.IsDisposed) 
                        continue;
                
                    try
                    {
                        ass = loadedAcL.Acl.LoadFromAssemblyName(assemblyName);
                        if (ass is not null)
                            return ass;
                    }
                    catch
                    {
                        // LoadFromAssemblyName throws, no need to propagate
                    }
                }
            }
            
            ass = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
            if (ass is not null)
                return ass;
        }
        finally
        {
            IsResolving = false;
        }
        
        return null;
    }
    

    private void OnUnload(AssemblyLoadContext alc)
    {
        CompiledAssembly = null;
        CompiledAssemblyImage = null;
        _dependencyResolvers.Clear();
        _assemblyManager = null;
        base.Unloading -= OnUnload;
        this.IsDisposed = true;
    }
}
