/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */

package net.ranides.assira.reflection.impl;

import lombok.Data;
import lombok.experimental.UtilityClass;

import net.ranides.assira.collection.maps.Cache;
import net.ranides.assira.reflection.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * This is internal factory for IMethod implementations.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class FMethod {
    
    private static final FCache<IExecutable> D_METHODS = new FCache<>();
    private static final FCache<IExecutable> R_METHODS = new FCache<>();
    private static final Cache<FrameKey, IMethod> S_METHODS = Cache.getInstance();

    /**
     * Creates IMethod for provided reflective method using context for type resolution.
     * Please note, that there is distinction between declared and inherited fields.
     *
     * @param declared declared
     * @param context context
     * @param method method
     * @return IMethod
     */
    public static IMethod newMethod(boolean declared, IContext context, Method method) {
        return (IMethod)(declared ? D_METHODS : R_METHODS).get(context, method, () -> new RMethod(declared, context, method, null));
    }

    /**
     * Creates IMethod for provided declared reflective field.
     * It uses reflection to resolve declaring class (and context)
     *
     * @param method method
     * @return IMethod
     */
    public static IMethod newMethod(Method method) {
        IContext c = IClass.typeinfo(method.getDeclaringClass()).context();
        return FMethod.newMethod(true, c, method);
    }

    /**
     * Creates IMethod for method from provided stack frame.
     * It works only if method name is non-ambiguous.
     * Throws exception if method name points to overloaded methods.
     *
     * @param frame frame
     * @return IMethod
     */
    public static IMethod newMethod(StackTraceElement frame) {
        FrameKey key = new FrameKey(frame);
        return S_METHODS.get(key, () -> {
            return IClass.typeinfo(frame.getClassName())
                .methods()
                .require(IAttribute.DECLARED)
                .require(frame.getMethodName())
                .single()
                .<IReflectiveException>orElseThrow(() -> {
                    throw new IReflectiveException("Ambiguous method: " + frame.getClassName() + "#" + frame.getMethodName());
                });
        });
    }

    /**
     * Creates IConstructor for provided reflective constructor using context for type resolution.
     *
     * @param context context
     * @param method method
     * @param <T> T
     * @return IConstructor
     */
    @SuppressWarnings("unchecked")
    public static <T> IConstructor<T> newConstructor(IContext context, Constructor<T> method) {
        return (IConstructor)D_METHODS.get(context, method, () -> new RConstructor<>(context, method));
    }

    /**
     * Creates IConstructor for provided reflective constructor.
     * It uses reflection to resolve declaring class (and context)
     *
     * @param method method
     * @param <T> T
     * @return IConstructor
     */
    public static <T> IConstructor<T> newConstructor(Constructor<T> method) {
        IContext c = IClass.typeinfo(method.getDeclaringClass()).context();
        return FMethod.newConstructor(c, method);
    }

    @Data
    private static final class FrameKey {

        private final String clazz;
        private final String method;

        public FrameKey(StackTraceElement frame) {
            this.clazz = frame.getClassName();
            this.method = frame.getMethodName();
        }
    }
    
}
