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

package net.ranides.assira.reflection.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import net.ranides.assira.collection.maps.Cache;
import net.ranides.assira.collection.maps.MapCollectors;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.HashUtils;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IMethod;
import net.ranides.assira.text.StrBuilder;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class AnnotationUtils {
    
    private static final Method EQUALS = IClass.OBJECT.method("equals").get().reflective();
    
    private static final Method HASHCODE = IClass.OBJECT.method("hashCode").get().reflective();
    
    private static final Method TOSTRING  = IClass.OBJECT.method("toString").get().reflective();
    
    private static final Cache<Class<? extends Annotation>, Model> CACHE = new Cache<>();
    
    private AnnotationUtils() {
        /* utility class */
    }
    
    public static <A extends Annotation> A make(Class<A> type) {
        return make(type, Collections.emptyMap());
    }
    
    public static <A extends Annotation> A make(Class<A> type, Map<String, Object> params) {
        Model model = CACHE.get(type, () -> new Model(type));
        Handler handler = new Handler(model, params);
        return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler));
    }
    
    static final class Model {
        
        private final Class<?> type;
        
        private final List<IMethod> methods;
        
        private final Map<String,Object> values;

        public Model(Class<? extends Annotation> type) {
            this.type = type;
            this.methods = IClass.typeinfo(type)
                .methods()
                .require(IAttribute.DECLARED)
                .require(IAttribute.PUBLIC)
                .list();
            this.values = methods
                .stream()
                .collect(MapCollectors.unique(m -> m.name(), m -> m.reflective().getDefaultValue()));
        }

        public boolean compare(Object a, Object b) {
            if(!type.isInstance(a) || !type.isInstance(b)) {
                return false;
            }
            return methods.stream().allMatch(m -> CompareUtils.equals(m.apply(a), m.apply(b)));
        }
        
        public int hashcode(Object object) {
            int hc = 0;
            for(IMethod m : methods) {
                hc += 127*m.name().hashCode() ^ HashUtils.hash(m.apply(object));
            }
            return hc;
        }
        
        public String toString(Object object, Map<String,Object> params) {
            StrBuilder sb = new StrBuilder();
            sb
                .append('@')
                .append(type.getName())
                .open("(",")",", ")
                .options()
                .blank("null");
            for(String name : values.keySet()) {
                sb.item().append(name).append('=').append(get(name, params));
            }
            return sb.close().toString();
        }
        
        public Object get(String name, Map<String,Object> params) {
            if(null != params) {
                Object var = params.get(name);
                if(var!=null || params.containsKey(name)) {
                    return var;
                }
            }
            return values.get(name);
        }
        
    }
    
    private static class Handler implements InvocationHandler {
        
        private final Model model;
        private final Map<String,Object> map;

        public Handler(Model model, Map<String, Object> map) {
            this.model = model;
            this.map = map;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if(EQUALS.equals(method)) {
                return model.compare(proxy, args[0]);
            }
            if(HASHCODE.equals(method)) {
                return model.hashcode(proxy);
            }
            if(TOSTRING.equals(method)) {
                return model.toString(proxy, map);
            }
            return model.get(method.getName(), map);
        }
        
    }
    
}
