// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;

namespace ILCompiler.Reflection.ReadyToRun
{
    public class PEExportTable
    {
        private readonly Dictionary<string, int> _namedExportRva;
        private readonly Dictionary<int, int> _ordinalRva;

        private PEExportTable(PEReader peReader)
        {
            _namedExportRva = new Dictionary<string, int>();
            _ordinalRva = new Dictionary<int, int>();

            DirectoryEntry exportTable = peReader.PEHeaders.PEHeader.ExportTableDirectory;
            if ((exportTable.Size == 0) || (exportTable.RelativeVirtualAddress == 0))
                return;

            PEMemoryBlock peImage = peReader.GetEntireImage();
            BlobReader exportTableHeader = peImage.GetReader(peReader.GetOffset(exportTable.RelativeVirtualAddress), exportTable.Size);
            if (exportTableHeader.Length == 0)
            {
                return;
            }

            // +0x00: reserved
            exportTableHeader.ReadUInt32();
            // +0x04: TODO: time/date stamp
            exportTableHeader.ReadUInt32();
            // +0x08: major version
            exportTableHeader.ReadUInt16();
            // +0x0A: minor version
            exportTableHeader.ReadUInt16();
            // +0x0C: DLL name RVA
            exportTableHeader.ReadUInt32();
            // +0x10: ordinal base
            int minOrdinal = exportTableHeader.ReadInt32();
            // +0x14: number of entries in the address table
            int addressEntryCount = exportTableHeader.ReadInt32();
            // +0x18: number of name pointers
            int namePointerCount = exportTableHeader.ReadInt32();
            // +0x1C: export address table RVA
            int addressTableRVA = exportTableHeader.ReadInt32();
            // +0x20: name pointer RVA
            int namePointerRVA = exportTableHeader.ReadInt32();
            // +0x24: ordinal table RVA
            int ordinalTableRVA = exportTableHeader.ReadInt32();

            int[] addressTable = new int[addressEntryCount];
            BlobReader addressTableReader = peImage.GetReader(peReader.GetOffset(addressTableRVA), sizeof(int) * addressEntryCount);
            for (int entryIndex = 0; entryIndex < addressEntryCount; entryIndex++)
            {
                addressTable[entryIndex] = addressTableReader.ReadInt32();
            }

            ushort[] ordinalTable = new ushort[namePointerCount];
            BlobReader ordinalTableReader = peImage.GetReader(peReader.GetOffset(ordinalTableRVA), sizeof(ushort) * namePointerCount);
            for (int entryIndex = 0; entryIndex < namePointerCount; entryIndex++)
            {
                ushort ordinalIndex = ordinalTableReader.ReadUInt16();
                ordinalTable[entryIndex] = ordinalIndex;
                _ordinalRva.Add(entryIndex + minOrdinal, addressTable[ordinalIndex]);
            }

            BlobReader namePointerReader = peImage.GetReader(peReader.GetOffset(namePointerRVA), sizeof(int) * namePointerCount);
            for (int entryIndex = 0; entryIndex < namePointerCount; entryIndex++)
            {
                int nameRVA = namePointerReader.ReadInt32();
                if (nameRVA != 0)
                {
                    int nameOffset = peReader.GetOffset(nameRVA);
                    BlobReader nameReader = peImage.GetReader(nameOffset, peImage.Length - nameOffset);
                    StringBuilder nameBuilder = new StringBuilder();
                    for (byte ascii; (ascii = nameReader.ReadByte()) != 0;)
                    {
                        nameBuilder.Append((char)ascii);
                    }
                    _namedExportRva.Add(nameBuilder.ToString(), addressTable[ordinalTable[entryIndex]]);
                }
            }
        }

        public static PEExportTable Parse(PEReader peReader)
        {
            return new PEExportTable(peReader);
        }

        public bool TryGetValue(string exportName, out int rva) => _namedExportRva.TryGetValue(exportName, out rva);
        public bool TryGetValue(int ordinal, out int rva) => _ordinalRva.TryGetValue(ordinal, out rva);
    }

    public static class PEReaderExtensions
    {
        /// <summary>
        /// Get the index in the image byte array corresponding to the RVA
        /// </summary>
        /// <param name="reader">PE reader representing the executable image to parse</param>
        /// <param name="rva">The relative virtual address</param>
        public static int GetOffset(this PEReader reader, int rva)
        {
            int index = reader.PEHeaders.GetContainingSectionIndex(rva);
            if (index == -1)
            {
                throw new BadImageFormatException("Failed to convert invalid RVA to offset: " + rva);
            }
            SectionHeader containingSection = reader.PEHeaders.SectionHeaders[index];
            return rva - containingSection.VirtualAddress + containingSection.PointerToRawData;
        }

        /// <summary>
        /// Parse export table directory for a given PE reader.
        /// </summary>
        /// <param name="reader">PE reader representing the executable image to parse</param>
        public static PEExportTable GetExportTable(this PEReader reader)
        {
            return PEExportTable.Parse(reader);
        }

        /// <summary>
        /// Check whether the file is a ReadyToRun image and returns the RVA of its ReadyToRun header if positive.
        /// </summary>
        /// <param name="reader">PEReader representing the executable to check for the presence of ReadyToRun header</param>
        /// <param name="rva">RVA of the ReadyToRun header if available, 0 when not</param>
        /// <returns>true when the PEReader represents a ReadyToRun image, false otherwise</returns>
        public static bool TryGetReadyToRunHeader(this PEReader reader, out int rva)
        {
            return reader.GetExportTable().TryGetValue("RTR_HEADER", out rva);
        }
    }
}
