package com.limegroup.gnutella.messages;

import java.io.IOException;
import java.net.SocketAddress;

import org.limewire.inspection.InspectablePrimitive;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.limegroup.gnutella.messages.Message.Network;
import com.limegroup.gnutella.messages.MessageFactory.MessageParser;
import com.limegroup.gnutella.messages.vendor.VendorMessageFactory;
import com.limegroup.gnutella.routing.RouteTableMessage;

@Singleton
public class MessageParserBinderImpl implements MessageParserBinder {

    private final PingReplyFactory pingReplyFactory;
    private final QueryReplyFactory queryReplyFactory;
    private final QueryRequestFactory queryRequestFactory;
    private final VendorMessageFactory vendorMessageFactory;
    private final PingRequestFactory pingRequestFactory;

    @InspectablePrimitive("bad hops messages")
    private static volatile long badHops;
    @InspectablePrimitive("bad ttl messages")
    private static volatile long badTTL;
    @InspectablePrimitive("high hops messages")
    private static volatile long highHops;
    @InspectablePrimitive("adjusted ttl messages")
    private static volatile long adjustings;
    @InspectablePrimitive("parsed messages")
    private static volatile long parsings;
    
    @Inject
    public MessageParserBinderImpl(PingReplyFactory pingReplyFactory,
            QueryRequestFactory queryRequestFactory,
            QueryReplyFactory queryReplyFactory,
            VendorMessageFactory vendorMessageFactory,
            PingRequestFactory pingRequestFactory) {
        this.pingReplyFactory = pingReplyFactory;
        this.queryRequestFactory = queryRequestFactory;
        this.queryReplyFactory = queryReplyFactory;
        this.vendorMessageFactory = vendorMessageFactory;
        this.pingRequestFactory = pingRequestFactory;
    }
    
    public void bind(MessageFactory messageFactory) {
        messageFactory.setParser(Message.F_PING, new PingRequestParser());
        messageFactory.setParser(Message.F_PING_REPLY, new PingReplyParser());
        messageFactory.setParser(Message.F_QUERY, new QueryRequestParser());
        messageFactory.setParser(Message.F_QUERY_REPLY, new QueryReplyParser());
        messageFactory.setParser(Message.F_PUSH, new PushRequestParser());
        messageFactory.setParser(Message.F_ROUTE_TABLE_UPDATE, new RouteTableUpdateParser());
        messageFactory.setParser(Message.F_VENDOR_MESSAGE, new VendorMessageParser());
        messageFactory.setParser(Message.F_VENDOR_MESSAGE_STABLE, new VendorMessageStableParser());
    }
    
    /**
     * An abstract class for Gnutella Message parsers
     */
    public static abstract class GnutellaMessageParser implements MessageParser {
        
        public Message parse(byte[] header, byte[] payload,
                Network network, byte max, SocketAddress address) throws BadPacketException, IOException {
            

            // enforce ttl + hops <= max.
            // except for PingReply messages
            
            byte func = header[16];
            byte ttl = header[17];
            byte hops = header[18];

            if (hops < 0) {
                badHops++;
                throw new BadPacketException("Negative (or very large) hops");
            } else if (ttl < 0) {
                badTTL++;
                throw new BadPacketException("Negative (or very large) TTL");
            } else if ((hops > max) 
                    && (func != Message.F_PING_REPLY)) {
                highHops++;
                throw new BadPacketException("func: " + func + ", ttl: " + ttl
                        + ", hops: " + hops);
            } else if ((ttl + hops > max) 
                    && (func != Message.F_PING_REPLY)) {
                adjustings++;
                ttl = (byte) (max - hops); // overzealous client;
                // readjust accordingly
                assert(ttl >= 0); // should hold since hops<=softMax ==>
                // new ttl>=0
            } else
                parsings++;

            // Delayed GUID allocation
            byte[] guid = new byte[16];
            System.arraycopy(header, 0, guid, 0, guid.length /* 16 */);
            
            return parse(guid, ttl, hops, payload, network);
        }
        
        protected abstract Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException;
    }
    
    private class PingRequestParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, byte[] payload, Network network)
                throws BadPacketException {
            return pingRequestFactory.createFromNetwork(guid, ttl, hops, payload, network);
        }
    }
    
    private class PingReplyParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException {
            return pingReplyFactory.createFromNetwork(guid, ttl, hops, payload, network);
        }
    }
    
    private class QueryRequestParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException {
            if (payload.length < 3) {
                throw new BadPacketException("Query request too short: " + payload.length);
            }
            
            return queryRequestFactory.createNetworkQuery(guid, ttl, hops, payload, network);
        }
    }
    
    private class QueryReplyParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException {
            if (payload.length < 26) {
                throw new BadPacketException("Query reply too short: " + payload.length);
            }
            
            return queryReplyFactory.createFromNetwork(guid, ttl, hops,
                    payload, network);
        }
    }
    
    private static class PushRequestParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException {
            return new PushRequestImpl(guid, ttl, hops, payload, network);
        }
    }
    
    private static class RouteTableUpdateParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException {
            // The exact subclass of RouteTableMessage returned depends on
            // the variant stored within the payload. So leave it to the
            // static read(..) method of RouteTableMessage to actually call
            // the right constructor.
            return RouteTableMessage.read(guid, ttl, hops, payload, network);
        }
    }
    
    private class VendorMessageParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException {
            return vendorMessageFactory.deriveVendorMessage(guid, ttl, hops, payload, network);
        }
    }
    
    private class VendorMessageStableParser extends GnutellaMessageParser {
        @Override
        protected Message parse(byte[] guid, byte ttl, byte hops, 
                byte[] payload, Network network) throws BadPacketException {
            return vendorMessageFactory.deriveVendorMessage(guid, ttl, hops, payload, network);
        }
    }
}
