package com.workshare.msnos.core.routing;

import java.io.IOException;
import java.util.Collection;
import java.util.Set;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.workshare.msnos.core.Cloud;
import com.workshare.msnos.core.Gateway;
import com.workshare.msnos.core.Identifiable;
import com.workshare.msnos.core.LocalAgent;
import com.workshare.msnos.core.Message;
import com.workshare.msnos.core.Receipt;
import com.workshare.msnos.core.RemoteAgent;
import com.workshare.msnos.core.cloud.MessageValidators;
import com.workshare.msnos.core.cloud.MessageValidators.Result;
import com.workshare.msnos.core.payloads.TracePayload;
import com.workshare.msnos.core.protocols.ip.Endpoint;
import com.workshare.msnos.core.protocols.ip.Endpoint.Type;
import com.workshare.msnos.core.protocols.ip.NullGateway;
import com.workshare.msnos.core.protocols.ip.http.HttpGateway;
import com.workshare.msnos.core.protocols.ip.udp.UDPGateway;
import com.workshare.msnos.core.protocols.ip.www.WWWGateway;
import com.workshare.msnos.core.receipts.SingleReceipt;

public class Router {

    public static final Gateway NOOP_GATE = new NullGateway();

    public static final String SYSP_MAXIMUM_HOPS_CLOUD = "com.ws.nsnos.core.router.hops.cloud.max";
    public static final String SYSP_MAXIMUM_HOPS_DIRECT = "com.ws.nsnos.core.router.hops.direct.max";
    public static final String SYSP_MAXIMUM_MESSAGES_PER_RING = "com.ws.nsnos.core.router.ring,messages.max";

    private static final Logger routing = LoggerFactory.getLogger("routing");
    private static final Logger logger = LoggerFactory.getLogger(Router.class);

    private final Cloud cloud;
    private final Gateway udpGate;
    private final Gateway wwwGate;
    private final Gateway httpGate;
    private final MessageValidators validators;
    
    private final Route[] routes;

    private static enum Mode {TXX, FWD};
    private final ThreadLocal<Mode> mode = new ThreadLocal<Router.Mode>();

    
    public Router(Cloud cloud, Set<Gateway> gates) {
        this(cloud, getGate(gates, UDPGateway.class), getGate(gates, HttpGateway.class), getGate(gates, WWWGateway.class));
    }

    Router(Cloud cloud, UDPGateway udpGate, HttpGateway httpGate, WWWGateway wwwGate) {
        this.cloud = cloud;
        this.udpGate = (udpGate != null ? udpGate : NOOP_GATE);
        this.wwwGate = (wwwGate != null ? wwwGate : NOOP_GATE);
        this.httpGate = (httpGate != null ? httpGate : NOOP_GATE);
        this.validators = cloud.validators();
        
        this.routes = new Route[] {
            new TerminalRouteOnZeroHops(this),
            new WWWRouteBroadcast(this),
            new CloudRouteBroadcast(this),
            new HTTPRouteDirect(this),
            new UDPRouteSameRing(this),
            new HTTPRouteViaRing(this),
            new UDPRouteBroadcast(this),
        };
    }

    Router(Cloud cloud, UDPGateway udpGate, HttpGateway httpGate, WWWGateway wwwGate, Route[] routes) {
        this.cloud = cloud;
        this.udpGate = (udpGate != null ? udpGate : NOOP_GATE);
        this.wwwGate = (wwwGate != null ? wwwGate : NOOP_GATE);
        this.httpGate = (httpGate != null ? httpGate : NOOP_GATE);
        this.validators = cloud.validators();
        this.routes = routes;
    }

    public Gateway udpGateway() {
        return udpGate;
    }
    
    public Gateway httpGateway() {
        return httpGate;
    }
    
    public Gateway wwwGateway() {
        return wwwGate;
    }
   
    public Receipt send(Message message) {
        mode.set(Mode.TXX);
        return route(message);
    }

    public Receipt forward(Message message)  {
        mode.set(Mode.FWD);

        final Result result = validators.isForwardable(message);
        if (result.success()) {
            return route(message);
        } else
            return skip(message, result.reason());
    }

    Receipt route(Message message) {
        logger.debug("Routing message {}", message);
 
        for (Route route : routes) {
            Receipt receipt = route.send(message);
            if (receipt != null) {
                if (logger.isDebugEnabled())
                    logger.debug("Message {} routed via {}, result is {}", message, route.getClass().getSimpleName(), receipt);

                return receipt;
            }
        }
        
        routing.info("{} {} {} {} {} {}", "N/A", "N/A", "NO-ROUTE", mode.get(), message);
        logger.warn("Unable to send message {} no route found", message);
        return SingleReceipt.failure(message);
    }

    Cloud cloud() {
        return cloud;
    }
 
    boolean hasRouteFor(RemoteAgent remote) {
        Set<Endpoint> endpoints = remote.getEndpoints(Type.HTTP);
        return endpoints.size() > 0;
    }

    Receipt sendViaWWW(Message message, String how)  {
        return this.send(message, null, message.getHops(), wwwGate, how);
    }
    
    Receipt sendViaUDP(Message message, int hops, String how)  {
        if (udpGate.name().equals(message.getReceivingGate())) {
            routing.info("{} {} {} {} {} {}", mode.get(), how, udpGate.name(), "UDP-TO-UDP", message);
            return SingleReceipt.failure(message);
        }
        
        return this.send(message, null, hops, udpGate, how);
    }

    Receipt sendViaHTTP(Message message, Identifiable to, int hops, String how)  {
        return this.send(message, to, hops, httpGate, how);
    }

    private Receipt send(Message message, Identifiable to, int hops, Gateway gate, String how) {
        
        if (message.getType() == Message.Type.TRC) {
            TracePayload payload = (TracePayload) message.getData();
            payload = payload.crumbed(findSource(), findDestination(to, message), gate, message.getHops());
            message = message.data(payload);
        }
        
        Receipt receipt;
        try {
            final Message hoppedMessage = message.withHops(hops);
            receipt = gate.send(cloud, hoppedMessage, to);
            routing.info("{} {} {} {} {} {} {}", mode.get(), how, gate.name(), receipt.getStatus(), hops, hoppedMessage);
        } catch (IOException e) {
            receipt = SingleReceipt.failure(message);
            routing.info("{} {} {} {} {} {}", mode.get(), how, gate.name(), "GATE-FAILURE", message);
            logger.warn("Unable to send message {} trough gateway {}", message, gate);
        }
        
        return receipt;
    }

    private UUID findDestination(Identifiable to, Message message) {
        if (to != null)
            return to.getIden().getUUID();
        else
            return message.getTo().getUUID();
    }

    private UUID findSource() {
        Collection<LocalAgent> locals = cloud.getLocalAgents();
        if (!locals.isEmpty()) 
            return locals.iterator().next().getIden().getUUID();
        else
            return cloud.getRing().uuid();
    }

    Receipt skip(Message message, String how) {
        routing.debug("{} {} {}", mode.get(), how, message);
        return SingleReceipt.failure(message);
    }
    
    Receipt terminal(Message message, String how) {
        routing.info("{} {} {} {}", mode.get(), how, "NONE", message);
        return SingleReceipt.failure(message);
    }
    
    @SuppressWarnings("unchecked")
    private static <T> T getGate(Set<? super T> gates, Class<T> clazz) {
        for (Object gate : gates) {
            if (gate.getClass() == clazz)
                return (T)gate;
        }
        
        return null;
    }
}
