using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.PortableExecutable;
using Microsoft.DiaSymReader;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.Metadata;
using System.IO;
using System.Reflection;

namespace Semmle.Extraction.PDB
{
    /// <summary>
    /// A PDB reader using Microsoft.DiaSymReader.Native.
    /// This is an unmanaged Windows DLL, which therefore only works on Windows.
    /// </summary>
    class NativePdbReader : IPdb
    {
        sealed class Document : ISourceFile
        {
            readonly ISymUnmanagedDocument document;

            public Document(ISymUnmanagedDocument doc)
            {
                document = doc;
                contents = new Lazy<string>(() =>
                {
                    bool isEmbedded;
                    if (document.HasEmbeddedSource(out isEmbedded) == 0 && isEmbedded)
                    {
                        var rawContents = document.GetEmbeddedSource().ToArray();
                        return System.Text.Encoding.Default.GetString(rawContents);
                    }
                    else
                    {
                        return File.Exists(Path) ? File.ReadAllText(Path) : null;
                    }
                });
            }

            public override bool Equals(object obj)
            {
                var otherDoc = obj as Document;
                return otherDoc != null && Path.Equals(otherDoc.Path);
            }

            public override int GetHashCode() => Path.GetHashCode();

            public string Path => document.GetName();

            public override string ToString() => Path;

            readonly Lazy<string> contents;

            public string Contents => contents.Value;
        }

        public IEnumerable<ISourceFile> SourceFiles => reader.GetDocuments().Select(d => new Document(d));

        public IMethod GetMethod(MethodDebugInformationHandle h)
        {
            int methodToken = MetadataTokens.GetToken(h.ToDefinitionHandle());
            var method = reader.GetMethod(methodToken);
            if (method != null)
            {
                int count;
                if (method.GetSequencePointCount(out count) != 0 || count == 0)
                    return null;

                var s = method.GetSequencePoints().
                    Where(sp => !sp.IsHidden).
                    Select(sp => new SequencePoint(sp.Offset, new Location(new Document(sp.Document), sp.StartLine, sp.StartColumn, sp.EndLine, sp.EndColumn))).
                    ToArray();

                return s.Any() ? new Method { SequencePoints = s } : null;
            }
            return null;
        }

        NativePdbReader(string path)
        {
            pdbStream = new FileStream(path, FileMode.Open);
            var metadataProvider = new MdProvider();
            reader = SymUnmanagedReaderFactory.CreateReader<ISymUnmanagedReader5>(pdbStream, metadataProvider);
        }

        readonly ISymUnmanagedReader5 reader;
        readonly FileStream pdbStream;

        public static NativePdbReader CreateFromAssembly(string assemblyPath, PEReader peReader)
        {
            // The Native PDB reader uses an unmanaged Windows DLL
            // so only works on Windows.
            if (!Semmle.Util.Win32.IsWindows())
                return null;

            var debugDirectory = peReader.ReadDebugDirectory();

            foreach (var path in debugDirectory.
                Where(d => d.Type == DebugDirectoryEntryType.CodeView).
                Select(peReader.ReadCodeViewDebugDirectoryData).
                Select(cv => cv.Path).
                Where(path => File.Exists(path)))
            {
                return new NativePdbReader(path);
            }

            return null;
        }

        public void Dispose()
        {
            pdbStream.Dispose();
        }
    }

    /// <summary>
    /// This is not used but is seemingly needed in order to use DiaSymReader.
    /// </summary>
    class MdProvider : ISymReaderMetadataProvider
    {
        public MdProvider()
        {
        }

        public object GetMetadataImport() => null;

        public unsafe bool TryGetStandaloneSignature(int standaloneSignatureToken, out byte* signature, out int length) =>
            throw new NotImplementedException();

        public bool TryGetTypeDefinitionInfo(int typeDefinitionToken, out string namespaceName, out string typeName, out TypeAttributes attributes, out int baseTypeToken) =>
            throw new NotImplementedException();

        public bool TryGetTypeDefinitionInfo(int typeDefinitionToken, out string namespaceName, out string typeName, out TypeAttributes attributes) =>
            throw new NotImplementedException();

        public bool TryGetTypeReferenceInfo(int typeReferenceToken, out string namespaceName, out string typeName, out int resolutionScopeToken) =>
            throw new NotImplementedException();

        public bool TryGetTypeReferenceInfo(int typeReferenceToken, out string namespaceName, out string typeName) =>
            throw new NotImplementedException();
    }
}
