// Copyright (c) 2021 Daniel Grunwald, Siegfried Pammer
// 
// 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.

#nullable enable

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;

using ICSharpCode.Decompiler.IL.ControlFlow;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util;

namespace ICSharpCode.Decompiler.IL.Transforms
{
	class PatternMatchingTransform : IILTransform
	{

		void IILTransform.Run(ILFunction function, ILTransformContext context)
		{
			if (!context.Settings.PatternMatching)
				return;
			foreach (var container in function.Descendants.OfType<BlockContainer>())
			{
				ControlFlowGraph? cfg = null;
				foreach (var block in container.Blocks.Reverse())
				{
					if (PatternMatchValueTypes(block, container, context, ref cfg))
					{
						continue;
					}
					if (PatternMatchRefTypes(block, container, context, ref cfg))
					{
						continue;
					}
				}
			}
		}

		/// Block {
		///		...
		///		stloc V(isinst T(testedOperand))
		///		if (comp.o(ldloc V == ldnull)) br falseBlock
		///		br trueBlock
		/// }
		/// 
		/// All other uses of V are in blocks dominated by trueBlock.
		/// =>
		/// Block {
		///		...
		///		if (match.type[T].notnull(V = testedOperand)) br trueBlock
		///		br falseBlock
		/// }
		///
		/// - or -
		/// 
		/// Block {
		/// 	stloc s(isinst T(testedOperand))
		/// 	stloc v(ldloc s)
		/// 	if (logic.not(comp.o(ldloc s != ldnull))) br falseBlock
		/// 	br trueBlock
		/// }
		/// =>
		/// Block {
		///		...
		///		if (match.type[T].notnull(V = testedOperand)) br trueBlock
		///		br falseBlock
		/// }
		/// 
		/// All other uses of V are in blocks dominated by trueBlock.
		private bool PatternMatchRefTypes(Block block, BlockContainer container, ILTransformContext context, ref ControlFlowGraph? cfg)
		{
			if (!block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst))
				return false;
			int pos = block.Instructions.Count - 3;
			if (condition.MatchLdLoc(out var conditionVar))
			{
				// stloc conditionVar(comp.o(ldloc s == ldnull))
				// if (logic.not(ldloc conditionVar)) br trueBlock
				if (pos < 0)
					return false;
				if (!(conditionVar.IsSingleDefinition && conditionVar.LoadCount == 1
					&& conditionVar.Kind == VariableKind.StackSlot))
				{
					return false;
				}
				if (!block.Instructions[pos].MatchStLoc(conditionVar, out condition))
					return false;
				pos--;
			}
			if (condition.MatchCompEqualsNull(out var loadInNullCheck))
			{
				ExtensionMethods.Swap(ref trueInst, ref falseInst);
			}
			else if (condition.MatchCompNotEqualsNull(out loadInNullCheck))
			{
				// do nothing
			}
			else
			{
				return false;
			}
			if (!loadInNullCheck.MatchLdLoc(out var s))
				return false;
			if (!s.IsSingleDefinition)
				return false;
			if (s.Kind is not (VariableKind.Local or VariableKind.StackSlot))
				return false;
			if (pos < 0)
				return false;
			// stloc V(isinst T(testedOperand))
			ILInstruction storeToV = block.Instructions[pos];
			if (!storeToV.MatchStLoc(out var v, out var value))
				return false;
			if (value.MatchLdLoc(s))
			{
				// stloc v(ldloc s)
				pos--;
				if (pos < 0 || !block.Instructions[pos].MatchStLoc(s, out value))
					return false;
				if (v.Kind is not (VariableKind.Local or VariableKind.StackSlot))
					return false;
				if (s.LoadCount != 2)
					return false;
			}
			else
			{
				if (v != s)
					return false;
			}
			IType? unboxType;
			if (value is UnboxAny unboxAny)
			{
				// stloc S(unbox.any T(isinst T(testedOperand)))
				unboxType = unboxAny.Type;
				value = unboxAny.Argument;
			}
			else
			{
				unboxType = null;
			}
			if (value is not IsInst { Argument: var testedOperand, Type: var type })
				return false;
			if (type.IsReferenceType != true)
				return false;
			if (!(unboxType == null || type.Equals(unboxType)))
				return false;

			if (!v.Type.Equals(type))
				return false;
			if (!CheckAllUsesDominatedBy(v, container, trueInst, storeToV, loadInNullCheck, context, ref cfg))
				return false;
			context.Step($"Type pattern matching {v.Name}", block);
			//	if (match.type[T].notnull(V = testedOperand)) br trueBlock

			var ifInst = (IfInstruction)block.Instructions.SecondToLastOrDefault()!;

			ifInst.Condition = new MatchInstruction(v, testedOperand) {
				CheckNotNull = true,
				CheckType = true
			}.WithILRange(ifInst.Condition);
			ifInst.TrueInst = trueInst;
			block.Instructions[block.Instructions.Count - 1] = falseInst;
			block.Instructions.RemoveRange(pos, ifInst.ChildIndex - pos);
			v.Kind = VariableKind.PatternLocal;

			if (trueInst.MatchBranch(out var trueBlock) && trueBlock.IncomingEdgeCount == 1 && trueBlock.Parent == container)
			{
				DetectPropertySubPatterns((MatchInstruction)ifInst.Condition, trueBlock, falseInst);
			}

			return true;
		}

		private bool DetectPropertySubPatterns(MatchInstruction parentPattern, Block block, ILInstruction parentFalseInst)
		{
			// if (match.notnull.type[System.String] (V_0 = callvirt get_C(ldloc V_2))) br IL_0022
			// br IL_0037
			if (block.Instructions.Count == 2 && block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst))
			{
				if (MatchInstruction.IsPatternMatch(condition, out var operand))
				{
					if (operand is not CallInstruction {
						Method: {
							SymbolKind: SymbolKind.Accessor,
							AccessorKind: MethodSemanticsAttributes.Getter
						},
						Arguments: [LdLoc ldloc]
					} call)
					{
						return false;
					}
					if (ldloc.Variable != parentPattern.Variable)
					{
						return false;
					}
					if (!DetectExitPoints.CompatibleExitInstruction(parentFalseInst, falseInst))
					{
						return false;
					}
					parentPattern.SubPatterns.Add(condition);
					block.Instructions.RemoveAt(0);
					block.Instructions[0] = trueInst;
					return true;
				}
			}
			return false;
		}

		private bool CheckAllUsesDominatedBy(ILVariable v, BlockContainer container, ILInstruction trueInst,
			ILInstruction storeToV, ILInstruction? loadInNullCheck, ILTransformContext context, ref ControlFlowGraph? cfg)
		{
			var targetBlock = trueInst as Block;
			if (targetBlock == null && !trueInst.MatchBranch(out targetBlock))
			{
				return false;
			}

			if (targetBlock.Parent != container)
				return false;
			if (targetBlock.IncomingEdgeCount != 1)
				return false;
			cfg ??= new ControlFlowGraph(container, context.CancellationToken);
			var targetBlockNode = cfg.GetNode(targetBlock);
			var uses = v.LoadInstructions.Concat<ILInstruction>(v.AddressInstructions)
				.Concat(v.StoreInstructions.Cast<ILInstruction>());
			foreach (var use in uses)
			{
				if (use == storeToV || use == loadInNullCheck)
					continue;
				Block? found = null;
				for (ILInstruction? current = use; current != null; current = current.Parent)
				{
					if (current.Parent == container)
					{
						found = (Block)current;
						break;
					}
				}
				if (found == null)
					return false;
				var node = cfg.GetNode(found);
				if (!targetBlockNode.Dominates(node))
					return false;
			}
			return true;
		}

		/// Block {
		///		...
		///		[stloc temp(ldloc testedOperand)]
		/// 	if (comp.o(isinst T(ldloc testedOperand) == ldnull)) br falseBlock
		/// 	br unboxBlock
		/// }
		/// 
		/// Block unboxBlock (incoming: 1) {
		/// 	stloc V(unbox.any T(ldloc temp))
		/// 	...
		/// }
		/// =>
		/// Block {
		///		...
		///		if (match.type[T].notnull(V = testedOperand)) br unboxBlock
		///		br falseBlock
		///	}
		private bool PatternMatchValueTypes(Block block, BlockContainer container, ILTransformContext context, ref ControlFlowGraph? cfg)
		{
			if (!MatchIsInstBlock(block, out var type, out var testedOperand, out var testedVariable,
				out var boxType1, out var unboxBlock, out var falseBlock))
			{
				return false;
			}
			StLoc? tempStore = block.Instructions.ElementAtOrDefault(block.Instructions.Count - 3) as StLoc;
			if (tempStore == null || !tempStore.Value.MatchLdLoc(testedVariable))
			{
				tempStore = null;
			}
			if (!MatchUnboxBlock(unboxBlock, type, out var unboxOperand, out var boxType2, out var storeToV))
			{
				return false;
			}
			if (!object.Equals(boxType1, boxType2))
			{
				return false;
			}
			if (unboxOperand == testedVariable)
			{
				// do nothing
			}
			else if (unboxOperand == tempStore?.Variable)
			{
				if (!(tempStore.Variable.IsSingleDefinition && tempStore.Variable.LoadCount == 1))
					return false;
			}
			else
			{
				return false;
			}
			if (!CheckAllUsesDominatedBy(storeToV.Variable, container, unboxBlock, storeToV, null, context, ref cfg))
				return false;
			context.Step($"PatternMatching with {storeToV.Variable.Name}", block);
			var ifInst = (IfInstruction)block.Instructions.SecondToLastOrDefault()!;
			ifInst.Condition = new MatchInstruction(storeToV.Variable, testedOperand) {
				CheckNotNull = true,
				CheckType = true
			};
			((Branch)ifInst.TrueInst).TargetBlock = unboxBlock;
			((Branch)block.Instructions.Last()).TargetBlock = falseBlock;
			unboxBlock.Instructions.RemoveAt(0);
			if (unboxOperand == tempStore?.Variable)
			{
				block.Instructions.Remove(tempStore);
			}
			// HACK: condition detection uses StartILOffset of blocks to decide which branch of if-else
			// should become the then-branch. Change the unboxBlock StartILOffset from an offset inside
			// the pattern matching machinery to an offset belonging to an instruction in the then-block.
			unboxBlock.SetILRange(unboxBlock.Instructions[0]);
			storeToV.Variable.Kind = VariableKind.PatternLocal;
			return true;
		}

		///	...
		/// if (comp.o(isinst T(ldloc testedOperand) == ldnull)) br falseBlock
		/// br unboxBlock
		/// - or -
		/// ...
		/// if (comp.o(isinst T(box ``0(ldloc testedOperand)) == ldnull)) br falseBlock
		/// br unboxBlock
		private bool MatchIsInstBlock(Block block,
			[NotNullWhen(true)] out IType? type,
			[NotNullWhen(true)] out ILInstruction? testedOperand,
			[NotNullWhen(true)] out ILVariable? testedVariable,
			out IType? boxType,
			[NotNullWhen(true)] out Block? unboxBlock,
			[NotNullWhen(true)] out Block? falseBlock)
		{
			type = null;
			testedOperand = null;
			testedVariable = null;
			boxType = null;
			unboxBlock = null;
			falseBlock = null;
			if (!block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst))
			{
				return false;
			}
			if (condition.MatchCompEqualsNull(out var arg))
			{
				ExtensionMethods.Swap(ref trueInst, ref falseInst);
			}
			else if (condition.MatchCompNotEqualsNull(out arg))
			{
				// do nothing
			}
			else
			{
				return false;
			}
			if (!arg.MatchIsInst(out testedOperand, out type))
			{
				return false;
			}
			if (!(testedOperand.MatchBox(out var boxArg, out boxType) && boxType.Kind == TypeKind.TypeParameter))
			{
				boxArg = testedOperand;
			}
			if (!boxArg.MatchLdLoc(out testedVariable))
			{
				return false;
			}
			return trueInst.MatchBranch(out unboxBlock) && falseInst.MatchBranch(out falseBlock)
				&& unboxBlock.Parent == block.Parent && falseBlock.Parent == block.Parent;
		}

		/// Block unboxBlock (incoming: 1) {
		/// 	stloc V(unbox.any T(ldloc testedOperand))
		/// 	...
		/// 	- or -
		/// 	stloc V(unbox.any T(isinst T(box ``0(ldloc testedOperand))))
		/// 	...
		/// }
		private bool MatchUnboxBlock(Block unboxBlock, IType type, [NotNullWhen(true)] out ILVariable? testedVariable,
			out IType? boxType, [NotNullWhen(true)] out StLoc? storeToV)
		{
			boxType = null;
			storeToV = null;
			testedVariable = null;
			if (unboxBlock.IncomingEdgeCount != 1)
				return false;
			storeToV = unboxBlock.Instructions[0] as StLoc;
			if (storeToV == null)
				return false;
			var value = storeToV.Value;
			if (!(value.MatchUnboxAny(out var arg, out var t) && t.Equals(type)))
				return false;
			if (arg.MatchIsInst(out var isinstArg, out var isinstType) && isinstType.Equals(type))
			{
				arg = isinstArg;
			}
			if (arg.MatchBox(out var boxArg, out boxType) && boxType.Kind == TypeKind.TypeParameter)
			{
				arg = boxArg;
			}
			if (!arg.MatchLdLoc(out testedVariable))
			{
				return false;
			}
			if (boxType != null && !boxType.Equals(testedVariable.Type))
			{
				return false;
			}
			return true;
		}
	}
}