using System;
using ICSharpCode.Decompiler.Util;
using ICSharpCode.Decompiler.Disassembler;
using SRM = System.Reflection.Metadata;
using ILOpCode = System.Reflection.Metadata.ILOpCode;
using ICSharpCode.Decompiler;

using static System.Reflection.Metadata.PEReaderExtensions;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Metadata;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Threading;
using System.Collections.Concurrent;

namespace ICSharpCode.ILSpy.Search
{
	class LiteralSearchStrategy : AbstractEntitySearchStrategy
	{
		readonly TypeCode searchTermLiteralType;
		readonly object searchTermLiteralValue;

		public LiteralSearchStrategy(Language language, ApiVisibility apiVisibility, IProducerConsumerCollection<SearchResult> resultQueue, params string[] terms)
			: base(language, apiVisibility, resultQueue, terms)
		{
			if (terms.Length == 1) {
				var lexer = new Lexer(new LATextReader(new System.IO.StringReader(terms[0])));
				var value = lexer.NextToken();

				if (value != null && value.LiteralValue != null) {
					TypeCode valueType = Type.GetTypeCode(value.LiteralValue.GetType());
					switch (valueType) {
						case TypeCode.Byte:
						case TypeCode.SByte:
						case TypeCode.Int16:
						case TypeCode.UInt16:
						case TypeCode.Int32:
						case TypeCode.UInt32:
						case TypeCode.Int64:
						case TypeCode.UInt64:
							searchTermLiteralType = TypeCode.Int64;
							searchTermLiteralValue = CSharpPrimitiveCast.Cast(TypeCode.Int64, value.LiteralValue, false);
							break;
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.String:
							searchTermLiteralType = valueType;
							searchTermLiteralValue = value.LiteralValue;
							break;
					}
				}
			}
		}

		public override void Search(PEFile module, CancellationToken cancellationToken)
		{
			cancellationToken.ThrowIfCancellationRequested();
			var metadata = module.Metadata;
			var typeSystem = module.GetTypeSystemWithCurrentOptionsOrNull();
			if (typeSystem == null) return;

			foreach (var handle in metadata.MethodDefinitions) {
				cancellationToken.ThrowIfCancellationRequested();
				var md = metadata.GetMethodDefinition(handle);
				if (!md.HasBody() || !MethodIsLiteralMatch(module, md)) continue;
				var method = ((MetadataModule)typeSystem.MainModule).GetDefinition(handle);
				if (!CheckVisibility(method)) continue;
				OnFoundResult(method);
			}

			foreach (var handle in metadata.FieldDefinitions) {
				cancellationToken.ThrowIfCancellationRequested();
				var fd = metadata.GetFieldDefinition(handle);
				if (!fd.HasFlag(System.Reflection.FieldAttributes.Literal))
					continue;
				var constantHandle = fd.GetDefaultValue();
				if (constantHandle.IsNil)
					continue;
				var constant = metadata.GetConstant(constantHandle);
				var blob = metadata.GetBlobReader(constant.Value);
				if (!IsLiteralMatch(metadata, blob.ReadConstant(constant.TypeCode)))
					continue;
				IField field = ((MetadataModule)typeSystem.MainModule).GetDefinition(handle);
				if (!CheckVisibility(field)) continue;
				OnFoundResult(field);
			}
		}

		bool IsLiteralMatch(MetadataReader metadata, object val)
		{
			if (val == null)
				return false;
			switch (searchTermLiteralType) {
				case TypeCode.Int64:
					TypeCode tc = Type.GetTypeCode(val.GetType());
					if (tc >= TypeCode.SByte && tc <= TypeCode.UInt64)
						return CSharpPrimitiveCast.Cast(TypeCode.Int64, val, false).Equals(searchTermLiteralValue);
					else
						return false;
				case TypeCode.Single:
				case TypeCode.Double:
				case TypeCode.String:
					return searchTermLiteralValue.Equals(val);
				default:
					// substring search with searchTerm
					return IsMatch(val.ToString());
			}
		}

		bool MethodIsLiteralMatch(PEFile module, MethodDefinition methodDefinition)
		{
			var blob = module.Reader.GetMethodBody(methodDefinition.RelativeVirtualAddress).GetILReader();
			if (searchTermLiteralType == TypeCode.Int64) {
				long val = (long)searchTermLiteralValue;
				while (blob.RemainingBytes > 0) {
					ILOpCode code;
					switch (code = ILParser.DecodeOpCode(ref blob)) {
						case ILOpCode.Ldc_i8:
							if (val == blob.ReadInt64())
								return true;
							break;
						case ILOpCode.Ldc_i4:
							if (val == blob.ReadInt32())
								return true;
							break;
						case ILOpCode.Ldc_i4_s:
							if (val == blob.ReadSByte())
								return true;
							break;
						case ILOpCode.Ldc_i4_m1:
							if (val == -1)
								return true;
							break;
						case ILOpCode.Ldc_i4_0:
							if (val == 0)
								return true;
							break;
						case ILOpCode.Ldc_i4_1:
							if (val == 1)
								return true;
							break;
						case ILOpCode.Ldc_i4_2:
							if (val == 2)
								return true;
							break;
						case ILOpCode.Ldc_i4_3:
							if (val == 3)
								return true;
							break;
						case ILOpCode.Ldc_i4_4:
							if (val == 4)
								return true;
							break;
						case ILOpCode.Ldc_i4_5:
							if (val == 5)
								return true;
							break;
						case ILOpCode.Ldc_i4_6:
							if (val == 6)
								return true;
							break;
						case ILOpCode.Ldc_i4_7:
							if (val == 7)
								return true;
							break;
						case ILOpCode.Ldc_i4_8:
							if (val == 8)
								return true;
							break;
						default:
							ILParser.SkipOperand(ref blob, code);
							break;
					}
				}
			} else if (searchTermLiteralType != TypeCode.Empty) {
				ILOpCode expectedCode;
				switch (searchTermLiteralType) {
					case TypeCode.Single:
						expectedCode = ILOpCode.Ldc_r4;
						break;
					case TypeCode.Double:
						expectedCode = ILOpCode.Ldc_r8;
						break;
					case TypeCode.String:
						expectedCode = ILOpCode.Ldstr;
						break;
					default:
						throw new InvalidOperationException();
				}
				while (blob.RemainingBytes > 0) {
					var code = ILParser.DecodeOpCode(ref blob);
					if (code != expectedCode) {
						ILParser.SkipOperand(ref blob, code);
						continue;
					}
					switch (code) {
						case ILOpCode.Ldc_r4:
							if ((float)searchTermLiteralValue == blob.ReadSingle())
								return true;
							break;
						case ILOpCode.Ldc_r8:
							if ((double)searchTermLiteralValue == blob.ReadDouble())
								return true;
							break;
						case ILOpCode.Ldstr:
							if ((string)searchTermLiteralValue == ILParser.DecodeUserString(ref blob, module.Metadata))
								return true;
							break;
					}
				}
			} else {
				while (blob.RemainingBytes > 0) {
					var code = ILParser.DecodeOpCode(ref blob);
					if (code != ILOpCode.Ldstr) {
						ILParser.SkipOperand(ref blob, code);
						continue;
					}
					if (IsMatch(ILParser.DecodeUserString(ref blob, module.Metadata)))
						return true;
				}
			}
			return false;
		}
	}
}
