package wayerr.reflect;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * Класс предоставляющий {@link Type} или {@link Class} по имени типа. Также
 * позволяет получить некоторую информацию о типе.
 *
 * @author rad
 */
public class TypeFactory {

    private static final Map<Class, String> m_primToName = new HashMap<Class, String>();
    private static final Map<String, Class> m_nameToPrim = new HashMap<String, Class>();

    static {
        // Карта соответсвий имен примитивных типов
        m_primToName.put(byte.class, "B");
        m_primToName.put(char.class, "C");
        m_primToName.put(double.class, "D");
        m_primToName.put(float.class, "F");
        m_primToName.put(int.class, "I");
        m_primToName.put(long.class, "J");
        m_primToName.put(short.class, "S");
        m_primToName.put(boolean.class, "Z");

        m_nameToPrim.put("B", byte.class);
        m_nameToPrim.put("C", char.class);
        m_nameToPrim.put("D", double.class);
        m_nameToPrim.put("F", float.class);
        m_nameToPrim.put("I", int.class);
        m_nameToPrim.put("J", long.class);
        m_nameToPrim.put("S", short.class);
        m_nameToPrim.put("Z", boolean.class);
    }

    //private Type m_type;
    private enum TypeClass {

        PRIMITIVE,
        ARRAY,
        CLASS,
        PARAMETRIZED
    }

    /**
     *
     * @param name - имя типа (возвращаемое {
     * @see Type.toString()} )
     */
    public TypeFactory(String name) throws ClassNotFoundException {
        //m_type = parse(name);
    }

    public static Type parse(String name) throws ClassNotFoundException {
        TypeClass tc = getTypeClass(name);
        Type t = null;
        switch(tc) {
            case PRIMITIVE:
                t = createPrimitive(name);
                break;
            case CLASS:
                t = createClass(name);
                break;
            case ARRAY:
                t = createArray(name);
                break;
            case PARAMETRIZED:
                t = createParametrized(name);
                break;

        }
        return t;
    }

    /**
     * Определяет класс типа
     *
     * @param name
     * @return
     */
    private static TypeClass getTypeClass(String name) {
        TypeClass tc = null;
        char start = name.charAt(0);
        char end = name.charAt(name.length() - 1);
        if(name.length() == 1) {//примитивный тип обозначенный одной буквой
            tc = TypeClass.PRIMITIVE;
        } else if(start == '[' || end == ']') { // массив
            tc = TypeClass.ARRAY;
        } else if(end == '>') { // параметризованный (напр List<String>)
            tc = TypeClass.PARAMETRIZED;
        } else { // либо класс, либо примитив обозначеннй словом (напр. int )
            if(name.indexOf(".") > -1 || Character.isUpperCase(start)) {
        // тип имеет полное обозначение или тип имеет краткое обозначение
                //и начинается с большой буквы, значит не примитив или примитив
                //в боксе напр. java.lang.Integer)
                tc = TypeClass.CLASS;
            } else {
                tc = TypeClass.PRIMITIVE;
            }
        }
        return tc;
    }

    private static Class<?> createPrimitive(String name) throws ClassNotFoundException {
        if(name.length() > 1) {
            name = getShortPrimName(name);
        }
        Class<?> t = m_nameToPrim.get(name);
        if(t == null) {
            throw new ClassNotFoundException("Can not load primitive type for name: " + name);
        }
        return t;
    }

    private static Class<?> createArray(String name) throws ClassNotFoundException {
        if(name.charAt(0) != '[') {
            //array имеет имя вроде java.lang.String[]
            name = name.substring(0, name.length() - 2);//обрезание скобок []
            String letter = (name.indexOf('.') > -1) ? "L" : getShortPrimName(name);
            name = "[" + letter + name + ";";
        }
        return createClass(name);
    }

    private static Class<?> createClass(String name) throws ClassNotFoundException {
        return Class.forName(name, false, TypeFactory.class.getClassLoader());
    }

    private static String getShortPrimName(String name) {
        return ("boolean".equals(name)) ? "Z" : name.substring(0, 1).toUpperCase(); //z - boolean
    }

    private static Type createParametrized(String name) throws ClassNotFoundException {
        int first = name.indexOf("<");
        int last = name.lastIndexOf(">");
        String raw = name.substring(0, first);
        ArrayList<Type> params = new ArrayList<Type>();
        String paramsSring = name.substring(first + 1, last);
        //парсинг
        char[] chars = paramsSring.toCharArray();
        int start = 0;
        for(int i = 0; i < chars.length; i++) {
            char ch = chars[i];
            if(ch == '<') {  // пропускаем до '>'
                int rec = 1;// означает вложенность "<>"
                while(rec > 0) { // пока не закроются все открытые '<'
                    ch = chars[++i];
                    if(ch == '<') {// если открывается '<'
                        ++rec; // то уровень вложенности увеличиваем
                    } else if(ch == '>') { // а если закрывается
                        --rec; //то уменьшаем
                    }
                }
            }
            if(ch == ',' || i == (chars.length - 1)) {
                //если запятая или конец строки то один параметр закончился
                String paramName;
                paramName = paramsSring.substring(start,
                        (i == (chars.length - 1)) ? chars.length : i);// если конец строки то до конца резать
                params.add(TypeFactory.parse(paramName));
                if(ch == ' ' || ch == ',') {
                    // пропускаем запятые и пробелы
                    while(i < chars.length) {
                        ch = chars[++i];
                        if(ch != ' ' && ch != ',') {
                            break;
                        }
                    }
                }
                start = i;
            }
        }
        Type[] pa = new Type[params.size()];
        return new ParameterizedTypeImpl(createClass(raw), params.toArray(pa), null);
    }

    /**
     * Скрывает внутри себя набор костыей позволяющих получит имя типа удобное
     * для сериализации FIXME найти более удобоваримые пути решения данной
     * проблемы
     *
     * @param type
     * @return
     */
    public static String getName(Type type) {
        StringBuilder typeName = new StringBuilder();
        if(type instanceof GenericArrayType) {
      //WARNING workaround для получения вменяемого имени типа
            // берется базовый тип и добавляется перфикс массива и ';'
            // без этого кода лоя массива строк будет возвращаться
            // "java.lang.String[]" когда нужно "[Ljava.lang.String;"
            Type srcType = ((GenericArrayType)type).getGenericComponentType();
      //WARNING рекурсия из-за того что исходный тип может оказаться классом \
            //или чем еще что также придется причесывать, возможно этого делать ненужно
            typeName.append("[");
            if(srcType instanceof Class) {//нужно добавить идентификатор типа
                Class c = (Class)srcType;
                if(!c.isPrimitive()) {
                    typeName.append("L");//вида [L<имя класса>;
                    typeName.append(c.getName());
                    typeName.append(";");
                } else {
                    typeName.append(m_primToName.get(c));
                }
            } else {
                typeName.append(getName(srcType));
            }
        } else if(type instanceof Class) {
      //WARNING workaround для получения вменяемого имени типа
            //позволяет избавится от префикса class перед именем типа
            return ((Class)type).getName();
        }/* else if (type instanceof ParameterizedType){
         ParameterizedType pt = (ParameterizedType) type;
         Type raw = pt.getRawType();
         typeName.append(getTypeName(raw));
         Type params[] = pt.getActualTypeArguments();
         for(Type p : params){
         typeName.append(' ');
         typeName.append(getTypeName(p));
         }
         } */ else {
            typeName.append(type.toString());
        }
        return typeName.toString();
    }

    public static Class<?> getClass(String typeName) throws ClassNotFoundException {
        TypeClass tc = getTypeClass(typeName);
        Class<?> t = null;
        switch(tc) {
            case PRIMITIVE:
                t = createPrimitive(typeName);
                break;
            case CLASS:
                t = createClass(typeName);
                break;
            case ARRAY:
                t = createArray(typeName);
                break;
            case PARAMETRIZED:
                int first = typeName.indexOf("<");
                String raw = typeName.substring(0, first);
                t = createClass(raw);
                break;
        }
        return t;
    }
}
