using System;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Pdb;

namespace zzzUnity.Burst.CodeGen
{
    /// <summary>
    /// Provides an assembly resolver with deferred loading and a custom metadata resolver.
    /// </summary>
    /// <remarks>
    /// This class is not thread safe. It needs to be protected outside.
    /// </remarks>
#if BURST_COMPILER_SHARED
    public
#else
    internal
#endif
    class AssemblyResolver : BaseAssemblyResolver
    {
        private readonly ReadingMode _readingMode;

        public AssemblyResolver(ReadingMode readingMode = ReadingMode.Deferred)
        {
            _readingMode = readingMode;

            // We remove all setup by Cecil by default (it adds '.' and 'bin')
            ClearSearchDirectories();

            LoadDebugSymbols = false;       // We don't bother loading the symbols by default now, since we use SRM to handle symbols in a more thread safe manner
                                            // this is to maintain compatibility with the patch-assemblies path (see BclApp.cs), used by dots runtime
        }

        public bool LoadDebugSymbols { get; set; }

        protected void ClearSearchDirectories()
        {
            foreach (var dir in GetSearchDirectories())
            {
                RemoveSearchDirectory(dir);
            }
        }

        public AssemblyDefinition LoadFromStream(Stream peStream, Stream pdbStream = null, ISymbolReaderProvider customSymbolReader=null)
        {
            peStream.Position = 0;
            if (pdbStream != null)
            {
                pdbStream.Position = 0;
            }
            var readerParameters = CreateReaderParameters();
            if (customSymbolReader != null)
            {
                readerParameters.ReadSymbols = true;
                readerParameters.SymbolReaderProvider = customSymbolReader;
            }
            readerParameters.ReadingMode = _readingMode;
            try
            {
                readerParameters.SymbolStream = pdbStream;
                return AssemblyDefinition.ReadAssembly(peStream, readerParameters);
            }
            catch
            {
                readerParameters.ReadSymbols = false;
                readerParameters.SymbolStream = null;
                peStream.Position = 0;
                if (pdbStream != null)
                {
                    pdbStream.Position = 0;
                }
                return AssemblyDefinition.ReadAssembly(peStream, readerParameters);
            }
        }

        public override AssemblyDefinition Resolve(AssemblyNameReference name)
        {
            var readerParameters = CreateReaderParameters();
            readerParameters.ReadingMode = _readingMode;
            AssemblyDefinition assemblyDefinition;

            try
            {
                assemblyDefinition = Resolve(name, readerParameters);
            }
            catch (Exception ex)
            {
                if (readerParameters.ReadSymbols == true)
                {
                    // Attempt to load without symbols
                    readerParameters.ReadSymbols = false;
                    assemblyDefinition = Resolve(name, readerParameters);
                }
                else
                {
                    throw new AssemblyResolutionException(
                        name,
                        new Exception($"Failed to resolve assembly '{name}' in directories: {string.Join(Environment.NewLine, GetSearchDirectories())}", ex));
                }
            }

            return assemblyDefinition;
        }

        public bool TryResolve(AssemblyNameReference name, out AssemblyDefinition assembly)
        {
            try
            {
                assembly = Resolve(name);
                return true;
            }
            catch (AssemblyResolutionException)
            {
                assembly = null;
                return false;
            }
        }

        public new void AddSearchDirectory(string directory)
        {
            if (!GetSearchDirectories().Contains(directory))
            {
                base.AddSearchDirectory(directory);
            }
        }

        private ReaderParameters CreateReaderParameters()
        {
            var readerParams = new ReaderParameters
            {
                InMemory = true,
                AssemblyResolver = this,
                MetadataResolver =  new CustomMetadataResolver(this),
                ReadSymbols = LoadDebugSymbols     // We no longer use cecil to read symbol information, prefering SRM thread safe methods, so I`m being explicit here in case the default changes
            };

            if (LoadDebugSymbols)
            {
                readerParams.SymbolReaderProvider = new CustomSymbolReaderProvider(null);
            }

            return readerParams;
        }

        internal static string NormalizeFilePath(string path)
        {
            try
            {
                return Path.GetFullPath(new Uri(path).LocalPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
            }
            catch (Exception ex)
            {
                throw new Exception($"Could not normalize file path: {path}", ex);
            }
        }

        private class CustomMetadataResolver : MetadataResolver
        {
            public CustomMetadataResolver(IAssemblyResolver assemblyResolver) : base(assemblyResolver)
            {
            }

            public override MethodDefinition Resolve(MethodReference method)
            {
                if (method is MethodDefinition methodDef)
                {
                    return methodDef;
                }

                if (method.GetElementMethod() is MethodDefinition methodDef2)
                {
                    return methodDef2;
                }

                return base.Resolve(method);
            }
        }

        /// <summary>
        /// Custom implementation of <see cref="ISymbolReaderProvider"/> to:
        /// - to load pdb/mdb through a MemoryStream to avoid locking the file on the disk
        /// - catch any exceptions while loading the symbols and report them back
        /// </summary>
        private class CustomSymbolReaderProvider : ISymbolReaderProvider
        {
            private readonly Action<string, Exception> _logException;

            public CustomSymbolReaderProvider(Action<string, Exception> logException)
            {
                _logException = logException;
            }

            public ISymbolReader GetSymbolReader(ModuleDefinition module, string fileName)
            {
                if (string.IsNullOrWhiteSpace(fileName)) return null;

                string pdbFileName = fileName;
                try
                {
                    fileName = NormalizeFilePath(fileName);
                    pdbFileName = GetPdbFileName(fileName);

                    if (File.Exists(pdbFileName))
                    {
                        var pdbStream = ReadToMemoryStream(pdbFileName);
                        if (IsPortablePdb(pdbStream))
                            return new SafeDebugReaderProvider(new PortablePdbReaderProvider().GetSymbolReader(module, pdbStream));

                        return new SafeDebugReaderProvider(new NativePdbReaderProvider().GetSymbolReader(module, pdbStream));
                    }
                }
                catch (Exception ex) when (_logException != null)
                {
                    _logException?.Invoke($"Unable to load symbol `{pdbFileName}`", ex);
                    return null;
                }
                return null;
            }

            private static MemoryStream ReadToMemoryStream(string filename)
            {
                return new MemoryStream(File.ReadAllBytes(filename));
            }

            public ISymbolReader GetSymbolReader(ModuleDefinition module, Stream symbolStream)
            {
                throw new NotSupportedException();
            }

            private static string GetPdbFileName(string assemblyFileName)
            {
                return Path.ChangeExtension(assemblyFileName, ".pdb");
            }

            private static bool IsPortablePdb(Stream stream)
            {
                if (stream.Length < 4L)
                    return false;
                long position = stream.Position;
                try
                {
                    return (int)new BinaryReader(stream).ReadUInt32() == 1112167234;
                }
                finally
                {
                    stream.Position = position;
                }
            }

            /// <summary>
            /// This class is a wrapper around <see cref="ISymbolReader"/> to protect
            /// against failure while trying to read debug information in Mono.Cecil
            /// </summary>
            private class SafeDebugReaderProvider : ISymbolReader
            {
                private readonly ISymbolReader _reader;

                public SafeDebugReaderProvider(ISymbolReader reader)
                {
                    _reader = reader;
                }


                public void Dispose()
                {
                    try
                    {
                        _reader.Dispose();
                    }
                    catch
                    {
                        // ignored
                    }
                }

                public ISymbolWriterProvider GetWriterProvider()
                {
                    // We are not protecting here as we are not suppose to write to PDBs
                    return _reader.GetWriterProvider();
                }

                public bool ProcessDebugHeader(ImageDebugHeader header)
                {
                    try
                    {
                        return _reader.ProcessDebugHeader(header);
                    }
                    catch
                    {
                        // ignored
                    }

                    return false;
                }

                public MethodDebugInformation Read(MethodDefinition method)
                {
                    try
                    {
                        return _reader.Read(method);
                    }
                    catch
                    {
                        // ignored
                    }
                    return null;
                }
            }
        }
    }
}