/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2006 Phex Development Group
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  Created on May 11, 2005
 *  --- CVS Information ---
 *  $Id$
 */
package phex.udp;

import java.io.IOException;
import java.net.*;

import phex.common.PongCache;
import phex.common.ThreadTracking;
import phex.common.address.DefaultDestAddress;
import phex.common.address.DestAddress;
import phex.common.address.IpAddress;
import phex.connection.NetworkManager;
import phex.host.Host;
import phex.host.HostManager;
import phex.msg.*;
import phex.security.PhexSecurityManager;
import phex.utils.HexConverter;
import phex.utils.NLogger;
import phex.utils.NLoggerNames;

/**
 * @author Madhu
 */
public class UdpMessageEngine
{
    /**
     * Max udp packet size we will accept
     * as of now its small as we are dealing only with pings and pongs
     */
    public final static int MAX_PACKET_SIZE = 1024;
    
    /** 
     * HINT TO IMPLEMENTATION ABOUT THE SOCKET RECV BUFFER SIZE
     */ 
    public final static int MAX_RECV_BUFFER_SIZE = 16384;
    
    private DatagramSocket udpSocket;
    
    /** 
     * the send msg queue
     */
    private UdpMsgQueue sendQueue;
    
    /**
     * The guid routing table for pings
     */
    private UdpGuidRoutingTable pingRoutingTable;
    
    private final HostManager hostMgr;
    private final PhexSecurityManager securityManager;
        
    public UdpMessageEngine()
    {
        sendQueue = new UdpMsgQueue();
        
        securityManager = PhexSecurityManager.getInstance();
        hostMgr = HostManager.getInstance();
        
        //create the routing table with a lifetime of the 
        //udp send ping time interval....so a pong will only be accepted if
        //it comes within the period between 0 to 2 * lifetime
        pingRoutingTable = new UdpGuidRoutingTable( 
                UdpConnectionManager.UDP_PING_PERIOD );
        
        // bind a udp socket to the port on which we operate on
        int port = NetworkManager.getInstance().getLocalAddress().getPort();
        try
        {
            udpSocket = new DatagramSocket( port );
            udpSocket.setReceiveBufferSize( MAX_RECV_BUFFER_SIZE );
        }
        catch( SocketException e )
        {
            NLogger.warn( NLoggerNames.UDP_CONNECTION, " Couldnt bind to port "
                    + port, e );
            return;
        }

        // start receiver and sender threads.
        Thread thread = new Thread( ThreadTracking.rootThreadGroup, 
            new Reciever(), "UDP Message Receiver" );
        thread.start();
        
        thread = new Thread( ThreadTracking.rootThreadGroup, 
            new Sender(), "UDP Message Sender" );
        thread.start();        
    }
 
    /**
     * adds the message to the send queue
     * @param Msg
     * @param hostAddr
     * @return
     */
    public boolean addMessageToSend( Message msg, DestAddress hostAddr )
    {
        if( msg == null)
        {
            return false;
        }
        
        return sendQueue.addMessage( msg, hostAddr );
    }
    
    /**
     * removes a message from the queue and encapsulates it in
     * a datagram. It blocks if no message is available 
     * @return
     * a datagram on success or null on failure
     */
    private DatagramPacket getDatagramToSend()
    {
        UdpMsgQueue.QueueElement element;
        element = sendQueue.removeMessage();
        
        Message msg = element.getMsg();
        DestAddress address = element.getAddress();
        
//        if( !( netContainer.isConnectedToHost( address ) ) )
//        {
//          //dont send
//          return null;  
//        }
        
        byte[] data = null;
        
        if( msg instanceof PingMsg )
        {
            GUID guid = msg.getHeader().getMsgID();
            if( ! (pingRoutingTable.checkAndAddRouting( guid, address ) ) )
            {
                //could not add to routing table
                NLogger.warn( NLoggerNames.UDP_OUTGOING_MESSAGES,
                        " ping with duplicate guid not sent " + guid 
                        + " for message : " + msg );
                return null;
            }
            PingMsg ping = ( PingMsg )msg;
            data = ping.getBytes();
            
            NLogger.debug( NLoggerNames.UDP_OUTGOING_MESSAGES, 
                    " guid : " + guid + " successfully added to routing table for " +
                    		" udp ping : \n " + msg );
        }
        
        if( msg instanceof PongMsg )
        {
            PongMsg pong = ( PongMsg )msg;
            data = pong.getbytes();
        }
        
        if ( data == null )
        {
            return null;
        }
        
        try
        {
            InetAddress ipAddr;
            IpAddress ipAddress = address.getIpAddress();
            if ( ipAddress != null )
            {
                ipAddr = InetAddress.getByAddress( ipAddress.getHostIP() );
            }
            else
            {
                ipAddr = InetAddress.getByName( address.getHostName() );
            }
            int port = address.getPort();
            DatagramPacket packet = new DatagramPacket( data, data.length, 
                    ipAddr, port );
            NLogger.debug( NLoggerNames.UDP_OUTGOING_MESSAGES, " created udp datagram" +
                    " for msg " + msg + " \n to " + ipAddr );
            return packet;
        }
        catch( UnknownHostException e)
        {
            // just report
            NLogger.warn( NLoggerNames.UDP_OUTGOING_MESSAGES, 
                    " Could not create datagram  from message : " + msg, e );
        }
        // if it has reached here then packet is not created
        return null;
    }
    
    /**
     * reads a packet from the socket
     * @return
     * the packet or null if something went wrong
     */
    public DatagramPacket readMessage()
    {
        byte[] data = new byte[ MAX_PACKET_SIZE ]; 
        DatagramPacket packet = new DatagramPacket( data, data.length );
        
        try
        {
            udpSocket.receive( packet );
            return packet;
        }
        catch ( IOException e )
        {
            NLogger.warn( NLoggerNames.UDP_CONNECTION, 
                    " Could not read from udp socket " + udpSocket.getLocalSocketAddress(), e );
            return null;
        }
    }
    
    private void handlePing( MsgHeader header, byte[] body, Host fromHost )
    {
        PingMsg udpPing = null;
        try
        {
            udpPing = PingMsg.createUdpPingMsg( header, body, MsgHeader.DATA_LENGTH, fromHost );
            NLogger.debug( NLoggerNames.UDP_INCOMING_MESSAGES, " Recieved Udp Ping " +
            		"Msg From " + fromHost + " : " + udpPing );
            respondToPing( udpPing );
        }
        catch ( InvalidMessageException exp )
        {
            dropMessage( header, body, fromHost,
                "Invalid message: " + exp.getMessage() );
            NLogger.warn(NLoggerNames.UDP_INCOMING_MESSAGES, exp, exp );
        }
    }
    
    private void respondToPing( PingMsg udpPing )
    {
        PongMsg pong = PongMsg.createUdpPongMsg( udpPing );
        //add to send queue
        DestAddress address = udpPing.getHeader().getFromHost().getHostAddress(); 
        addMessageToSend( pong, address );
        NLogger.info( NLoggerNames.UDP_OUTGOING_MESSAGES,
                "added to send queue Udp Pong :" + pong + " \n \t to " + address );
    }
    
    private void handlePong( MsgHeader header, byte[] data, Host fromHost )
    {
        // first check if we had sent a ping to recieve a pong
        GUID guid = header.getMsgID();
        DestAddress address = pingRoutingTable.getAndRemoveRouting( guid ); 
        if( address == null )
        {
            // did not find routing for this pong
            NLogger.warn( NLoggerNames.UDP_INCOMING_MESSAGES, " Recieved Udp Pong " +
            		" with Guid not found in the routing table : " + header 
            		+ " \n \t Ignoring pong");
            return;
        }
        
        // thought of comparing the address in the table to the pong packet's address
        // but since its udp the packet can come from any interface of the packet's host
        // so just be happy that u sent a ping with the same guid
        
        PongMsg udpPong = null;
        try
        {
            udpPong = PongMsg.createUdpPongMsg( header, data, MsgHeader.DATA_LENGTH, fromHost );
            NLogger.debug( NLoggerNames.UDP_INCOMING_MESSAGES, " Recieved Udp Pong " +
            		"Msg From " + fromHost + " : " + udpPong );
        }
        catch ( InvalidMessageException e )
        {
            //just ignore 
            return;
        }
        
        byte[] pongIP = udpPong.getIP();
        byte access = securityManager.controlHostIPAccess( pongIP );
        if ( access == PhexSecurityManager.ACCESS_STRONGLY_DENIED )
        {
            // drop message
            // dropMessage( udpPong, "IP access strongly denied." );
            return;
        }

        // add address to host catcher...
        if ( access == PhexSecurityManager.ACCESS_GRANTED )
        {
            boolean isNew = hostMgr.getCaughtHostsContainer().addCaughtHost( udpPong );
            if ( isNew )
            {
                PongCache.getInstance().addPong( udpPong );
            }
        }
    }
    
    private void dropMessage( MsgHeader header, byte[] body, Host fromHost, String reason )
    {
        NLogger.info( NLoggerNames.IncomingMessages_Dropped, 
            "Dropping UDP message: " + reason + " from: " + fromHost );
        if ( NLogger.isDebugEnabled( NLoggerNames.UDP_INCOMING_MESSAGES ) )
        {
            NLogger.debug( NLoggerNames.IncomingMessages_Dropped,
                "Header: " + header.getDebugString() + " Body: " +
                " (" + HexConverter.toHexString( body, 0,
                header.getDataLength() ) + ")." );
        }
        // TODO should we count dropping udp? currently we dont
        // fromHost.incDropCount();
        // MessageCountStatistic.dropedMsgInCounter.increment( 1 );
    }
    
    
    /** 
     * Runs as a thread and sends messages that have been queued up.
     * @author Madhu
     */ 
    private class Sender implements Runnable
    {
        public void run()
        {
            while( true )
            {
                DatagramPacket packet = getDatagramToSend();
                
                if( packet == null )
                {
                    continue;
                }
                
                try
                {
                    udpSocket.send( packet );                    
                }
                catch ( IOException e )
                {
                    NLogger.warn( NLoggerNames.UDP_OUTGOING_MESSAGES, 
                            "Sending udp message " + packet + "failed ", e );
                }
            }
        }
    }
    
    /**
     * runs as a thread and picks up udp messages sent to us
     * @author Madhu
     */
    class Reciever implements Runnable
    {
        public void run()
        {
            DatagramPacket packet;
            while( true )
            {
                packet = readMessage();
                if( packet == null )
                {
                    continue;
                }
                
                byte[] packetData = packet.getData();
                int pktDataLength = packet.getLength();
                byte[] ip = packet.getAddress().getAddress();
                int port = packet.getPort();
                DestAddress address = new DefaultDestAddress( ip, port );
                
                //the data size shud be atleast a message's Header length 
                if( pktDataLength < MsgHeader.DATA_LENGTH )
                {
                    continue;
                }
                
//                //we only accept packets from hosts we are still connected to
//                //this is a sort of a crude restriction on the messages we can recieve 
//                if( ! ( netContainer.isConnectedToHost( address ) ) )
//                {
//                    continue;
//                }
                
                // TODO validate if the fromHost is a valid and acceptable source
                // we like to receive udp packages from.
                
                Host fromHost = hostMgr.getNetworkHostsContainer().getNetworkHost( address );
                if( fromHost == null )
                {
                    fromHost = new Host( address );
                }
                
                MsgHeader header = null;
                try
                {
                    header = MsgHeader.createMsgHeader( packetData, 0 );
                }
                catch ( Exception e )
                {
                    NLogger.warn( NLoggerNames.UDP_INCOMING_MESSAGES,
                            " Failed to create udp pong from datagram ", e );
                }
                
                // check if the header data length is valid 
                if( header.getDataLength() > ( pktDataLength - MsgHeader.DATA_LENGTH ) )
                {
                	NLogger.warn( NLoggerNames.UDP_INCOMING_MESSAGES,
                	 		" Msg Header Data length is invalid " );
                }
                
                header.countHop();
                
                // Now check the payload field and take appropriate action
                switch( header.getPayload() )
                {
                    case MsgHeader.PING_PAYLOAD :
                        NLogger.debug( NLoggerNames.UDP_INCOMING_MESSAGES, " Recvd Ping from : " + 
                                address );
                    	handlePing( header, packetData, fromHost );
                    	break;
                    case MsgHeader.PONG_PAYLOAD :
                        NLogger.debug( NLoggerNames.UDP_INCOMING_MESSAGES, " Recvd Pong from : " + 
                                address );
                    	handlePong( header, packetData, fromHost );
                    	break;
                    default:
                        NLogger.debug( NLoggerNames.UDP_INCOMING_MESSAGES, " Recvd unrecognized Msg from : " + 
                                address );
                    	break;
                }
            }
        }
    }
}
