/*
 Netjsendpacket.java - send packet handling of the VRML net java server

 Copyright (C) 2002    Peter Graf

 This file is part of VRMLNETJ - The VRML net java server.
 VRMLNETJ is free software.

    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

   For more information on the VRML net java server or Peter Graf,
   please see: http://www.mission-base.com/.

   $Log: vrmlnetj.java,v $

------------------------------------------------------------------------------
*/
//package vrmlnetj;

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * A class handling packets being sent, the class access is synchronized
 */
public class Netjsendpacket
{
    /**
     * The map packets that are sent are kept in for replay
     */
    private static Map table;

    /**
     * The socket used for all send operations
     */
    private static DatagramSocket dsocket;

    /**
     * The packet that really gets sent
     */
    private DatagramPacket packet;

    /**
     * The connection the packet is sent for
     */
    private Netjconn connection;

    /**
     * Times when the packet was created and when it was sent last
     */
    private long creationTime;
    private long lastSentTime;

    /**
     * Tag and packet Id used for packet
     */
    private String      tag;
    private String      PID;
    
    /**
     * buffer used for sending packets
     */
    private byte[] buffer;
    private int    offset;
    private int    dataoffset;
    private int    bufsize;

    /**
     * return the Connection ID / Packet ID pair for a packet
     */
    public String  getCidPid()
    {
        String s = connection.CID + "/" + PID;
        return( s );
    }

    /**
     * Create a new send packet
     */
    public Netjsendpacket(
        Netjconn conn,     /** connection the packet is created for */
        String t           /** tag ( RQ/AN ) used for packet header */
    )
    {
        // initialize the static fields, synchronized
        //
        if( dsocket == null )
        {
            newSocket();
        }
        if( table == null )
        {
            newTable();
        }

        // remember the connection the packet is meant for
        //
        connection = conn;

        // start with a 1 k buffer
        //
        buffer = new byte[ 1024 ];
        bufsize = 1024;
        offset = 0;

        // set the creation time of the packet
        //
        creationTime = System.currentTimeMillis();
        lastSentTime = 0;

        // set the tag if it is valid
        //
        if( t.equals( "RQ" ) || t.equals( "AN" ))
        {
            tag = new String( t );
        }
    }

    /**
     * set the packet ID for a packet
     */
    public void setPid( String p )
    {   
        PID = new String( p );
    }

    /**
     * set the header of a packet
     */
    private int setHeader()
    {
        byte[] barg = null;

        if( offset != 0 )
        {
            // header was already set
            //
            return( -1 );
        }

        if( tag == null || connection == null )
        {
            // a tag is needed in any case
            //
            return( -1 );
        }

        if( PID == null && tag.equals( "AN" ))
        {
            // answers can not contain an empty PID
            //
            return( -1 );
        }

        // set the tag to the packet
        //
        barg = tag.getBytes();
        if( barg.length > 2 )
        {
            return( -1 );
        }
        System.arraycopy( barg, 0, buffer, offset, barg.length );
        offset += barg.length;
        buffer[ offset++ ] = 0;

        // if the PID is empty the caller wants us to create a unique new one
        //
        if( PID == null )
        {
            String pid = null;
            String key = null;

            if( table == null )
            {
                return( -1 );
            }

            // synchronize the creation access to the table
            //
            synchronized( table )
            {
                // make a unique new packet id
                //
                for(;;)
                {
                    pid = vrmlnetj.randstr();
                    key = connection.CID + "/" + pid;

                    if( table.containsKey( key ))
                    {
                        continue;
                    }
                    vrmlnetj.log( "LOG", "NEW RQPA " + key );

                    table.put( key, this );
                    break;
                }
                PID = new String( pid );
            }
        }

        // set the PID to the packet
        //
        if( setString( PID ) < 0 )
        {
            return( -1 );
        }

        // set the CID to the packet
        //
        if( setString( connection.CID ) < 0 )
        {
            return( -1 );
        }

        dataoffset = offset;
        return( offset );
    }

    /**
     * set two strings to the packet to be sent
     */
    public int setString( String s1, String s2 )
    {
        int res = setString( s1 );
        if( res > 0 )
        {
           res = setString( s2 );
        }
        return( res );
    }

    /**
     * set a string to the packet to be sent
     */
    public int setString(
        String s          /** string to set to the packet      */
    )
    {
        if( s == null )
        {
            s = "";
        }

        if( offset < 1 )
        {
            if( setHeader() < 1 )
            {
                return( -1 );
            }
        }
        
        byte[] barg = s.getBytes();
        if( offset + barg.length >= buffer.length - 1 )
        {
            // create a larger buffer if needed
            //
            if(( bufsize < 32 * 1024 ) && ( offset + barg.length < 32 * 1024 ))
            {
                byte b[] = new byte[ 32 * 1024 ];
                System.arraycopy( buffer, 0, b, 0, buffer.length );
                buffer = b;
            }
            else
            {
                // tell the connection that the result size was bad
                //
                connection.sendBad( "SIZEOVERFLOW", "RESULT" );
                return( -1 );
            }
        }
        System.arraycopy( barg, 0, buffer, offset, barg.length );
        offset += barg.length;
        buffer[ offset++ ] = 0;

        return( offset );
    }

    /**
     * set bytes to the packet to be sent
     */
    public int setBytes( byte[] src, int srcPos, int length )
    {
        if( offset < 1 )
        {
            if( setHeader() < 1 )
            {
                return( -1 );
            }
        }

        if( offset + length >= buffer.length - 1 )
        {
            // create a larger buffer if needed
            //
            if(( bufsize < 32 * 1024 ) && ( offset + length < 32 * 1024 ))
            {
                byte b[] = new byte[ 32 * 1024 ];
                System.arraycopy( buffer, 0, b, 0, buffer.length );
                buffer = b;
            }
            else
            {
                return( -1 );
            }
        }
        System.arraycopy( src, srcPos, buffer, offset, length );
        offset += length;

        return( offset );
    }

    /**
     * Send a packet
     */
    public int send( )
    {
        // remember the last time we sent a packet to the connection
        //
        connection.setLastSentTime();

        // trace the packet sent
        //
        String debugstr = "";
        int start = 0;
        
        for( int i = 0; i < offset; i++ )
        {
            if( buffer[ i ] == 0 )
            {
                if( debugstr.length() > 0 )
                {
                    debugstr += " ";
                }
                    
                debugstr += new String( buffer, start, i - start );
                start = i + 1;
            }
        }

        vrmlnetj.log( "< ", connection.address.getHostName() + ":"
                            + connection.port + " " + debugstr );

        packet = new DatagramPacket( buffer, offset,
                                     connection.address, connection.port );

        // set the last sent time of the packet
        //
        lastSentTime = System.currentTimeMillis();

        udpSend( packet );

        return( 0 );
    }

    /**
     * Send a packet to a different connection
     */
    public int send( Netjconn conn )
    {
        // this is for sending a packet to another connection
        //
        Netjsendpacket newpacket = new Netjsendpacket( conn, this.tag );

        // copy the data bytes from the packet to the new one
        //
        newpacket.setBytes( buffer, dataoffset, offset - dataoffset );
        newpacket.send( );

        return( 0 );
    }

    /**
     * Handling of an answer received for a request
     */
    public static void answer( String cid, String pid )
    {
        if( table == null )
        {
            return;
        }

        // synchronize the table access
        //
        synchronized( table )
        {
            // remove the request from the table of requests
            //
            String id = cid + "/" + pid;

            vrmlnetj.log( "LOG", "DEL REQU " + id );
            table.remove( id );
        }
    }

    /**
     * Periodic handling of packets sent
     */
    public static int periodic()
    {
        if( table == null )
        {
            newTable();
        }
        if( dsocket == null )
        {
            newSocket();
        }

        // synchronize the table access
        //
        synchronized( table )
        {
            long now = System.currentTimeMillis();

            // run through all packets stored
            //
            Set s = table.keySet();
            HashSet expired = new HashSet();

            for( Iterator i = s.iterator(); i.hasNext(); )
            {
                String currkey = (String)i.next();
                Netjsendpacket current = (Netjsendpacket)table.get( currkey );

                // remove packets that do not have a tag at all
                //
                if( current.tag == null || null == current.connection.scene )
                {
                    vrmlnetj.log( "LOG", "TOT PACK " + current.getCidPid() );
                    expired.add( currkey );
                    continue;
                }

                // handle packets that have timed out
                //
                if( current.creationTime + 30000 < now )
                {
                    vrmlnetj.log( "LOG", "TOT PACK " + current.getCidPid() );
                    expired.add( currkey );
                    continue;
                }

                // if the packet has not been sent at all, we don't resend
                //
                if( current.lastSentTime < 1 )
                {
                    continue;
                }

                // handle packets that need to be resent
                //
                if( current.lastSentTime + 3000 < now )
                {
                    current.send( );
                }
            }

            // delete expired elements from the table
            //
            for( Iterator i = expired.iterator(); i.hasNext(); )
            {
                String currkey = (String)i.next();

                table.remove( currkey );
            }
        }

        return( 0 );
    }

    /**
     * send a UDP packet, synchronized
     */
    private synchronized void udpSend( DatagramPacket packet )
    {
        // Send the packet
        //
        try
        {
            dsocket.send( packet );
        }
        catch( Exception e ) { }
    }

    /**
     * Create the datagram socket, synchronized
     */
    private synchronized static void newSocket()
    {
        if( dsocket == null )
        {
            try
            {
                dsocket = new DatagramSocket();
            }
            catch (Exception e) { }
        }
    }

    /**
     * Create the packet table, synchronized
     */
    private synchronized static void newTable()
    {
        if( table == null )
        {
            table = new HashMap();
        }
    }
}
