// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team
// 
// 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.

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

using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Solution;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
using ICSharpCode.Decompiler.Util;

using SRM = System.Reflection.Metadata;

namespace ICSharpCode.ILSpy
{
	public class LanguageVersion
	{
		public string Version { get; }
		public string DisplayName { get; }

		public LanguageVersion(string version, string name = null)
		{
			this.Version = version ?? "";
			this.DisplayName = name ?? version.ToString();
		}

		public override string ToString()
		{
			return $"[LanguageVersion DisplayName={DisplayName}, Version={Version}]";
		}
	}

	/// <summary>
	/// Base class for language-specific decompiler implementations.
	/// </summary>
	/// <remarks>
	/// Implementations of this class must be thread-safe.
	/// </remarks>
	public abstract class Language
	{
		/// <summary>
		/// Gets the name of the language (as shown in the UI)
		/// </summary>
		public abstract string Name { get; }

		/// <summary>
		/// Gets the file extension used by source code files in this language.
		/// </summary>
		public abstract string FileExtension { get; }

		public virtual string ProjectFileExtension {
			get { return null; }
		}

		public virtual IReadOnlyList<LanguageVersion> LanguageVersions {
			get { return EmptyList<LanguageVersion>.Instance; }
		}

		public bool HasLanguageVersions => LanguageVersions.Count > 0;

		/// <summary>
		/// Gets the syntax highlighting used for this language.
		/// </summary>
		public virtual ICSharpCode.AvalonEdit.Highlighting.IHighlightingDefinition SyntaxHighlighting {
			get {
				return ICSharpCode.AvalonEdit.Highlighting.HighlightingManager.Instance.GetDefinitionByExtension(this.FileExtension);
			}
		}

		public virtual TextView.IBracketSearcher BracketSearcher {
			get {
				return TextView.DefaultBracketSearcher.DefaultInstance;
			}
		}

		public virtual void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options)
		{
			WriteCommentLine(output, TypeToString(method.DeclaringTypeDefinition, includeNamespace: true) + "." + method.Name);
		}

		public virtual void DecompileProperty(IProperty property, ITextOutput output, DecompilationOptions options)
		{
			WriteCommentLine(output, TypeToString(property.DeclaringTypeDefinition, includeNamespace: true) + "." + property.Name);
		}

		public virtual void DecompileField(IField field, ITextOutput output, DecompilationOptions options)
		{
			WriteCommentLine(output, TypeToString(field.DeclaringTypeDefinition, includeNamespace: true) + "." + field.Name);
		}

		public virtual void DecompileEvent(IEvent @event, ITextOutput output, DecompilationOptions options)
		{
			WriteCommentLine(output, TypeToString(@event.DeclaringTypeDefinition, includeNamespace: true) + "." + @event.Name);
		}

		public virtual void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options)
		{
			WriteCommentLine(output, TypeToString(type, includeNamespace: true));
		}

		public virtual void DecompileNamespace(string nameSpace, IEnumerable<ITypeDefinition> types, ITextOutput output, DecompilationOptions options)
		{
			WriteCommentLine(output, nameSpace);
		}

		public virtual ProjectId DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options)
		{
			WriteCommentLine(output, assembly.FileName);
			var asm = assembly.GetPEFileOrNull();
			if (asm == null)
				return null;
			if (options.FullDecompilation && options.SaveAsProjectDirectory != null)
			{
				throw new NotSupportedException($"Language '{Name}' does not support exporting assemblies as projects!");
			}
			var metadata = asm.Metadata;
			if (metadata.IsAssembly)
			{
				var name = metadata.GetAssemblyDefinition();
				if ((name.Flags & System.Reflection.AssemblyFlags.WindowsRuntime) != 0)
				{
					WriteCommentLine(output, metadata.GetString(name.Name) + " [WinRT]");
				}
				else if (metadata.TryGetFullAssemblyName(out string assemblyName))
				{
					WriteCommentLine(output, assemblyName);
				}
				else
				{
					WriteCommentLine(output, "ERR: Could not read assembly name");
				}
			}
			else
			{
				WriteCommentLine(output, metadata.GetString(metadata.GetModuleDefinition().Name));
			}
			return null;
		}

		public virtual void WriteCommentLine(ITextOutput output, string comment)
		{
			output.WriteLine("// " + comment);
		}

		#region TypeToString
		/// <summary>
		/// Converts a type definition, reference or specification into a string. This method is used by tree nodes and search results.
		/// </summary>
		public virtual string TypeToString(IType type, bool includeNamespace)
		{
			var visitor = new TypeToStringVisitor(includeNamespace);
			type.AcceptVisitor(visitor);
			return visitor.ToString();
		}

		class TypeToStringVisitor : TypeVisitor
		{
			readonly bool includeNamespace;
			readonly StringBuilder builder;

			public override string ToString()
			{
				return builder.ToString();
			}

			public TypeToStringVisitor(bool includeNamespace)
			{
				this.includeNamespace = includeNamespace;
				this.builder = new StringBuilder();
			}

			public override IType VisitArrayType(ArrayType type)
			{
				base.VisitArrayType(type);
				builder.Append('[');
				builder.Append(',', type.Dimensions - 1);
				builder.Append(']');
				return type;
			}

			public override IType VisitByReferenceType(ByReferenceType type)
			{
				base.VisitByReferenceType(type);
				builder.Append('&');
				return type;
			}

			public override IType VisitModOpt(ModifiedType type)
			{
				type.ElementType.AcceptVisitor(this);
				builder.Append(" modopt(");
				type.Modifier.AcceptVisitor(this);
				builder.Append(")");
				return type;
			}

			public override IType VisitModReq(ModifiedType type)
			{
				type.ElementType.AcceptVisitor(this);
				builder.Append(" modreq(");
				type.Modifier.AcceptVisitor(this);
				builder.Append(")");
				return type;
			}

			public override IType VisitPointerType(PointerType type)
			{
				base.VisitPointerType(type);
				builder.Append('*');
				return type;
			}

			public override IType VisitTypeParameter(ITypeParameter type)
			{
				base.VisitTypeParameter(type);
				EscapeName(builder, type.Name);
				return type;
			}

			public override IType VisitParameterizedType(ParameterizedType type)
			{
				type.GenericType.AcceptVisitor(this);
				builder.Append('<');
				for (int i = 0; i < type.TypeArguments.Count; i++)
				{
					if (i > 0)
						builder.Append(',');
					type.TypeArguments[i].AcceptVisitor(this);
				}
				builder.Append('>');
				return type;
			}

			public override IType VisitTupleType(TupleType type)
			{
				type.UnderlyingType.AcceptVisitor(this);
				return type;
			}

			public override IType VisitFunctionPointerType(FunctionPointerType type)
			{
				builder.Append("method ");
				if (type.CallingConvention != SignatureCallingConvention.Default)
				{
					builder.Append(type.CallingConvention.ToILSyntax());
					builder.Append(' ');
				}
				type.ReturnType.AcceptVisitor(this);
				builder.Append(" *(");
				bool first = true;
				foreach (var p in type.ParameterTypes)
				{
					if (first)
						first = false;
					else
						builder.Append(", ");

					p.AcceptVisitor(this);
				}
				builder.Append(')');
				return type;
			}

			public override IType VisitOtherType(IType type)
			{
				WriteType(type);
				return type;
			}

			private void WriteType(IType type)
			{
				if (includeNamespace)
					EscapeName(builder, type.FullName);
				else
					EscapeName(builder, type.Name);
				if (type.TypeParameterCount > 0)
				{
					builder.Append('`');
					builder.Append(type.TypeParameterCount);
				}
			}

			public override IType VisitTypeDefinition(ITypeDefinition type)
			{
				switch (type.KnownTypeCode)
				{
					case KnownTypeCode.Object:
						builder.Append("object");
						break;
					case KnownTypeCode.Boolean:
						builder.Append("bool");
						break;
					case KnownTypeCode.Char:
						builder.Append("char");
						break;
					case KnownTypeCode.SByte:
						builder.Append("int8");
						break;
					case KnownTypeCode.Byte:
						builder.Append("uint8");
						break;
					case KnownTypeCode.Int16:
						builder.Append("int16");
						break;
					case KnownTypeCode.UInt16:
						builder.Append("uint16");
						break;
					case KnownTypeCode.Int32:
						builder.Append("int32");
						break;
					case KnownTypeCode.UInt32:
						builder.Append("uint32");
						break;
					case KnownTypeCode.Int64:
						builder.Append("int64");
						break;
					case KnownTypeCode.UInt64:
						builder.Append("uint64");
						break;
					case KnownTypeCode.Single:
						builder.Append("float32");
						break;
					case KnownTypeCode.Double:
						builder.Append("float64");
						break;
					case KnownTypeCode.String:
						builder.Append("string");
						break;
					case KnownTypeCode.Void:
						builder.Append("void");
						break;
					case KnownTypeCode.IntPtr:
						builder.Append("native int");
						break;
					case KnownTypeCode.UIntPtr:
						builder.Append("native uint");
						break;
					case KnownTypeCode.TypedReference:
						builder.Append("typedref");
						break;
					default:
						WriteType(type);
						break;
				}
				return type;
			}
		}
		#endregion

		/// <summary>
		/// Converts a member signature to a string.
		/// This is used for displaying the tooltip on a member reference.
		/// </summary>
		public virtual string GetTooltip(IEntity entity)
		{
			return GetDisplayName(entity, true, true, true);
		}

		/// <summary>
		/// Converts a member signature to a string.
		/// This is used for displaying the tooltip on a member reference.
		/// </summary>
		public virtual RichText GetRichTextTooltip(IEntity entity)
		{
			return GetTooltip(entity);
		}

		public virtual string FieldToString(IField field, bool includeDeclaringTypeName, bool includeNamespace, bool includeNamespaceOfDeclaringTypeName)
		{
			if (field == null)
				throw new ArgumentNullException(nameof(field));
			return GetDisplayName(field, includeDeclaringTypeName, includeNamespace, includeNamespaceOfDeclaringTypeName) + " : " + TypeToString(field.ReturnType, includeNamespace);
		}

		public virtual string PropertyToString(IProperty property, bool includeDeclaringTypeName, bool includeNamespace, bool includeNamespaceOfDeclaringTypeName)
		{
			if (property == null)
				throw new ArgumentNullException(nameof(property));
			return GetDisplayName(property, includeDeclaringTypeName, includeNamespace, includeNamespaceOfDeclaringTypeName) + " : " + TypeToString(property.ReturnType, includeNamespace);
		}

		public virtual string MethodToString(IMethod method, bool includeDeclaringTypeName, bool includeNamespace, bool includeNamespaceOfDeclaringTypeName)
		{
			if (method == null)
				throw new ArgumentNullException(nameof(method));

			int i = 0;
			var buffer = new StringBuilder();
			buffer.Append(GetDisplayName(method, includeDeclaringTypeName, includeNamespace, includeNamespaceOfDeclaringTypeName));
			var typeParameters = method.TypeParameters;
			if (typeParameters.Count > 0)
			{
				buffer.Append("``");
				buffer.Append(typeParameters.Count);
				buffer.Append('<');
				foreach (var tp in typeParameters)
				{
					if (i > 0)
						buffer.Append(", ");
					buffer.Append(tp.Name);
					i++;
				}
				buffer.Append('>');
			}
			buffer.Append('(');

			i = 0;
			var parameters = method.Parameters;
			foreach (var param in parameters)
			{
				if (i > 0)
					buffer.Append(", ");
				buffer.Append(TypeToString(param.Type, includeNamespace));
				i++;
			}
			buffer.Append(')');
			if (!method.IsConstructor)
			{
				buffer.Append(" : ");
				buffer.Append(TypeToString(method.ReturnType, includeNamespace));
			}
			return buffer.ToString();
		}

		public virtual string EventToString(IEvent @event, bool includeDeclaringTypeName, bool includeNamespace, bool includeNamespaceOfDeclaringTypeName)
		{
			if (@event == null)
				throw new ArgumentNullException(nameof(@event));
			var buffer = new StringBuilder();
			buffer.Append(GetDisplayName(@event, includeDeclaringTypeName, includeNamespace, includeNamespaceOfDeclaringTypeName));
			buffer.Append(" : ");
			buffer.Append(TypeToString(@event.ReturnType, includeNamespace));
			return buffer.ToString();
		}

		protected string GetDisplayName(IEntity entity, bool includeDeclaringTypeName, bool includeNamespace, bool includeNamespaceOfDeclaringTypeName)
		{
			string entityName;
			if (entity is ITypeDefinition t && !t.MetadataToken.IsNil)
			{
				MetadataReader metadata = t.ParentModule.PEFile.Metadata;
				var typeDef = metadata.GetTypeDefinition((TypeDefinitionHandle)t.MetadataToken);
				entityName = EscapeName(metadata.GetString(typeDef.Name));
			}
			else
			{
				entityName = EscapeName(entity.Name);
			}
			if (includeNamespace || includeDeclaringTypeName)
			{
				if (entity.DeclaringTypeDefinition != null)
					return TypeToString(entity.DeclaringTypeDefinition, includeNamespaceOfDeclaringTypeName) + "." + entityName;
				return EscapeName(entity.Namespace) + "." + entityName;
			}
			else
			{
				return entityName;
			}
		}

		/// <summary>
		/// Used for WPF keyboard navigation.
		/// </summary>
		public override string ToString()
		{
			return Name;
		}

		public virtual bool ShowMember(IEntity member)
		{
			return true;
		}

		/// <summary>
		/// This should produce a string representation of the entity for search to match search strings against.
		/// </summary>
		public virtual string GetEntityName(PEFile module, EntityHandle handle, bool fullName, bool omitGenerics)
		{
			MetadataReader metadata = module.Metadata;
			switch (handle.Kind)
			{
				case HandleKind.TypeDefinition:
					if (fullName)
						return EscapeName(((TypeDefinitionHandle)handle).GetFullTypeName(metadata).ToILNameString(omitGenerics));
					var td = metadata.GetTypeDefinition((TypeDefinitionHandle)handle);
					return EscapeName(metadata.GetString(td.Name));
				case HandleKind.FieldDefinition:
					var fd = metadata.GetFieldDefinition((FieldDefinitionHandle)handle);
					if (fullName)
						return EscapeName(fd.GetDeclaringType().GetFullTypeName(metadata).ToILNameString(omitGenerics) + "." + metadata.GetString(fd.Name));
					return EscapeName(metadata.GetString(fd.Name));
				case HandleKind.MethodDefinition:
					var md = metadata.GetMethodDefinition((MethodDefinitionHandle)handle);
					string methodName = metadata.GetString(md.Name);
					if (!omitGenerics)
					{
						int genericParamCount = md.GetGenericParameters().Count;
						if (genericParamCount > 0)
							methodName += "``" + genericParamCount;
					}
					if (fullName)
						return EscapeName(md.GetDeclaringType().GetFullTypeName(metadata).ToILNameString(omitGenerics) + "." + methodName);
					return EscapeName(methodName);
				case HandleKind.EventDefinition:
					var ed = metadata.GetEventDefinition((EventDefinitionHandle)handle);
					var declaringType = metadata.GetMethodDefinition(ed.GetAccessors().GetAny()).GetDeclaringType();
					if (fullName)
						return EscapeName(declaringType.GetFullTypeName(metadata).ToILNameString(omitGenerics) + "." + metadata.GetString(ed.Name));
					return EscapeName(metadata.GetString(ed.Name));
				case HandleKind.PropertyDefinition:
					var pd = metadata.GetPropertyDefinition((PropertyDefinitionHandle)handle);
					declaringType = metadata.GetMethodDefinition(pd.GetAccessors().GetAny()).GetDeclaringType();
					if (fullName)
						return EscapeName(declaringType.GetFullTypeName(metadata).ToILNameString(omitGenerics) + "." + metadata.GetString(pd.Name));
					return EscapeName(metadata.GetString(pd.Name));
				default:
					return null;
			}
		}

		public virtual CodeMappingInfo GetCodeMappingInfo(PEFile module, SRM.EntityHandle member)
		{
			var declaringType = member.GetDeclaringType(module.Metadata);

			if (declaringType.IsNil && member.Kind == SRM.HandleKind.TypeDefinition)
			{
				declaringType = (SRM.TypeDefinitionHandle)member;
			}

			return new CodeMappingInfo(module, declaringType);
		}

		public static string GetPlatformDisplayName(PEFile module)
		{
			var headers = module.Reader.PEHeaders;
			var architecture = headers.CoffHeader.Machine;
			var characteristics = headers.CoffHeader.Characteristics;
			var corflags = headers.CorHeader.Flags;
			switch (architecture)
			{
				case Machine.I386:
					if ((corflags & CorFlags.Prefers32Bit) != 0)
						return "AnyCPU (32-bit preferred)";
					if ((corflags & CorFlags.Requires32Bit) != 0)
						return "x86";
					// According to ECMA-335, II.25.3.3.1 CorFlags.Requires32Bit and Characteristics.Bit32Machine must be in sync
					// for assemblies containing managed code. However, this is not true for C++/CLI assemblies.
					if ((corflags & CorFlags.ILOnly) == 0 && (characteristics & Characteristics.Bit32Machine) != 0)
						return "x86";
					return "AnyCPU (64-bit preferred)";
				case Machine.Amd64:
					return "x64";
				case Machine.IA64:
					return "Itanium";
				default:
					return architecture.ToString();
			}
		}

		public static string GetRuntimeDisplayName(PEFile module)
		{
			return module.Metadata.MetadataVersion;
		}

		/// <summary>
		/// Escape characters that cannot be displayed in the UI.
		/// </summary>
		public static StringBuilder EscapeName(StringBuilder sb, string name)
		{
			foreach (char ch in name)
			{
				if (char.IsWhiteSpace(ch) || char.IsControl(ch) || char.IsSurrogate(ch))
					sb.AppendFormat("\\u{0:x4}", (int)ch);
				else
					sb.Append(ch);
			}
			return sb;
		}

		/// <summary>
		/// Escape characters that cannot be displayed in the UI.
		/// </summary>
		public static string EscapeName(string name)
		{
			return EscapeName(new StringBuilder(name.Length), name).ToString();
		}
	}
}
