package org.elixir_lang.structure_view.element;
import com.intellij.ide.util.treeView.smartTree.TreeElement;
import com.intellij.navigation.ItemPresentation;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.ElementDescriptionLocation;
import com.intellij.psi.PsiElement;
import com.intellij.usageView.UsageViewTypeLocation;
import org.apache.commons.lang.math.IntRange;
import org.elixir_lang.errorreport.Logger;
import org.elixir_lang.navigation.item_presentation.NameArity;
import org.elixir_lang.psi.call.Call;
import org.elixir_lang.psi.impl.ElixirPsiImplUtil;
import org.elixir_lang.structure_view.element.modular.*;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static org.elixir_lang.psi.call.name.Function.*;
import static org.elixir_lang.psi.call.name.Module.KERNEL;
import static org.elixir_lang.psi.impl.ElixirPsiImplUtil.enclosingMacroCall;
public class CallDefinitionClause extends Element<Call> implements Presentable, Visible {
/*
* Constants
*/
/*
* Fields
*/
private final CallDefinition callDefinition;
@NotNull
private final Visibility visibility;
/*
* Public Static Methods
*/
/**
* Description of element used in {@link org.elixir_lang.FindUsagesProvider}.
*
* @param call a {@link Call} that has already been checked with {@link #is(Call)}
* @param location where the description will be used
* @return
*/
@Nullable
public static String elementDescription(@NotNull Call call, @NotNull ElementDescriptionLocation location) {
String elementDescription = null;
if (isFunction(call)) {
elementDescription = functionElementDescription(call, location);
} else if (isMacro(call)) {
elementDescription = macroElementDescription(call, location);
}
return elementDescription;
}
/**
* The module or {@code quote} that encapsulates {@code call}
*
* @param call a def(macro)?p?
* @return {@code null} if gets to the enclosing file without finding a quote or module
*/
@Contract(pure = true)
@Nullable
public static Modular enclosingModular(@NotNull Call call) {
Modular modular = null;
Call enclosingMacroCall = enclosingModularMacroCall(call);
if (enclosingMacroCall != null) {
modular = modular(enclosingMacroCall);
}
return modular;
}
/**
* The enclosing macro call that acts as the modular scope of {@code call}. Ignores enclosing {@code for} calls that
* {@link ElixirPsiImplUtil#enclosingMacroCall} doesn't.
*
* @param call a def(macro)?p?
*/
@Nullable
public static Call enclosingModularMacroCall(@NotNull Call call) {
Call enclosedCall = call;
Call enclosingMacroCall;
while(true) {
enclosingMacroCall = enclosingMacroCall(enclosedCall);
if (enclosingMacroCall != null &&
(enclosingMacroCall.isCalling(KERNEL, ALIAS) ||
enclosingMacroCall.isCallingMacro(KERNEL, FOR, 2))) {
enclosedCall = enclosingMacroCall;
} else {
break;
}
}
return enclosingMacroCall;
}
@Contract(pure = true)
@Nullable
public static Modular modular(@NotNull Call enclosingMacroCall) {
Modular modular = null;
// All classes under {@link org.elixir_lang.structure_view.element.Modular}
if (Implementation.is(enclosingMacroCall)) {
Modular grandScope = enclosingModular(enclosingMacroCall);
modular = new Implementation(grandScope, enclosingMacroCall);
} else if (Module.is(enclosingMacroCall)) {
Modular grandScope = enclosingModular(enclosingMacroCall);
modular = new Module(grandScope, enclosingMacroCall);
} else if (Protocol.is(enclosingMacroCall)) {
Modular grandScope = enclosingModular(enclosingMacroCall);
modular = new Protocol(grandScope, enclosingMacroCall);
} else if (Quote.is(enclosingMacroCall)) {
Call quoteEnclosingMacroCall = enclosingMacroCall(enclosingMacroCall);
Quote quote = null;
if (quoteEnclosingMacroCall == null) {
quote = new Quote(enclosingMacroCall);
} else if (CallDefinitionClause.is(quoteEnclosingMacroCall)) {
CallDefinitionClause callDefinitionClause = CallDefinitionClause.fromCall(quoteEnclosingMacroCall);
if (callDefinitionClause == null) {
Logger.error(
CallDefinitionClause.class,
"Cannot construct CallDefinitionClause from quote's enclosing macro call",
quoteEnclosingMacroCall
);
} else {
quote = new Quote(
callDefinitionClause,
enclosingMacroCall
);
}
} else {
quote = new Quote(modular(quoteEnclosingMacroCall), enclosingMacroCall);
}
if (quote != null) {
modular = quote.modular();
}
} else if (Unknown.is(enclosingMacroCall)) {
Modular grandScope = enclosingModular(enclosingMacroCall);
modular = new Unknown(grandScope, enclosingMacroCall);
}
return modular;
}
/**
* The head of the call definition.
*
* @param call a call that {@link #is(Call)}.
* @return element for {@code name(arg, ...) when ...} in {@code def* name(arg, ...) when ...}
*/
@Nullable
public static PsiElement head(@NotNull Call call) {
PsiElement[] primaryArguments = call.primaryArguments();
PsiElement head = null;
if (primaryArguments != null && primaryArguments.length > 0) {
head = primaryArguments[0];
}
return head;
}
public static boolean is(@NotNull Call call) {
return isFunction(call) || isMacro(call);
}
public static boolean isFunction(@NotNull Call call) {
return isPrivateFunction(call) || isPublicFunction(call);
}
public static boolean isMacro(@NotNull Call call) {
return isPrivateMacro(call) || isPublicMacro(call);
}
public static boolean isPrivateFunction(@NotNull Call call) {
return isCallingKernelMacroOrHead(call, DEFP);
}
public static boolean isPrivateMacro(@NotNull Call call) {
return isCallingKernelMacroOrHead(call, DEFMACROP);
}
public static boolean isPublicFunction(@NotNull Call call) {
return isCallingKernelMacroOrHead(call, DEF);
}
public static boolean isPublicMacro(@NotNull Call call) {
return isCallingKernelMacroOrHead(call, DEFMACRO);
}
@Nullable
public static PsiElement nameIdentifier(@NotNull Call call) {
PsiElement head = head(call);
PsiElement nameIdentifier = null;
if (head != null) {
nameIdentifier = CallDefinitionHead.nameIdentifier(head);
}
return nameIdentifier;
}
/**
* The name and arity range of the call definition this clause belongs to.
*
* @param call
* @return The name and arities of the {@link CallDefinition} this clause belongs. Multiple arities occur when
* default arguments are used, which produces an arity for each default argument that is turned on and off.
* @see Call#resolvedFinalArityRange()
*/
@Nullable
public static Pair<String, IntRange> nameArityRange(Call call) {
PsiElement head = head(call);
Pair<String, IntRange> pair = null;
if (head != null) {
pair = CallDefinitionHead.nameArityRange(head);
}
return pair;
}
/**
* Whether the {@code call} is defining something for runtime, like a function, or something for compile time, like
* a macro.
*
* @param call def(macro)?p?
* @return {@link Timed.Time#COMPILE} for defmacrop?; {@link Timed.Time#RUN} for defp?
*/
@NotNull
public static Timed.Time time(Call call) {
Timed.Time time = null;
if (isFunction(call)) {
time = Timed.Time.RUN;
} else if (isMacro(call)) {
time = Timed.Time.COMPILE;
}
//noinspection ConstantConditions
return time;
}
/**
* @param call
* @return {@code Visible.Visibility.PUBLIC} for {@code def} or {@code defmacro}; {@code Visible.Visibility.PRIVATE}
* for {@code defp} and {@code defmacrop}; {@code null} only if {@code call} is unrecognized
*/
@Nullable
public static Visible.Visibility visibility(Call call) {
Visible.Visibility callVisibility = null;
if (isPublicFunction(call) || isPublicMacro(call)) {
callVisibility = Visible.Visibility.PUBLIC;
} else if (isPrivateFunction(call) || isPrivateMacro(call)) {
callVisibility = Visible.Visibility.PRIVATE;
}
return callVisibility;
}
/*
* Private Static Methods
*/
@Nullable
private static String functionElementDescription(@NotNull Call call, @NotNull ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "function";
}
return elementDescription;
}
private static boolean isCallingKernelMacroOrHead(@NotNull final Call call, @NotNull final String resolvedName) {
return call.isCallingMacro(KERNEL, resolvedName, 2) ||
call.isCalling(KERNEL, resolvedName, 1);
}
private static String macroElementDescription(@NotNull Call call, @NotNull ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "macro";
}
return elementDescription;
}
/*
* Constructors
*/
/**
* Constructs {@link #callDefinition} from {@code code}, such as when showing structure in Go To Symbol
*
* @param call a def(macro)?p? call
*/
@Nullable
public static CallDefinitionClause fromCall(Call call) {
CallDefinition callDefinition = CallDefinition.fromCall(call);
CallDefinitionClause callDefinitionClause = null;
if (callDefinition != null) {
callDefinitionClause = new CallDefinitionClause(callDefinition, call);
}
return callDefinitionClause;
}
/**
* Constructs a clause for {@code callDefinition}.
*
* @param callDefinition holds all sibling clauses for {@code call} for the same name, arity. and time
* @param call a def(macro)?p? call
*/
public CallDefinitionClause(CallDefinition callDefinition, Call call) {
super(call);
this.callDefinition = callDefinition;
//noinspection ConstantConditions
this.visibility = visibility(call);
}
/*
* Public Instance Methods
*/
@NotNull
@Override
public TreeElement[] getChildren() {
Call[] childCalls = ElixirPsiImplUtil.macroChildCalls(navigationItem);
TreeElement[] children = childCallTreeElements(childCalls);
if (children == null) {
children = new TreeElement[0];
}
return children;
}
/**
* Returns the presentation of the tree element.
*
* @return the element presentation.
*/
@NotNull
@Override
public ItemPresentation getPresentation() {
PsiElement head = head(navigationItem);
return new org.elixir_lang.navigation.item_presentation.CallDefinitionHead(
(NameArity) callDefinition.getPresentation(),
visibility(),
head
);
}
/**
* The visibility of the element.
*
* @return {@code Visible.Visibility.PUBLIC} for public call definitions ({@code def} and {@code defmacro});
* {@code Visible.Visibility.PRIVATE} for private call definitions ({@code defp} and {@code defmacrop}).
*/
@NotNull
@Override
public Visibility visibility() {
return visibility;
}
/*
* Private Instance Methods
*/
private void addChildCall(List<TreeElement> treeElementList, Call childCall) {
TreeElement childCallTreeElement = null;
if (Implementation.is(childCall)) {
childCallTreeElement = new Implementation(callDefinition.getModular(), childCall);
} else if (Module.is(childCall)) {
childCallTreeElement = new Module(callDefinition.getModular(), childCall);
} else if (Quote.is(childCall)) {
childCallTreeElement = new Quote(this, childCall);
}
if (childCallTreeElement != null) {
treeElementList.add(childCallTreeElement);
}
}
@Contract(pure = true)
@Nullable
private TreeElement[] childCallTreeElements(@Nullable Call[] childCalls) {
TreeElement[] treeElements = null;
if (childCalls != null) {
List<TreeElement> treeElementList = new ArrayList<TreeElement>(childCalls.length);
for (Call childCall : childCalls) {
addChildCall(treeElementList, childCall);
}
treeElements = treeElementList.toArray(new TreeElement[treeElementList.size()]);
}
return treeElements;
}
/**
* Whether this clause would match the given arguments and be called.
*
* @param arguments argument being passed to this clauses' function.
* @return {@code true} if arguments matches up-to the available information about the arguments; otherwise,
* {@code false}
*/
public boolean isMatch(PsiElement[] arguments) {
return false;
}
}