package fr.adrienbrault.idea.symfony2plugin.dic;
import com.intellij.codeInsight.hints.HintInfo;
import com.intellij.codeInsight.hints.InlayInfo;
import com.intellij.codeInsight.hints.InlayParameterHintsProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlText;
import com.intellij.util.Consumer;
import com.jetbrains.php.lang.psi.elements.Function;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.Parameter;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.resolve.types.PhpType;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.config.xml.XmlHelper;
import fr.adrienbrault.idea.symfony2plugin.config.xml.XmlServiceContainerAnnotator;
import fr.adrienbrault.idea.symfony2plugin.dic.container.dict.ServiceTypeHint;
import fr.adrienbrault.idea.symfony2plugin.dic.container.util.ServiceContainerUtil;
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLScalar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class ServiceArgumentParameterHintsProvider implements InlayParameterHintsProvider {
@NotNull
@Override
public List<InlayInfo> getParameterHints(PsiElement psiElement) {
if(!Symfony2ProjectComponent.isEnabled(psiElement)) {
return Collections.emptyList();
}
List<InlayInfo> inlays = new ArrayList<>();
Match typeHint = getTypeHint(psiElement);
if(typeHint != null) {
inlays.add(new InlayInfo(typeHint.parameter, typeHint.targetOffset));
}
return inlays;
}
@Nullable
@Override
public HintInfo getHintInfo(PsiElement psiElement) {
return null;
}
@NotNull
@Override
public Set<String> getDefaultBlackList() {
return Collections.emptySet();
}
@Override
public String getInlayPresentation(@NotNull String inlayText) {
// remove ":"
return inlayText;
}
@Nullable
private Match getTypeHint(@NotNull PsiElement psiElement) {
if(psiElement instanceof YAMLScalar) {
// arguments: [@foobar]
ServiceTypeHint serviceTypeHint = ServiceContainerUtil.getYamlConstructorTypeHint(
(YAMLScalar) psiElement,
new ContainerCollectionResolver.LazyServiceCollector(psiElement.getProject())
);
if(serviceTypeHint != null) {
String s = attachMethodInstances(serviceTypeHint.getMethod(), serviceTypeHint.getIndex());
if(s != null) {
return new Match(s, psiElement.getTextRange().getEndOffset());
}
}
// call: [setFoo, [@foo]]
final Match[] match = {null};
YamlHelper.visitServiceCallArgumentMethodIndex((YAMLScalar) psiElement, parameter ->
match[0] = new Match(createTypeHintFromParameter(psiElement.getProject(), parameter), psiElement.getTextRange().getEndOffset())
);
return match[0];
} else if (psiElement instanceof XmlAttributeValue) {
// <service><argument type="service" id="a<caret>a"></service>
PsiElement xmlAttribute = psiElement.getParent();
if(xmlAttribute instanceof XmlAttribute && "id".equalsIgnoreCase(((XmlAttribute) xmlAttribute).getName())) {
PsiElement argumentTag = xmlAttribute.getParent();
if (argumentTag instanceof XmlTag) {
if ("service".equalsIgnoreCase(((XmlTag) argumentTag).getName())) {
// <service type="alias" id="a<caret>a"/>
String alias = ((XmlTag) argumentTag).getAttributeValue("alias");
if (alias != null && StringUtils.isNotBlank(alias)) {
PhpClass serviceClass = ServiceUtil.getServiceClass(psiElement.getProject(), alias);
if (serviceClass != null) {
return new Match(serviceClass.getName(), argumentTag.getTextRange().getEndOffset());
}
}
} else if ("argument".equalsIgnoreCase(((XmlTag) argumentTag).getName())) {
// <service><argument type="service" id="a<caret>a"></service>
PsiElement serviceTag = argumentTag.getParent();
if (serviceTag instanceof XmlTag && "service".equals(((XmlTag) serviceTag).getName())) {
Pair<String, Method> parameterHint = findMethodParameterHint((XmlTag) argumentTag);
if (parameterHint != null) {
return new Match(parameterHint.getFirst(), argumentTag.getTextRange().getEndOffset());
}
}
// <call method="setMailer">
// <argument type="service" id="ma<caret>iler" />
// </call>
final Match[] match = {null};
XmlHelper.visitServiceCallArgumentMethodIndex((XmlAttributeValue) psiElement, parameter ->
match[0] = new Match(createTypeHintFromParameter(psiElement.getProject(), parameter), argumentTag.getTextRange().getEndOffset())
);
return match[0];
}
}
}
} else if(psiElement instanceof XmlText) {
// <service><argument>%a<caret>a%</argument></service>
PsiElement argumentTag = psiElement.getParent();
// match only: <argument>%a<caret>a%</argument>
// ignore: <argument>\n<caret><argument></argument></argument>
if(argumentTag instanceof XmlTag && "argument".equals(((XmlTag) argumentTag).getName()) && ((XmlTag) argumentTag).getSubTags().length == 0) {
PsiElement serviceTag = argumentTag.getParent();
if(serviceTag instanceof XmlTag && "service".equals(((XmlTag) serviceTag).getName())) {
Pair<String, Method> parameterHint = findMethodParameterHint((XmlTag) argumentTag);
if(parameterHint != null) {
return new Match(parameterHint.getFirst(), argumentTag.getTextRange().getEndOffset());
}
}
}
}
return null;
}
private Pair<String, Method> foo(@NotNull Project project, @NotNull String aClass, java.util.function.Function<Void, Integer> function) {
if(StringUtils.isNotBlank(aClass)) {
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(project, aClass);
if(phpClass != null) {
Method constructor = phpClass.getConstructor();
if(constructor != null) {
Integer argumentIndex = function.apply(null);
if(argumentIndex >= 0) {
String s = attachMethodInstances(constructor, argumentIndex);
if(s == null) {
return null;
}
return Pair.create(s, constructor);
}
}
}
}
return null;
}
@Nullable
private Pair<String, Method> findMethodParameterHint(@NotNull XmlTag argumentTag) {
PsiElement serviceTag = argumentTag.getParent();
if("service".equalsIgnoreCase(((XmlTag) serviceTag).getName())) {
String aClass = ((XmlTag) serviceTag).getAttributeValue("class");
if(aClass != null && StringUtils.isNotBlank(aClass)) {
return foo(argumentTag.getProject(), aClass, aVoid -> XmlServiceContainerAnnotator.getArgumentIndex(argumentTag));
}
}
return null;
}
@Nullable
private String attachMethodInstances(@NotNull Function function, int parameterIndex) {
Parameter[] constructorParameter = function.getParameters();
if(parameterIndex >= constructorParameter.length) {
return null;
}
Parameter parameter = constructorParameter[parameterIndex];
return createTypeHintFromParameter(function.getProject(), parameter);
}
@NotNull
private String createTypeHintFromParameter(@NotNull Project project, Parameter parameter) {
String className = parameter.getDeclaredType().toString();
if(PhpType.isNotExtendablePrimitiveType(className)) {
return parameter.getName();
}
int i = className.lastIndexOf("\\");
if(i > 0) {
return className.substring(i + 1);
}
PhpClass expectedClass = PhpElementsUtil.getClassInterface(project, className);
if(expectedClass != null) {
return expectedClass.getName();
}
return parameter.getName();
}
private static class Match {
@NotNull
private final String parameter;
private final int targetOffset;
Match(@NotNull String parameter, int targetOffset) {
this.parameter = parameter;
this.targetOffset = targetOffset;
}
}
}