/*
 * @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.experimental.UtilityClass;

import java.lang.reflect.AnnotatedType;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.List;

import net.ranides.assira.collection.arrays.NativeArrayBuilder;
import net.ranides.assira.lexer.StringTokenizer;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IContext;
import net.ranides.assira.reflection.IExecutable;
import net.ranides.assira.reflection.IReflectiveException;
import net.ranides.assira.reflection.util.ClassUtils;

/**
 * This is internal factory for IClass implementations.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@SuppressWarnings("unchecked")
@UtilityClass
public final class FClass {
    
    private static final FCache<IClass> CACHE = new FCache<>();

    /**
     * Creates IClass for provided type, using context for type resolution.
     *
     * @param context context
     * @param type type
     * @return IClass
     */
    public static IClass<?> newClass(IContext context, Class<?> type) {
        return CACHE.get(context, type, () -> new RClass<>(context, type));
    }

    /**
     * Creates IClass for provided type, using context for type resolution.
     *
     * @param context context
     * @param type type
     * @return IClass
     */
    public static IClass<?> newClass(IContext context, WildcardType type) {
        return CACHE.get(context, type, () -> new RWClass<>(context, type));
    }

    /**
     * Creates IClass for provided type, using context for type resolution.
     *
     * @param context context
     * @param type type
     * @return IClass
     */
    public static IClass<?> newClass(IContext context, TypeVariable<?> type) {
        return CACHE.get(context, type, () -> new RWClass<>(context, type));
    }

    /**
     * Creates IClass for provided annotated type, using context for type resolution.
     *
     * Loosely speaking, every field, parameter, or return class can be annotated.
     * We should use this method if we want to preserve information about type-annotations.
     *
     * @param context context
     * @param itype itype
     * @param atype atype
     * @return IClass
     */
    public static IClass<?> newClass(IContext context, Type itype, AnnotatedType atype) {
        if(itype instanceof GenericArrayType) {
            return FClass.newClass(context, (GenericArrayType)itype);
        } else {
            return CACHE.get(context, atype, () -> new RFClass<>(context, context.typeinfo(itype), atype));
        }
    }

    public static IClass<?> newArgumentClass(IExecutable method, IContext context, Type itype, AnnotatedType atype) {
        if(itype instanceof GenericArrayType) {
            return FClass.newClass(context, (GenericArrayType) itype);
        } else {
            return CACHE.get(context, atype, () -> newArgumentClass0(method, context, itype, atype));
        }
    }

    private static RFClass<?> newArgumentClass0(IExecutable method, IContext context, Type itype, AnnotatedType atype) {
        boolean isGenericMethodParam = method.params().stream().map(IClass::reflective).anyMatch(itype::equals);

        if(!isGenericMethodParam) {
            return new RFClass<>(context, context.typeinfo(itype), atype);
        }

        IClass<?> typeinfo = context.typeinfo(itype);
        if(typeinfo instanceof RWClass<?>) {
            typeinfo = ((RWClass<?>)typeinfo).swap();
        }

        return new RFClass<>(context, typeinfo, atype);
    }

    /**
     * Creates IClass for provided annotated type, using context for type resolution.
     *
     * Loosely speaking, every field, parameter, or return class can be annotated.
     * We should use this method if we want to preserve information about type-annotations.
     *
     * @param context context
     * @param itype itype
     * @param atype atype
     * @return IClass
     */
    public static IClass<?> newRFClass(IContext context, IClass<?> itype, AnnotatedType atype) {
        return CACHE.get(context, atype, () -> new RFClass<>(context, itype, atype));
    }

    /**
     * Creates IClass for provided parameterized (generic) type, using context for type resolution.
     *
     * @param context context
     * @param type type
     * @param ptype ptype
     * @param <R> R
     * @return IClass
     */
    public static <R> IClass<R> newClass(IContext context, Class<R> type, ParameterizedType ptype) {
        return CACHE.get(context, ptype, () -> new RPClass<>(context, type, ptype));
    }

    /**
     * Creates IClass for provided generic type, using "params" for type resolution.
     *
     * @param type type
     * @param params params
     * @param <R> R
     * @return IClass
     */
    public static <R> IClass<R> newClass(Class<R> type, IClass<?>[] params) {
        return new GContext(type, params).typeinfo(type);
    }

    /**
     * Creates IClass for provided parameterized (generic) type, using context for type resolution.
     *
     * It is very specific edge case, for generic arrays.
     *
     * @param context context
     * @param gtype gtype
     * @return IClass
     */
    public static IClass<?> newClass(IContext context, GenericArrayType gtype) {
        return new RGClass<>(context, gtype);
    }

    public static IClass<?> newClass(IContext context, String typename) {
        int iparam = typename.indexOf('<');
        if(iparam == -1) {
            return context.typeinfo(newSimpleClass(typename));
        } else {
            StringTokenizer tokenizer = new StringTokenizer(typename);
            return parseClassName(context, tokenizer);
        }
    }

    private static IClass<?> parseClassName(IContext context, StringTokenizer tokenizer) {
        tokenizer.skip(Character::isWhitespace);
        String typename = tokenizer.get(c -> Character.isJavaIdentifierPart(c) || c == '.');
        Class<?> type = newSimpleClass(typename);

        tokenizer.skip(Character::isWhitespace);
        if (tokenizer.peek() != '<') {
            return context.typeinfo(type);
        } else {
            tokenizer.skip();
            IClass<?>[] params = parseClassNames(context, tokenizer);
            return IClass.generic(type, params);
        }
    }

    private static IClass<?>[] parseClassNames(IContext context, StringTokenizer tokenizer) {
        NativeArrayBuilder out = new NativeArrayBuilder(IClass.class);

        while (true){
            out.value(parseClassName(context, tokenizer));

            if(tokenizer.end()) {
                throw new IllegalArgumentException("Expected > at the end of list");
            }
            char ch = tokenizer.get();
            if(ch == '>') {
                break;
            } else if(ch != ',') {
                throw new IllegalArgumentException("Expected , inside parameter list");
            }
        }

        return out.toArray();
    }

    private static Class<?> newSimpleClass(String typename) {
        return ClassUtils.forName(typename).orElseThrow(() -> new IReflectiveException("Unknown class name: " + typename));
    }

}
