//
// MetadataWriter.cs
//
// Author:
//   Jb Evain (jbevain@gmail.com)
//
// (C) 2005 Jb Evain
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

namespace Mono.Cecil.Metadata {

	using System;
	using System.Collections;
	using System.IO;
	using System.Text;

	using Mono.Cecil;
	using Mono.Cecil.Binary;

	internal sealed class MetadataWriter : BaseMetadataVisitor {

		AssemblyDefinition m_assembly;
		MetadataRoot m_root;
		TargetRuntime m_runtime;
		ImageWriter m_imgWriter;
		MetadataTableWriter m_tableWriter;
		MemoryBinaryWriter m_binaryWriter;

		IDictionary m_stringCache;
		MemoryBinaryWriter m_stringWriter;

		IDictionary m_guidCache;
		MemoryBinaryWriter m_guidWriter;

		IDictionary m_usCache;
		MemoryBinaryWriter m_usWriter;

		IDictionary m_blobCache;
		MemoryBinaryWriter m_blobWriter;

		MemoryBinaryWriter m_tWriter;

		MemoryBinaryWriter m_cilWriter;

		MemoryBinaryWriter m_fieldDataWriter;
		MemoryBinaryWriter m_resWriter;

		uint m_mdStart, m_mdSize;
		uint m_resStart, m_resSize;
		uint m_snsStart, m_snsSize;
		uint m_debugHeaderStart;
		uint m_imporTableStart;

		uint m_entryPointToken;

		RVA m_cursor = new RVA (0x2050);

		public MemoryBinaryWriter CilWriter {
			get { return m_cilWriter; }
		}

		public MemoryBinaryWriter StringWriter {
			get { return m_stringWriter; }
		}

		public MemoryBinaryWriter GuidWriter {
			get { return m_guidWriter; }
		}

		public MemoryBinaryWriter UserStringWriter {
			get { return m_usWriter; }
		}

		public MemoryBinaryWriter BlobWriter {
			get { return m_blobWriter; }
		}

		public uint DebugHeaderPosition {
			get { return m_debugHeaderStart; }
		}

		public uint ImportTablePosition {
			get { return m_imporTableStart; }
		}

		public uint EntryPointToken {
			get { return m_entryPointToken; }
			set { m_entryPointToken = value; }
		}

		public TargetRuntime TargetRuntime {
			get { return m_runtime; }
		}

		public MetadataWriter (AssemblyDefinition asm, MetadataRoot root,
			AssemblyKind kind, TargetRuntime rt, BinaryWriter writer)
		{
			m_assembly = asm;
			m_root = root;
			m_runtime = rt;
			m_imgWriter = new ImageWriter (this, kind, writer);
			m_binaryWriter = m_imgWriter.GetTextWriter ();

			m_stringCache = new Hashtable ();
			m_stringWriter = new MemoryBinaryWriter (Encoding.UTF8);
			m_stringWriter.Write ((byte) 0);

			m_guidCache = new Hashtable ();
			m_guidWriter = new MemoryBinaryWriter ();

			m_usCache = new Hashtable ();
			m_usWriter = new MemoryBinaryWriter (Encoding.Unicode);
			m_usWriter.Write ((byte) 0);

			m_blobCache = new Hashtable (ByteArrayEqualityComparer.Instance, ByteArrayEqualityComparer.Instance);
			m_blobWriter = new MemoryBinaryWriter ();
			m_blobWriter.Write ((byte) 0);

			m_tWriter = new MemoryBinaryWriter ();
			m_tableWriter = new MetadataTableWriter (this, m_tWriter);

			m_cilWriter = new MemoryBinaryWriter ();

			m_fieldDataWriter = new MemoryBinaryWriter ();
			m_resWriter = new MemoryBinaryWriter ();
		}

		public MetadataRoot GetMetadataRoot ()
		{
			return m_root;
		}

		public ImageWriter GetImageWriter ()
		{
			return m_imgWriter;
		}

		public MemoryBinaryWriter GetWriter ()
		{
			return m_binaryWriter;
		}

		public MetadataTableWriter GetTableVisitor ()
		{
			return m_tableWriter;
		}

		public void AddData (int length)
		{
			m_cursor += new RVA ((uint) length);
		}

		public RVA GetDataCursor ()
		{
			return m_cursor;
		}

		public uint AddString (string str)
		{
			if (str == null || str.Length == 0)
				return 0;

			if (m_stringCache.Contains (str))
				return (uint) m_stringCache [str];

			uint pointer = (uint) m_stringWriter.BaseStream.Position;
			m_stringCache [str] = pointer;
			m_stringWriter.Write (Encoding.UTF8.GetBytes (str));
			m_stringWriter.Write ('\0');
			return pointer;
		}

		public uint AddBlob (byte [] data)
		{
			if (data == null || data.Length == 0)
				return 0;

			object cached = m_blobCache [data];
			if (cached != null)
				return (uint) cached;

			uint pointer = (uint) m_blobWriter.BaseStream.Position;
			m_blobCache [data] = pointer;
			Utilities.WriteCompressedInteger (m_blobWriter, data.Length);
			m_blobWriter.Write (data);
			return pointer;
		}

		public uint AddGuid (Guid g)
		{
			if (m_guidCache.Contains (g))
				return (uint) m_guidCache [g];

			uint pointer = (uint) m_guidWriter.BaseStream.Position;
			m_guidCache [g] = pointer;
			m_guidWriter.Write (g.ToByteArray ());
			return pointer + 1;
		}

		public uint AddUserString (string str)
		{
			if (str == null)
				return 0;

			if (m_usCache.Contains (str))
				return (uint) m_usCache [str];

			uint pointer = (uint) m_usWriter.BaseStream.Position;
			m_usCache [str] = pointer;
			byte [] us = Encoding.Unicode.GetBytes (str);
			Utilities.WriteCompressedInteger (m_usWriter, us.Length + 1);
			m_usWriter.Write (us);
			m_usWriter.Write ((byte) (RequiresSpecialHandling (us) ? 1 : 0));
			return pointer;
		}

		static bool RequiresSpecialHandling (byte [] chars)
		{
			for (int i = 0; i < chars.Length; i++) {
				byte c = chars [i];
				if ((i % 2) == 1)
					if (c != 0)
						return true;

				if (InRange (0x01, 0x08, c) ||
					InRange (0x0e, 0x1f, c) ||
					c == 0x27 ||
					c == 0x2d ||
					c == 0x7f) {

					return true;
				}
			}

			return false;
		}

		static bool InRange (int left, int right, int value)
		{
			return left <= value && value <= right;
		}

		void CreateStream (string name)
		{
			MetadataStream stream = new MetadataStream ();
			stream.Header.Name = name;
			stream.Heap = MetadataHeap.HeapFactory (stream);
			m_root.Streams.Add (stream);
		}

		void SetHeapSize (MetadataHeap heap, MemoryBinaryWriter data, byte flag)
		{
			if (data.BaseStream.Length > 65536) {
				m_root.Streams.TablesHeap.HeapSizes |= flag;
				heap.IndexSize = 4;
			} else
				heap.IndexSize = 2;
		}

		public uint AddResource (byte [] data)
		{
			uint offset = (uint) m_resWriter.BaseStream.Position;
			m_resWriter.Write (data.Length);
			m_resWriter.Write (data);
			m_resWriter.QuadAlign ();
			return offset;
		}

		public void AddFieldInitData (byte [] data)
		{
			m_fieldDataWriter.Write (data);
			m_fieldDataWriter.QuadAlign ();
		}

		uint GetStrongNameSignatureSize ()
		{
			if (m_assembly.Name.PublicKey != null) {
				// in fx 2.0 the key may be from 384 to 16384 bits
				// so we must calculate the signature size based on
				// the size of the public key (minus the 32 byte header)
				int size = m_assembly.Name.PublicKey.Length;
				if (size > 32)
					return (uint) (size - 32);
				// note: size == 16 for the ECMA "key" which is replaced
				// by the runtime with a 1024 bits key (128 bytes)
			}
			return 128; // default strongname signature size
		}

		public override void VisitMetadataRoot (MetadataRoot root)
		{
			WriteMemStream (m_cilWriter);
			WriteMemStream (m_fieldDataWriter);
			m_resStart = (uint) m_binaryWriter.BaseStream.Position;
			WriteMemStream (m_resWriter);
			m_resSize = (uint) (m_binaryWriter.BaseStream.Position - m_resStart);

			// for now, we only reserve the place for the strong name signature
			if ((m_assembly.Name.Flags & AssemblyFlags.PublicKey) > 0) {
				m_snsStart = (uint) m_binaryWriter.BaseStream.Position;
				m_snsSize = GetStrongNameSignatureSize ();
				m_binaryWriter.Write (new byte [m_snsSize]);
				m_binaryWriter.QuadAlign ();
			}

			// save place for debug header
			if (m_imgWriter.GetImage ().DebugHeader != null) {
				m_debugHeaderStart = (uint) m_binaryWriter.BaseStream.Position;
				m_binaryWriter.Write (new byte [m_imgWriter.GetImage ().DebugHeader.GetSize ()]);
				m_binaryWriter.QuadAlign ();
			}

			m_mdStart = (uint) m_binaryWriter.BaseStream.Position;

			if (m_stringWriter.BaseStream.Length > 1) {
				CreateStream (MetadataStream.Strings);
				SetHeapSize (root.Streams.StringsHeap, m_stringWriter, 0x01);
				m_stringWriter.QuadAlign ();
			}

			if (m_guidWriter.BaseStream.Length > 0) {
				CreateStream (MetadataStream.GUID);
				SetHeapSize (root.Streams.GuidHeap, m_guidWriter, 0x02);
			}

			if (m_blobWriter.BaseStream.Length > 1) {
				CreateStream (MetadataStream.Blob);
				SetHeapSize (root.Streams.BlobHeap, m_blobWriter, 0x04);
				m_blobWriter.QuadAlign ();
			}

			if (m_usWriter.BaseStream.Length > 2) {
				CreateStream (MetadataStream.UserStrings);
				m_usWriter.QuadAlign ();
			}

			m_root.Header.MajorVersion = 1;
			m_root.Header.MinorVersion = 1;

			switch (m_runtime) {
			case TargetRuntime.NET_1_0 :
				m_root.Header.Version = "v1.0.3705";
				break;
			case TargetRuntime.NET_1_1 :
				m_root.Header.Version = "v1.1.4322";
				break;
			case TargetRuntime.NET_2_0 :
				m_root.Header.Version = "v2.0.50727";
				break;
			case TargetRuntime.NET_4_0 :
				m_root.Header.Version = "v4.0.20506";
				break;
			}

			m_root.Streams.TablesHeap.Tables.Accept (m_tableWriter);

			if (m_tWriter.BaseStream.Length == 0)
				m_root.Streams.Remove (m_root.Streams.TablesHeap.GetStream ());
		}

		public override void VisitMetadataRootHeader (MetadataRoot.MetadataRootHeader header)
		{
			m_binaryWriter.Write (header.Signature);
			m_binaryWriter.Write (header.MajorVersion);
			m_binaryWriter.Write (header.MinorVersion);
			m_binaryWriter.Write (header.Reserved);
			m_binaryWriter.Write (header.Version.Length + 3 & (~3));
			m_binaryWriter.Write (Encoding.ASCII.GetBytes (header.Version));
			m_binaryWriter.QuadAlign ();
			m_binaryWriter.Write (header.Flags);
			m_binaryWriter.Write ((ushort) m_root.Streams.Count);
		}

		public override void VisitMetadataStreamCollection (MetadataStreamCollection streams)
		{
			foreach (MetadataStream stream in streams) {
				MetadataStream.MetadataStreamHeader header = stream.Header;

				header.Offset = (uint) (m_binaryWriter.BaseStream.Position);
				m_binaryWriter.Write (header.Offset);
				MemoryBinaryWriter container;
				string name = header.Name;
				uint size = 0;
				switch (header.Name) {
				case MetadataStream.Tables :
					container = m_tWriter;
					size += 24; // header
					break;
				case MetadataStream.Strings :
					name += "\0\0\0\0";
					container = m_stringWriter;
					break;
				case MetadataStream.GUID :
					container = m_guidWriter;
					break;
				case MetadataStream.Blob :
					container = m_blobWriter;
					break;
				case MetadataStream.UserStrings :
					container = m_usWriter;
					break;
				default :
					throw new MetadataFormatException ("Unknown stream kind");
				}

				size += (uint) (container.BaseStream.Length + 3 & (~3));
				m_binaryWriter.Write (size);
				m_binaryWriter.Write (Encoding.ASCII.GetBytes (name));
				m_binaryWriter.QuadAlign ();
			}
		}

		void WriteMemStream (MemoryBinaryWriter writer)
		{
			m_binaryWriter.Write (writer);
			m_binaryWriter.QuadAlign ();
		}

		void PatchStreamHeaderOffset (MetadataHeap heap)
		{
			long pos = m_binaryWriter.BaseStream.Position;
			m_binaryWriter.BaseStream.Position = heap.GetStream ().Header.Offset;
			m_binaryWriter.Write ((uint) (pos - m_mdStart));
			m_binaryWriter.BaseStream.Position = pos;
		}

		public override void VisitGuidHeap (GuidHeap heap)
		{
			PatchStreamHeaderOffset (heap);
			WriteMemStream (m_guidWriter);
		}

		public override void VisitStringsHeap (StringsHeap heap)
		{
			PatchStreamHeaderOffset (heap);
			WriteMemStream (m_stringWriter);
		}

		public override void VisitTablesHeap (TablesHeap heap)
		{
			PatchStreamHeaderOffset (heap);
			m_binaryWriter.Write (heap.Reserved);
			switch (m_runtime) {
			case TargetRuntime.NET_1_0 :
			case TargetRuntime.NET_1_1 :
				heap.MajorVersion = 1;
				heap.MinorVersion = 0;
				break;
			case TargetRuntime.NET_2_0 :
			case TargetRuntime.NET_4_0 :
				heap.MajorVersion = 2;
				heap.MinorVersion = 0;
				break;
			}
			m_binaryWriter.Write (heap.MajorVersion);
			m_binaryWriter.Write (heap.MinorVersion);
			m_binaryWriter.Write (heap.HeapSizes);
			m_binaryWriter.Write (heap.Reserved2);
			m_binaryWriter.Write (heap.Valid);
			m_binaryWriter.Write (heap.Sorted);
			WriteMemStream (m_tWriter);
		}

		public override void VisitBlobHeap (BlobHeap heap)
		{
			PatchStreamHeaderOffset (heap);
			WriteMemStream (m_blobWriter);
		}

		public override void VisitUserStringsHeap (UserStringsHeap heap)
		{
			PatchStreamHeaderOffset (heap);
			WriteMemStream (m_usWriter);
		}

		void PatchHeader ()
		{
			Image img = m_imgWriter.GetImage ();

			img.CLIHeader.EntryPointToken = m_entryPointToken;

			if ((m_assembly.Name.Flags & AssemblyFlags.PublicKey) == 0)
				img.CLIHeader.Flags &= ~RuntimeImage.StrongNameSigned;

			if (m_mdSize > 0)
				img.CLIHeader.Metadata = new DataDirectory (
					img.TextSection.VirtualAddress + m_mdStart, m_imporTableStart - m_mdStart);

			if (m_resSize > 0)
				img.CLIHeader.Resources = new DataDirectory (
					img.TextSection.VirtualAddress + m_resStart, m_resSize);

			if (m_snsStart > 0)
				img.CLIHeader.StrongNameSignature = new DataDirectory (
					img.TextSection.VirtualAddress + m_snsStart, m_snsSize);

			if (m_debugHeaderStart > 0)
				img.PEOptionalHeader.DataDirectories.Debug = new DataDirectory (
					img.TextSection.VirtualAddress + m_debugHeaderStart, 0x1c);
		}

		public override void TerminateMetadataRoot (MetadataRoot root)
		{
			m_mdSize = (uint) (m_binaryWriter.BaseStream.Position - m_mdStart);
			m_imporTableStart = (uint) m_binaryWriter.BaseStream.Position;
			m_binaryWriter.Write (new byte [0x60]); // imports
			m_imgWriter.Initialize ();
			PatchHeader ();
			root.GetImage ().Accept (m_imgWriter);
		}
	}
}
