package edu.neu.ccs.demeterf.http.server;

import java.lang.reflect.Method;
import edu.neu.ccs.demeterf.lib.*;
import edu.neu.ccs.demeterf.http.classes.*;

import java.net.Socket;

public class ServerDispatch {
    static void p(String s){ Factory.p(s); }
    
    private Map<String, Method> dispatch;
    private Object target;
    
    private ServerDispatch(Map<String, Method> dis, Object targ){
        dispatch = dis; target = targ;
    }
    
    /** Create a dispatcher, selecting methods with the default argument types. */
    public static ServerDispatch create(Object handler){
        return create(handler,DefaultFormals);
    }
    /** Create a dispatcher, selecting methods with the given formal argument types. */
    public static ServerDispatch create(Object handler, final Class<?>[] formals){
        Class<?> c = handler.getClass();
        if (!c.isAnnotationPresent(Server.class)) {
            throw Factory.error("Cannot Create Server for unannotated class '" +
                    c.getCanonicalName() + "'");
        }
        Server s = c.getAnnotation(Server.class);
        return new ServerDispatch(List.create(c.getDeclaredMethods()).fold(
                new List.Fold<Method, Map<String, Method>>(){
                    public Map<String, Method> fold(Method m, Map<String, Method> res){
                        if (!m.isAnnotationPresent(Path.class)) {
                            return res;
                        }
                        String p = check(m, formals);
                        m.setAccessible(true);
                        p("Mapping '" + p + "' to " + m.getName());
                        return res.put(p, m);
                    }
                }, Map.<String, Method> create()), handler);
    }
    /** Get the target/server handler object */
    public Object getTarget(){ return target; }
    /** Handle a given HTTP request using this dispatcher (no socket) */
    public HTTPResp handle(HTTPReq req){ return handle(req,null); }
    
    /** Handle a given HTTP request using this dispatcher, passing the
     *    server's socket */
    public HTTPResp handle(HTTPReq req, Socket sock){
        try {
            String path = req.trimmedUrl();
            p("HTTP Path: " + path);
            HTTPResp res;
            if (!dispatch.containsKey(path)){
                p("Unbound Path '"+path+"' Trying Default");
                path = Path.EMPTY;
            }
        
            Object[] args;
            if (!dispatch.containsKey(path)){
                p("No Default Path");
                res = HTTPResp.textError("Path Not Found");
            } else {
                Method m = dispatch.get(path);
                switch(m.getParameterTypes().length){
                case 0:args = new Object[] {};break;
                case 1:args = new Object[] { req };break;
                default:
                    if(sock == null)
                        throw Factory.error("Missing Expected Socket Argument");
                    args = new Object[] { req, sock };break;
                }
                res = (HTTPResp) m.invoke(target, args);
            }
            return res;
        }catch(IllegalAccessException e){
            return HTTPResp.textError(HTTPResp.MIN_SERVER_ERROR, "Server Error",
                    "Inaccessable Request Handler: "+e);
        }catch(java.lang.reflect.InvocationTargetException e){
            return HTTPResp.textError(HTTPResp.MIN_SERVER_ERROR, "Handler Exception",
                    "Exception in Request Handler: "+e.getCause());
        }
    }
    
    
    /** Minimal Parameters... */
    static Class<?>[] MinimalFormals = { HTTPReq.class };
    /** Allowed Parameters... */
    static Class<?>[] DefaultFormals = { HTTPReq.class, Socket.class };

    /** Do a sanity check of the types of an annotated method */
    public static String check(Method m, Class<?>[] formals){
        Path p = m.getAnnotation(Path.class);
        if (!HTTPResp.class.equals(m.getReturnType())) {
            throw Factory.error("Return Type is incorrect: '" + m.getReturnType().getClass() + "'");
        }
        Class<?>[] params = m.getParameterTypes();
        if (params.length > formals.length) {
            throw Factory.error("Too many Parameters for Server Method '" + m.getName() + "'");
        }

        checkParams(m.getName(), 0, params, formals);
        if(p == null)return null;
        return p.value();
    }

    /** Check that the parameters of the method are fine... */
    private static void checkParams(String method, int i, Class<?>[] ps, Class<?>[] fmls){
        if (i >= ps.length || i >= fmls.length)return;
        
        if (!fmls[i].isAssignableFrom(ps[i])) {
            throw Factory.error("Parameter #" + i + " of " + method + " is of an incorrect type. " + "Expecting '"
                    + fmls[i].getName() + "'");
        }
        checkParams(method, i+1, ps, fmls);
    }
}