/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.reflection.inspect;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import net.ranides.assira.collection.CollectionUtils;
import net.ranides.assira.generic.Predicate;
import net.ranides.assira.reflection.InspectException;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class ClassInspector {
    
    private static final Class[] EMPTY = new Class[0];
    
    private ClassInspector() {
        // utility class
    }
    
    /**
     * Próbuje utworzyć klasę o podanej nazwie za pomocą ClassLoadera
     * powiązanego z bieżącym wątkiem.
     * @param className
     * @return
     * @throws InspectException
     * @throws SecurityException
     */
    public static Class<?> forName(String className) throws InspectException {
        assert className != null;
        try {
            return Class.forName(className, true, Thread.currentThread().getContextClassLoader());
        } catch (ClassNotFoundException ex) {
            throw new InspectException(ex);
        }
    }
    
    public static Class<?> forObject(Object value) {
        return (value==null) ? void.class : value.getClass();
    }

    /**
     * Grupowa wersja funkcji {@code Class.forObject}.
     * Konwertuje tablicę obiektów na tablicę klas tych obiektów
     * @param values
     * @return
     */
    @SuppressWarnings("PMD.MethodReturnsInternalArray")
    public static Class<?>[] forObjects(Object... values) {
        if( values.length == 0) {
            return EMPTY;
        }
        Class<?> types[] = new Class<?>[values.length];
        for(int i=0; i<values.length; i++) {
            types[i] = forObject(values[i]);
        }
        return types;
    }
    
    /**
     * Zwraca listę wszystkich klas w łańcuchu dziedziczenia.
     * Pierwszy element listy to klasa przekazana jako argument.
     * Następny element to jej rodzic. I tak dalej - aż do napotkania na klasę
     * {@code Object}, która jest ostatnim elementem listy.
     * @param clazz
     * @return
     */
    public static List<Class<?>> getAncestors(Class<?> clazz) {
        LinkedList<Class<?>> list = new LinkedList<>();
        for(Class<?> c = clazz; c!=null; c=c.getSuperclass() ) {  // NOPMD
            list.addFirst(c);
        }
        return list;
    }

    /**
     * Zwraca listę wszystkich klas w łańcuchu dziedziczenia.
     * Pierwszy element listy to klasa przekazana jako argument.
     * Następny element to jej rodzic. I tak dalej - aż do napotkania na klasę
     * {@code root}, która jest ostatnim elementem listy.
     * @param <T>
     * @param clazz
     * @param root
     * @return
     */
    public static <T> List<Class<?>> getAncestors(Class<T> clazz, Class<? super T> root) {
        assert clazz != null;
        LinkedList<Class<?>> list = new LinkedList<>();
        // ograniczenia na argumentach gwarantują nam, że root jest rodzicem clazz
        // czyli nigdy nie natrafimy na getSuperrclass() == null
        for(Class<?> c = clazz; c!=root; c=c.getSuperclass() ) {  // NOPMD
            list.addFirst(c);
        }
        return list;
    }
    
    /**
     * Zwraca wszystkie klasy w łańcuchu dziedziczenia oraz wszystkie zaimplementowane
     * interfejsy. Kolejność zwracanych klas jest następująca:
     * <ul>
     *      <li>{@code clazz}</li>
     *      <li>interfejsy implementowane przez {@code clazz}</li>
     *      <li>{@code clazz.super}</li>
     *      <li>interfejsy implementowane przez {@code clazz.super}</li>
     *      <li>{@code clazz.super.super}</li>
     *      <li>...</li>
     *      <li>{@code Object.class}</li>
     * </ul>
     * @param <T>
     * @param clazz
     * @return
     */
    public static <T> Iterable<Class<?>> getAncestorTree(final Class<T> clazz) {
        return new Iterable<Class<?>>() {
            @Override
            public Iterator<Class<?>> iterator() {
                return new AncestorsIterator(clazz);
            }
        };
    }
    
    /**
     * Zwraca wszystkie interfejsy zaimplementowane przez klasę.
     * Kolejność zwracanych interfejsów jest następująca:
     * <ul>
     *      <li>{@code clazz} (jeśli interfejs)</li>
     *      <li>interfejsy implementowane przez {@code clazz}</li>
     *      <li>{@code clazz.super} (jeśli interfejs)</li>
     *      <li>interfejsy implementowane przez {@code clazz.super}</li>
     *      <li>{@code clazz.super.super} (jeśli interfejs)</li>
     *      <li>...</li>
     * </ul>
     * @param <T>
     * @param clazz
     * @return
     */
    public static <T> Iterable<Class<?>> getInterfaces(final Class<T> clazz) {
        return CollectionUtils.filter(getAncestorTree(clazz), new Predicate<Class<?>>() {
            @Override
            public Boolean apply(Class<?> source) {
                return source.isInterface();
            }
        });
    }
    
    private static class AncestorsIterator implements Iterator<Class<?>> {

        Class<?> clazz;
        Class<?>[] faces;
        int position;

        public AncestorsIterator(Class<?> clazz) {
            this.clazz = clazz;
            this.faces = clazz.getInterfaces();
            this.position = -1;
        }

        @Override
        public boolean hasNext() {
            if(clazz == null) {
                return false;
            }
            if(position >= faces.length && clazz.getSuperclass()==null) {
                return false;
            }
            return true;
        }

        @Override
        public Class<?> next() {
            if(position == -1) {
                position = 0;
                return clazz;
            }
            if(position >= faces.length) {
                clazz = clazz.getSuperclass();
                if(clazz != null) {
                    faces = clazz.getInterfaces();
                    position = 0;
                }
                return clazz;
            }
            return faces[position++];
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Immutable iterator.");
        }

    }
}
