package mhfc.net.common.util.reflection;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
import mhfc.net.common.util.Comparation;
import mhfc.net.common.util.Comparation.ComparationResult;
import mhfc.net.common.util.parsing.exceptions.AmbiguousCallException;
public class Disambiguator {
private static enum MethodApplicability implements Comparable<MethodApplicability> {
/**
* �15.12.2.2 without permitting boxing or unboxing conversion, or the use of variable arity method invocation
*/
SUBTYPING,
/**
* �15.12.2.3 allowing boxing and unboxing, but still precludes the use of variable arity method invocation
*/
METHOD_INVOKATION_CONVERSION,
/**
* �15.12.2.4 combined with variable arity methods, boxing, and unboxing
*/
VARARG,
/**
* None of the above
*/
NOT_APPLICABLE;
}
private static final MethodHandles.Lookup lookup = MethodHandles.lookup();
protected static final MethodHandle throwingHandle;
static {
try {
throwingHandle = lookup.findStatic(
Disambiguator.class,
"throwAmbiguousCall",
MethodType.methodType(void.class, Class[].class, List.class, Object[].class));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unused")
private static void throwAmbiguousCall(Class<?>[] expressions, List<MethodInfo> methods, Object... objs) {
throw new AmbiguousCallException(
"Arguments: " + Arrays.toString(expressions) + " Possiblities: " + methods.toString());
}
/**
* �15.12.2.5 Determine most specific method<br>
* Determines if candidate is more specific that precedent.<br>
*
* @param candidate
* @param precedent
* @return
*/
protected static boolean isMoreSpecific(MethodInfo candidate, MethodInfo precedent) {
if (!candidate.isVarArgs) {
if (precedent.argTypes.length != candidate.argTypes.length) {
return false;
}
return IntStream.range(0, candidate.argTypes.length).allMatch(j -> {
Class<?> clazzT = candidate.argTypes[j];
Class<?> clazzU = precedent.argTypes[j];
return ClassHelper.isSubtype(clazzU, clazzT);
});
}
if (!precedent.isVarArgs) {
return false;
}
int argLength = Math.max(candidate.argTypes.length, precedent.argTypes.length);
return IntStream.range(0, argLength).allMatch(j -> {
Class<?> clazzT = candidate.getVarArgType(j);
Class<?> clazzU = precedent.getVarArgType(j);
return ClassHelper.isSubtype(clazzU, clazzT);
});
}
protected static int compareMostSpecific(MethodInfo left, MethodInfo right) {
return isMoreSpecific(left, right)
? (isMoreSpecific(right, left) ? 0 : 1)
: (isMoreSpecific(right, left) ? -1 : 0);
}
private MethodApplicability bestStrategy;
// A list of all currently applicable methods, ambiguous if more than one.
private List<MethodInfo> allCurrentBest;
// The classes of expressions used as to call
private final Class<?>[] expressionClasses;
public Disambiguator(Class<?>... args) {
this.expressionClasses = mhfc.net.common.util.Objects.requireNonNullDeep(args);
this.bestStrategy = MethodApplicability.NOT_APPLICABLE;
this.allCurrentBest = new ArrayList<>();
}
public Disambiguator considerAll(Iterable<MethodInfo> methods) {
methods.forEach(this::consider);
return this;
}
public Disambiguator consider(MethodInfo candidate) {
// �15.12.2
MethodApplicability applicability = determineApplicability(candidate);
ComparationResult apResult = Comparation.comparing(bestStrategy).to(applicability);
if (apResult.favorsRight()) {
return this;
}
if (apResult.favorsLeft()) {
this.bestStrategy = applicability;
this.allCurrentBest.clear();
this.allCurrentBest.add(candidate);
return this;
}
if (bestStrategy == MethodApplicability.NOT_APPLICABLE) {
return this;
}
Comparation<MethodInfo> candidateComp = Comparation
.comparing(candidate, (Comparator<MethodInfo>) Disambiguator::compareMostSpecific);
boolean isMostSpecific = true;
boolean isLeastSpecific = true;
for (MethodInfo info : allCurrentBest) {
ComparationResult specifityResult = candidateComp.to(info);
if (specifityResult.favorsRight()) {
isMostSpecific = false;
} else if (specifityResult.meansEquals()) {
isMostSpecific = false;
isLeastSpecific = false;
} else {
isLeastSpecific = false;
}
}
if (isMostSpecific) {
this.allCurrentBest.clear();
this.allCurrentBest.add(candidate);
return this;
}
if (isLeastSpecific) {
return this;
}
// Ambiguous
allCurrentBest.add(candidate);
return this;
}
protected MethodApplicability determineApplicability(MethodInfo info) {
if (expressionClasses.length == info.argTypes.length) {
// Possibly applicable by subtyping
MethodApplicability potentialApplicability = MethodApplicability.SUBTYPING;
for (int i = 0; i < expressionClasses.length; i++) {
Class<?> eClazz = expressionClasses[i], aClazz = info.argTypes[i];
if (ClassHelper.isSubtype(eClazz, aClazz)) {
continue;
}
if (ClassHelper.isMethodInvocationConvertible(eClazz, aClazz)) {
potentialApplicability = MethodApplicability.METHOD_INVOKATION_CONVERSION;
continue;
}
if (info.isVarArgs && i == expressionClasses.length - 1) {
if (ClassHelper.isMethodInvocationConvertible(eClazz, info.varArgCompType)) {
return MethodApplicability.VARARG;
}
}
return MethodApplicability.NOT_APPLICABLE;
}
return potentialApplicability;
}
if (!info.isVarArgs || expressionClasses.length < info.argTypes.length - 1) {
// Not enough parameters for either
return MethodApplicability.NOT_APPLICABLE;
}
// Last parameters must be variable arity
for (int i = 0; i < expressionClasses.length; i++) {
Class<?> exClass = expressionClasses[i];
Class<?> paClass = i >= info.argTypes.length - 1 ? info.varArgCompType : info.argTypes[i];
if (!ClassHelper.isMethodInvocationConvertible(exClass, paClass)) {
return MethodApplicability.NOT_APPLICABLE;
}
}
return MethodApplicability.VARARG;
}
protected MethodHandle getAmbiguityHandle() {
MethodHandle ret = throwingHandle.bindTo(expressionClasses).bindTo(new ArrayList<>(allCurrentBest))
.asVarargsCollector(Object[].class);
assert ret.isVarargsCollector();
return ret;
}
public Optional<MethodHandle> getCurrent() {
if (allCurrentBest.isEmpty() || bestStrategy == MethodApplicability.NOT_APPLICABLE) {
return Optional.empty();
}
if (allCurrentBest.size() == 1) {
return Optional.of(allCurrentBest.get(0).method);
}
return Optional.of(getAmbiguityHandle());
}
}