/*
 * @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.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import net.ranides.assira.collection.ArrayUtils;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.math.HashHelper;
import net.ranides.assira.reflection.GenericClass;
import net.ranides.assira.reflection.InspectException;
import net.ranides.assira.reflection.MethodFactory;
import net.ranides.assira.reflection.MethodFactory.ConstructorPointer;
import net.ranides.assira.reflection.MethodFactory.MethodPointer;
import net.ranides.assira.reflection.GenericMethod;
import net.ranides.assira.reflection.util.ClassUtils;
import net.ranides.assira.reflection.util.MethodUtils;
import net.ranides.strom.helpers.HashStrategy;
import net.ranides.strom.objects.CustomSet;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class MethodInspector<T> {
    
    private final Class<?> clazz;
    private List<Class<?>> follow = null;
    
    
    private MethodInspector(Class<?> root) {
        this.clazz = root;
        this.follow = Arrays.<Class<?>>asList(root);
    }
    
    public static <T> MethodInspector<T> inclass(Class<T> clazz) {
        return new MethodInspector(clazz);
    }
    
    public static MethodInspector<?> inclass(GenericClass clazz) {
        return new MethodInspector(clazz.rawType());
    }
    
    public static MethodInspector<?> inclass(String clazz) {
        return new MethodInspector(ClassInspector.forName(clazz));
    }
    
    public static <Q> MethodInspector<Q> inobject(Q object) {
        return new MethodInspector(ClassInspector.forObject(object));
    }

    @Override
    public int hashCode() {
        return HashHelper.hashValues(3, 97, clazz, follow);
    }

    @Override
    public boolean equals(Object object) {
        if (object == null) {
            return false;
        }
        if(object == this) {
            return true;
        }
        if (getClass() != object.getClass()) {
            return false;
        }
        final MethodInspector other = (MethodInspector) object;
        if (!ValueUtils.equals(this.clazz, other.clazz)) {
            return false;
        }
        if (!ValueUtils.equals(this.follow, other.follow)) {
            return false;
        }
        return true;
    }
    
    /**
     * All parent classes
     * @return 
     */
    public MethodInspector<T> follow() {
        follow = ClassInspector.getAncestors((Class)clazz);
        return this;
    }
    
    /**
     * all parent classess until "root"
     * @param root
     * @return 
     */
    public MethodInspector<T> follow(Class<?> root) {
        if( !root.isAssignableFrom(clazz) ) {
            throw new InspectException(root.getName() + " is not superclass of " + clazz.getName());
        }
        follow = ClassInspector.getAncestors((Class)clazz, (Class)root);
        return this;
    }
    
    /**
     * all parent classess until "root"
     * @param root
     * @return 
     */
    public MethodInspector<T> follow(GenericClass root) { 
        return follow(root.rawType());
    }
    
    /**
     * all parent classess until "root" class
     * @param root
     * @return 
     */
    public MethodInspector<T> follow(String root) { 
        return follow(ClassInspector.forName(root));
    }
    
    public ConstructorFilter<T> constructors() {
        return new ConstructorFilter(clazz);
    }
    
    public MethodFilter published() {
        return new PublicFilter(clazz);
    }
    
    public MethodFilter declared() {
        return new DeclaredFilter(follow);
    }
    
    @SuppressWarnings("PMD.TooManyDifferentMethods")
    public static abstract class Filter<Self> {
        
        protected int modifiersMust = 0;
        protected int modifiersNot = 0;
        protected Class[] annotations = null;
        protected Class[] params = null;
        protected boolean autoboxing = false;
        protected boolean covariant = false;
        protected boolean contravariant = false;
        
        @SuppressWarnings("unchecked")
        protected final Self self() {
            return (Self)this;
        }
        
        protected void init() {
            if(null == params) {
                return;
            }
            if(autoboxing) {
                for(int i=0; i<params.length; i++) {
                    params[i] = ClassUtils.box(params[i]);
                }
            }
        }
        
        protected final boolean checkModifiers(Member method) {
            int val = method.getModifiers();
            if( (val & modifiersMust) != modifiersMust ) {
                return false;
            }
            if( (val & modifiersNot) != 0 ) {
                return false;
            }
            return true;
        }
        
        protected final boolean checkAnnotations(AccessibleObject method) {
            if(null == annotations) {
                return true;
            }
            for(Class ann : annotations) {
                if( null == method.getAnnotation(ann) ) {
                    return false;
                }
            }
            return true;
        }
        
        protected final boolean checkParams(Class[] array) {
            if( null == params) {
                return true;
            }
            if( params.length != array.length) {
                return false;
            }
            for(int i=0; i<array.length; i++) {
                if( !checkParam(array[i], params[i])) {
                    return false;
                }
            }
            return true;
        }
        
        private boolean checkParam(Class current, Class req) {
            Class<?> orig = current;
            if(ValueUtils.equals(req, orig)) {
                return true;
            }
            if(autoboxing) {
               orig = ClassUtils.box(orig);
               if(ValueUtils.equals(req, orig)) {
                   return true;
               }
            }
            if(covariant && orig.isAssignableFrom(req)) {
                return true;
            }
            if(contravariant && req.isAssignableFrom(orig)) {
                return true;
            }
            return false;
        }
        
        public Self require(int modifiers) {
            modifiersMust |= modifiers;
            return self();
        }
        
        public Self discard(int modifiers) {
            modifiersNot |= modifiers;
            return self();
        }
        
        public Self annotated(Class<?> ... annotations) {
            if(null != this.annotations) {
                this.annotations = ArrayUtils.concat(this.annotations, annotations);
            } else {
                this.annotations = ArrayUtils.arraycopy(annotations);
            }
            return self();
        }
        
        public Self params() {
            this.params = new Class[0];
            return self();
        }
        
        public Self params(Class<?> ... params) {
            this.params = ArrayUtils.arraycopy(params);
            return self();
        }

        public Self params(GenericClass ... params) {
            this.params = new Class[params.length];
            for(int i=0; i<params.length; i++) {
                this.params[i] = params[i].rawType();
            }
            return self();
        }
        
        public Self paramsMatch(Object ... params) {
            this.params = ClassInspector.forObjects(params);
            this.covariant = true;
            this.autoboxing = true;
            return self();
        }
        
        public Self covariant() {
            this.covariant = true;
            return self();
        }
        
        public Self contravariant() {
            this.contravariant = true;
            return self();
        }

        public Self autoboxing() {
            this.autoboxing = true;
            return self();
        }
        
        public abstract List<? extends GenericMethod<?>> pointers();
        
        public abstract GenericMethod<?> first();

        public abstract int count();
        
        public abstract boolean exists();
        
    }
    
    public static abstract class MethodFilter extends Filter<MethodFilter> {
        
        protected String name = null;
        protected Class returns = null;
        
        public MethodFilter name(String name) {
            this.name = name;
            return self();
        }

        public MethodFilter returns(Class<?> clazz) {
            this.returns = clazz;
            return self();
        }

        public MethodFilter returns(GenericClass clazz) {
            return returns(clazz.rawType());
        }

        @Override
        protected void init() {
            super.init();
            if(null == returns) {
                return;
            }
            if(autoboxing) {
                returns = ClassUtils.box(returns);
            }
        }
        
        protected final boolean checkReturns(Method method) {
            if(null == returns) {
                return true;
            }
            Class<?> orig = method.getReturnType();
            if(ValueUtils.equals(returns,orig)) {
                return true;
            }
            if(autoboxing) {
               orig = ClassUtils.box(orig);
               if(ValueUtils.equals(returns,orig)) {
                   return true;
               }
            }
            if(covariant &&  returns.isAssignableFrom(orig)) {
                return true;
            }
            if(contravariant &&  orig.isAssignableFrom(returns)) {
                return true;
            }
            return false;
        }
        
        protected final boolean checkName(Method method) {
            if(null == name) {
                return true;
            }
            return name.equals(method.getName());
        }
        
        @Override
        public abstract List<? extends MethodPointer<?>> pointers();
        
        public abstract Collection<Method> reflective();
        
        @Override
        public abstract MethodPointer<?> first();

        @Override
        public abstract int count();
        
        @Override
        public final boolean exists() {
            return null != first();
        }
        
    }
    
    public static class ConstructorFilter<R> extends Filter<ConstructorFilter<R>> {
        
        private final Class<R> clazz;

        public ConstructorFilter(Class<R> clazz) {
            this.clazz = clazz;
        }

        @Override
        public List<ConstructorPointer<R>> pointers() {
            init();
            List<ConstructorPointer<R>> ret = new ArrayList<>();
            for(Constructor ctor : clazz.getDeclaredConstructors()) {
                if( check(ctor) ) {
                    ret.add(MethodFactory.wrap(ctor));
                }
            }
            return ret;
        }
        
        public Collection<Constructor<R>> reflective() {
            init();
            List<Constructor<R>> ret = new ArrayList<>();
            for(Constructor ctor : clazz.getDeclaredConstructors()) {
                if( check(ctor) ) {
                    ret.add(ctor);
                }
            }
            return ret;
        }

        @Override
        public ConstructorPointer<R> first() {
            init();
            for(Constructor ctor : clazz.getDeclaredConstructors()) {
                if( check(ctor) ) {
                    return MethodFactory.wrap(ctor);
                }
            }
            return null;
        }

        @Override
        public int count() {
            init();
            int ret = 0;
            for(Constructor ctor : clazz.getDeclaredConstructors()) {
                if( check(ctor) ) {
                    ret++;
                }
            }
            return ret;
        }
        
        @Override
        public boolean exists() {
            return null != first();
        }
        
        private boolean check(Constructor<?> ctor) {
            if( ctor.isSynthetic() ) {
                return false;
            }
            if( !checkModifiers(ctor) ) {
                return false;
            }
            
            if( !checkParams(ctor.getParameterTypes()) ) {
                return false;
            }
            if( !checkAnnotations(ctor) ) {
                return false;
            }
            return true;
        }

    }

    private static class PublicFilter extends MethodFilter {
        
        private final Class<?> clazz;

        public PublicFilter(Class<?> clazz) {
            this.clazz = clazz;
        }

        @Override
        public List<MethodPointer<?>> pointers() {
            init();
            List<MethodPointer<?>> ret = new ArrayList<>();
            for(Method method : clazz.getMethods()) {
                if( check(method) ) {
                    ret.add(MethodFactory.wrap(method));
                }
            }
            return ret;
        }
        
        @Override
        public Collection<Method> reflective() {
            init();
            List<Method> ret = new ArrayList<>();
            for(Method method : clazz.getMethods()) {
                if( check(method) ) {
                    ret.add(method);
                }
            }
            return ret;
        }

        @Override
        public MethodPointer first() {
            init();
            for(Method method : clazz.getMethods()) {
                if( check(method) ) {
                    return MethodFactory.wrap(method);
                }
            }
            return null;
        }

        @Override
        public int count() {
            init();
            int ret = 0;
            for(Method method : clazz.getMethods()) {
                if( check(method) ) {
                    ret++;
                }
            }
            return ret;
        }
        
        private boolean check(Method method) {
            if( method.isSynthetic() || method.isBridge() ) {
                return false;
            }
            if( !checkName(method) ) {
                return false;
            }
            if( !checkModifiers(method) ) {
                return false;
            }
            if( !checkReturns(method) ) {
                return false;
            }
            if( !checkParams(method.getParameterTypes()) ) {
                return false;
            }
            if( !checkAnnotations(method) ) {
                return false;
            }
            return true;
        }

    }
    
    private static class DeclaredFilter extends MethodFilter {
        
        private final List<Class<?>> list;

        public DeclaredFilter(List<Class<?>> list) {
            this.list = list;
        }

        @Override
        public List<MethodPointer<?>> pointers() {
            init();
            CustomSet<Method> set = HashMethods.set();
            List<MethodPointer<?>> ret = new ArrayList<>();
            for(Class<?> item : list) {
                for(Method method : item.getDeclaredMethods()) {
                    if( set.add(method) && check(method) ) {
                        ret.add(MethodFactory.wrap(method));
                    }
                }
            }
            return ret;
        }
        
        @Override
        public Collection<Method> reflective() {
            init();
            CustomSet<Method> set = HashMethods.set();
            for(Class<?> item : list) {
                for(Method method : item.getDeclaredMethods()) {
                    if( check(method) ) {
                        set.add(method);
                    }
                }
            }
            return set;
        }

        @Override
        public MethodPointer first() {
            init();
            for(Class<?> item : list) {
                for(Method method : item.getDeclaredMethods()) {
                    if( check(method) ) {
                        return MethodFactory.wrap(method);
                    }
                }
            }
            return null;
        }

        @Override
        public int count() {
            init();
            CustomSet<Method> set = HashMethods.set();
            int ret = 0;
            for(Class<?> item : list) {
                for(Method method : item.getDeclaredMethods()) {
                    if( set.add(method) && check(method) ) {
                        ret++;
                    }
                }
            }
            return ret;
        }
        
        private boolean check(Method method) {
            if( !checkName(method) ) {
                return false;
            }
            if( !checkModifiers(method) ) {
                return false;
            }
            if( !checkReturns(method) ) {
                return false;
            }
            if( !checkParams(method.getParameterTypes()) ) {
                return false;
            }
            if( !checkAnnotations(method) ) {
                return false;
            }
            return true;
        }

    }
    
    private static class HashMethods implements HashStrategy<Method> {
        
        private static final HashMethods INSTANCE = new HashMethods();
        
        public static CustomSet<Method> set() {
            return new CustomSet<>(INSTANCE);
        }

        @Override
        public int hashCode(Method method) {
            return method.getName().hashCode() + Arrays.hashCode(method.getParameterTypes());
        }

        @Override
        public boolean equals(Method var1, Method var2) {
            return MethodUtils.overrides(var2, var1);
        }
        
    }
}
