/*
 * Copyright 2013-2017 consulo.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package consulo.csharp.lang.psi.impl.source.resolve.extensionResolver;

import java.util.Collection;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.intellij.openapi.progress.ProgressManager;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.ResolveState;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.SmartList;
import consulo.annotation.access.RequiredReadAction;
import consulo.csharp.lang.psi.CSharpCallArgument;
import consulo.csharp.lang.psi.CSharpCallArgumentListOwner;
import consulo.csharp.lang.psi.CSharpMethodDeclaration;
import consulo.csharp.lang.psi.CSharpModifier;
import consulo.csharp.lang.psi.CSharpReferenceExpression;
import consulo.csharp.lang.psi.impl.CSharpNullableTypeUtil;
import consulo.csharp.lang.psi.impl.CSharpTypeUtil;
import consulo.csharp.lang.psi.impl.light.CSharpLightMethodDeclaration;
import consulo.csharp.lang.psi.impl.light.CSharpLightParameterList;
import consulo.csharp.lang.psi.impl.resolve.CSharpElementGroupImpl;
import consulo.csharp.lang.psi.impl.resolve.CSharpResolveContextUtil;
import consulo.csharp.lang.psi.impl.source.CSharpExpressionWithOperatorImpl;
import consulo.csharp.lang.psi.impl.source.resolve.CSharpResolveResult;
import consulo.csharp.lang.psi.impl.source.resolve.StubScopeProcessor;
import consulo.csharp.lang.psi.impl.source.resolve.genericInference.GenericInferenceUtil;
import consulo.csharp.lang.psi.impl.source.resolve.type.wrapper.GenericUnwrapTool;
import consulo.csharp.lang.psi.impl.source.resolve.util.CSharpResolveUtil;
import consulo.csharp.lang.psi.resolve.CSharpElementGroup;
import consulo.csharp.lang.psi.resolve.CSharpResolveContext;
import consulo.csharp.lang.psi.resolve.CSharpResolveSelector;
import consulo.dotnet.psi.DotNetParameter;
import consulo.dotnet.psi.DotNetParameterList;
import consulo.dotnet.psi.DotNetParameterListOwner;
import consulo.dotnet.resolve.DotNetGenericExtractor;
import consulo.dotnet.resolve.DotNetTypeRef;

/**
 * @author VISTALL
 * @since 26.03.14
 */
public class ExtensionResolveScopeProcessor extends StubScopeProcessor
{
	private final CSharpReferenceExpression myExpression;
	private final boolean myCompletion;
	private final StubScopeProcessor myProcessor;
	@Nullable
	private final CSharpCallArgumentListOwner myCallArgumentListOwner;

	private final List<CSharpMethodDeclaration> myResolvedElements = new SmartList<>();

	private ExtensionQualifierAsCallArgumentWrapper myArgumentWrapper;
	private DotNetTypeRef myQualifierTypeRef;

	public ExtensionResolveScopeProcessor(@Nonnull DotNetTypeRef qualifierTypeRef,
			@Nonnull CSharpReferenceExpression expression,
			boolean completion,
			@Nonnull StubScopeProcessor processor,
			@Nullable CSharpCallArgumentListOwner callArgumentListOwner)
	{
		myExpression = expression;
		myCompletion = completion;
		myProcessor = processor;
		myCallArgumentListOwner = callArgumentListOwner;

		myQualifierTypeRef = qualifierTypeRef;
		myArgumentWrapper = new ExtensionQualifierAsCallArgumentWrapper(expression.getProject(), qualifierTypeRef);
	}

	@RequiredReadAction
	public void unpackNullableTypeRef()
	{
		myQualifierTypeRef = CSharpNullableTypeUtil.unbox(myQualifierTypeRef);
		myArgumentWrapper = new ExtensionQualifierAsCallArgumentWrapper(myExpression.getProject(), myQualifierTypeRef);
	}

	@RequiredReadAction
	@Override
	public boolean execute(@Nonnull final PsiElement element, ResolveState state)
	{
		if(myCompletion)
		{
			DotNetGenericExtractor extractor = state.get(CSharpResolveUtil.EXTRACTOR);
			assert extractor != null;

			CSharpResolveContext context = CSharpResolveContextUtil.createContext(extractor, myExpression.getResolveScope(), element);

			context.processExtensionMethodGroups(elementGroup ->
			{
				Collection<CSharpMethodDeclaration> elements = elementGroup.getElements();
				for(CSharpMethodDeclaration psiElement : elements)
				{
					ProgressManager.checkCanceled();

					if(isAlreadyAdded(psiElement))
					{
						continue;
					}

					// if we typing inside file with extension methods, and extension method from this file is resolved to qualifier
					// it will show twice, from completion file copy and original file
					// skip completion copy
					if(isCompletionCopy(psiElement))
					{
						continue;
					}

					GenericInferenceUtil.GenericInferenceResult inferenceResult = inferenceGenericExtractor(psiElement);

					DotNetTypeRef firstParameterTypeRef = getFirstTypeRefOrParameter(psiElement, inferenceResult.getExtractor());

					if(!CSharpTypeUtil.isInheritableWithImplicit(firstParameterTypeRef, myQualifierTypeRef, myExpression))
					{
						continue;
					}

					myProcessor.pushResultExternally(new CSharpResolveResult(transform(psiElement, inferenceResult, null)));
				}
				return true;
			});
		}
		else
		{
			CSharpResolveSelector selector = state.get(CSharpResolveUtil.SELECTOR);
			if(selector == null)
			{
				return true;
			}

			DotNetGenericExtractor extractor = state.get(CSharpResolveUtil.EXTRACTOR);
			assert extractor != null;

			CSharpResolveContext context = CSharpResolveContextUtil.createContext(extractor, myExpression.getResolveScope(), element);

			Collection<PsiElement> psiElements = selector.doSelectElement(context, false);

			for(PsiElement e : psiElements)
			{
				CSharpElementGroup<?> elementGroup = (CSharpElementGroup<?>) e;

				for(PsiElement psiElement : elementGroup.getElements())
				{
					ProgressManager.checkCanceled();

					CSharpMethodDeclaration methodDeclaration = (CSharpMethodDeclaration) psiElement;

					if(isAlreadyAdded(methodDeclaration))
					{
						continue;
					}

					GenericInferenceUtil.GenericInferenceResult inferenceResult = inferenceGenericExtractor(methodDeclaration);

					DotNetTypeRef firstParameterTypeRef = getFirstTypeRefOrParameter(methodDeclaration, inferenceResult.getExtractor());

					if(!CSharpTypeUtil.isInheritableWithImplicit(firstParameterTypeRef, myQualifierTypeRef, myExpression))
					{
						continue;
					}

					myResolvedElements.add(transform(methodDeclaration, inferenceResult, element));
				}
			}
		}

		return true;
	}

	private boolean isCompletionCopy(@Nonnull PsiElement element)
	{
		PsiFile containingFile = element.getContainingFile();
		return containingFile != null && containingFile.getViewProvider().getVirtualFile() instanceof LightVirtualFile;
	}

	private boolean isAlreadyAdded(@Nonnull CSharpMethodDeclaration methodDeclaration)
	{
		for(CSharpMethodDeclaration resolvedElement : myResolvedElements)
		{
			if(resolvedElement.isEquivalentTo(methodDeclaration))
			{
				return true;
			}
		}
		return false;
	}

	@Nonnull
	@RequiredReadAction
	public GenericInferenceUtil.GenericInferenceResult inferenceGenericExtractor(CSharpMethodDeclaration methodDeclaration)
	{
		CSharpCallArgument[] newArguments;
		DotNetTypeRef[] typeArgumentRefs;

		// situation
		// using System.Linq;
		// ..
		//  if(index < _dictionary.Count)
		// ..
		// We try to search .Count - but without parameters
		// or we will get recursion search - due it will map call to
		// _dictionary.Count(index, _dictionary.Count);
		if(myCallArgumentListOwner instanceof CSharpExpressionWithOperatorImpl)
		{
			newArguments = CSharpCallArgument.EMPTY_ARRAY;
			typeArgumentRefs = DotNetTypeRef.EMPTY_ARRAY;
		}
		else
		{
			CSharpCallArgument[] arguments = myCallArgumentListOwner == null ? CSharpCallArgument.EMPTY_ARRAY : myCallArgumentListOwner.getCallArguments();
			newArguments = new CSharpCallArgument[arguments.length + 1];
			System.arraycopy(arguments, 0, newArguments, 1, arguments.length);

			newArguments[0] = myArgumentWrapper;

			typeArgumentRefs = myExpression.getTypeArgumentListRefs();
		}

		return GenericInferenceUtil.inferenceGenericExtractor(newArguments, typeArgumentRefs, myExpression, methodDeclaration);
	}

	@RequiredReadAction
	public void consumeAsMethodGroup()
	{
		if(myCompletion || myResolvedElements.isEmpty())
		{
			return;
		}

		CSharpMethodDeclaration methodDeclaration = myResolvedElements.get(0);
		assert methodDeclaration != null;
		CSharpElementGroupImpl element = new CSharpElementGroupImpl<>(myExpression.getProject(), methodDeclaration.getName(), myResolvedElements);
		myProcessor.pushResultExternally(new CSharpResolveResult(element, true));
	}

	@Nonnull
	@RequiredReadAction
	private DotNetTypeRef getFirstTypeRefOrParameter(DotNetParameterListOwner owner, DotNetGenericExtractor extractor)
	{
		DotNetParameter[] parameters = owner.getParameters();
		assert parameters.length != 0;
		assert parameters[0].hasModifier(CSharpModifier.THIS);
		return GenericUnwrapTool.exchangeTypeRef(parameters[0].toTypeRef(false), extractor, myExpression);
	}

	@RequiredReadAction
	private static CSharpMethodDeclaration transform(final CSharpMethodDeclaration methodDeclaration,
			@Nonnull GenericInferenceUtil.GenericInferenceResult inferenceResult,
			@Nullable PsiElement providerElement)
	{
		DotNetParameterList parameterList = methodDeclaration.getParameterList();
		assert parameterList != null;
		DotNetParameter[] oldParameters = methodDeclaration.getParameters();

		DotNetParameter[] parameters = new DotNetParameter[oldParameters.length - 1];
		System.arraycopy(oldParameters, 1, parameters, 0, parameters.length);

		CSharpLightParameterList lightParameterList = new CSharpLightParameterList(parameterList, parameters);
		CSharpLightMethodDeclaration declaration = new CSharpLightMethodDeclaration(methodDeclaration, lightParameterList)
		{
			@Override
			public boolean canNavigate()
			{
				return true;
			}

			@Override
			public void navigate(boolean requestFocus)
			{
				((Navigatable) methodDeclaration).navigate(requestFocus);
			}
		};

		CSharpMethodDeclaration extractedMethod = GenericUnwrapTool.extract(declaration, inferenceResult.getExtractor());
		extractedMethod.putUserData(CSharpResolveUtil.EXTENSION_METHOD_WRAPPER, methodDeclaration);
		extractedMethod.putUserData(GenericInferenceUtil.INFERENCE_RESULT, inferenceResult);
		if(providerElement != null)
		{
			extractedMethod.putUserData(CSharpResolveResult.FORCE_PROVIDER_ELEMENT, providerElement);
		}
		return extractedMethod;
	}
}