package mhfc.net.common.util.parsing.valueholders;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Array;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import mhfc.net.common.util.ExceptionLessFunctions;
import mhfc.net.common.util.parsing.Holder;
import mhfc.net.common.util.parsing.IValueHolder;
import mhfc.net.common.util.parsing.exceptions.FieldNotFoundException;
import mhfc.net.common.util.parsing.proxies.MemberMethodProxy;
import mhfc.net.common.util.reflection.FieldHelper;
import mhfc.net.common.util.reflection.MethodHelper;
import mhfc.net.common.util.reflection.OverloadedMethod;
import scala.actors.threadpool.Arrays;
/**
* Represents a member of an {@link IValueHolder}. This is dynamically determined based on the currently present origin
* in the origin.<br>
* The name of the field on the other hand is <b>not</b> dynamic.
*
* @author WorldSEnder
*/
public class MemberAccess implements IValueHolder {
private static interface IFieldAccess {
Holder get(Object instance) throws Throwable;
}
private static class FieldArrayLength implements IFieldAccess {
@Override
public Holder get(Object instance) {
Objects.requireNonNull(instance);
return Holder.valueOf(Array.getLength(instance));
}
}
private static class FieldArrayClone<T> implements IFieldAccess {
private final Class<? extends T[]> arrayClazz;
public FieldArrayClone(Class<? extends T[]> arrayClazz) {
assert arrayClazz.isArray();
this.arrayClazz = arrayClazz;
}
@Override
public Holder get(Object instance) {
T[] arr = arrayClazz.cast(instance);
return Holder.valueOf(Arrays.copyOf(arr, arr.length));
}
@SuppressWarnings("unchecked")
public static IFieldAccess forClass(Class<?> arrClass) {
assert arrClass.isArray();
@SuppressWarnings("rawtypes")
FieldArrayClone c = new FieldArrayClone(arrClass);
return c;
}
}
private static class FieldProxy<T> implements IFieldAccess {
private final MethodHandle field;
private final Class<?> fieldType;
private final Function<Object, Holder> rawToHolder;
public FieldProxy(MethodHandle f) {
this.field = Objects.requireNonNull(f);
this.fieldType = f.type().returnType();
this.rawToHolder = Holder.makeUnboxer(fieldType);
}
@Override
public Holder get(Object instance) {
return Holder.catching(Throwable.class, () -> {
Object fieldValue = this.field.invokeWithArguments(instance);
return rawToHolder.apply(fieldValue);
});
}
}
private static class FieldNotFound implements IFieldAccess {
private final Class<?> clazz;
private final String member;
public FieldNotFound(Class<?> clazz, String member) {
this.clazz = clazz;
this.member = member;
}
@Override
public Holder get(Object instance) {
throw new FieldNotFoundException(
"Can't get " + instance + "." + member + ": neither field '" + clazz.getName() + "." + member
+ "' nor special method 'Holder " + clazz.getName() + ".__getattr__(String)' found");
}
}
private static class MethodProxy implements IFieldAccess {
private final OverloadedMethod method;
public MethodProxy(OverloadedMethod methods) {
this.method = methods;
}
@Override
public Holder get(Object instance) {
return Holder.valueOf(new MemberMethodProxy(method.bindTo(instance)));
}
}
private static class SpecialAccessProxy implements IFieldAccess {
private final MethodHandle getattr;
private final String name;
private final Supplier<RuntimeException> error;
public SpecialAccessProxy(MethodHandle getter, String memberName) {
if (!getter.type().returnType().equals(Holder.class)) {
error = () -> new IllegalArgumentException("__getattr__ must return Holder");
} else {
error = null;
}
this.getattr = getter;
this.name = memberName;
}
@Override
public Holder get(Object instance) throws Throwable {
if (error != null) {
throw error.get();
}
return Holder.class.cast(getattr.invokeWithArguments(instance, name));
}
}
private static final IFieldAccess arrayLengthProxy = new FieldArrayLength();
private static Table<Class<?>, String, IFieldAccess> fieldCache;
private static LoadingCache<Class<?>, Boolean> proxyCache;
static {
fieldCache = HashBasedTable.create();
proxyCache = CacheBuilder.newBuilder().maximumSize(1000)
.removalListener(n -> fieldCache.rowKeySet().remove(n.getKey()))
.build(new CacheLoader<Class<?>, Boolean>() {
@Override
public Boolean load(Class<?> key) {
return Boolean.TRUE;
}
});
}
private static IFieldAccess resolveField(Class<?> clazz, String member) {
if (clazz.isPrimitive()) {
return new FieldNotFound(clazz, member);
}
proxyCache.getUnchecked(clazz); // Touch the cache for this class
IFieldAccess field = fieldCache.get(clazz, member);
if (field == null) {
field = computeFieldAccess(clazz, member);
fieldCache.put(clazz, member, field);
}
return field;
}
private static IFieldAccess computeFieldAccess(Class<?> clazz, String member) {
if (clazz.isArray()) {
if (member.equals("length")) {
return arrayLengthProxy;
}
if (member.equals("clone")) {
return FieldArrayClone.forClass(clazz);
}
}
Optional<MethodHandle> f = FieldHelper.find(clazz, member);
if (f.isPresent()) {
return new FieldProxy<>(f.get());
}
Optional<OverloadedMethod> m = MethodHelper.find(clazz, member);
if (m.isPresent()) {
return new MethodProxy(m.get());
}
Optional<OverloadedMethod> getattr = MethodHelper.find(clazz, "__getattr__");
Optional<MethodHandle> specialgetter = getattr.flatMap(o -> o.disambiguate(clazz, String.class));
if (specialgetter.isPresent()) {
return new SpecialAccessProxy(specialgetter.get(), member);
}
return new FieldNotFound(clazz, member);
}
private static Holder accessField(IFieldAccess field, Holder instance) throws Throwable {
return instance.ifValid(ExceptionLessFunctions.uncheckedFunction(inst -> {
return field.get(inst.boxed());
}));
}
public static IValueHolder makeMemberAccess(IValueHolder holder, String memberName) {
return new MemberAccess(holder, memberName);
}
private IValueHolder origin;
private String name;
private MemberAccess(IValueHolder object, String memberName) {
this.origin = object;
this.name = memberName;
}
@Override
public Holder snapshot() throws Throwable {
Holder holder = this.origin.snapshot();
return accessField(resolveField(holder.getType(), this.name), holder);
}
@Override
public String toString() {
return origin.toString() + "." + name;
}
}