// SapManager.java
// $Id: SapManager.java,v 1.2 1998/01/22 14:40:36 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.www.sap;

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

import org.w3c.tools.timers.*;

class Announce {
    SapManager engine   = null;
    long       interval = 60000;
    SapSession s        = null;
    Object     timer    = null;
    boolean    delete   = false ;
    int        dcount   = 0;

    /**
     * Set the announcement interval.
     * @param i The interval between two announces, or <strong>-1</strong>
     * if no announce is to be made.
     */

    synchronized void setInterval(long i) {
	this.interval = (((i > 0) && (i < SapManager.MIN_INTERVAL)) 
			 ? SapManager.MIN_INTERVAL 
			 : i);
	if ( interval > 0 ) {
	    if ( timer == null ) 
		engine.eventManager.registerTimer(interval, engine, this);
	} else {
	    if ( timer != null ) 
		engine.eventManager.recallTimer(timer);
	    timer = null;
	}
	    
    }

    /**
     * Get the announcement interval.
     * @return A long indicating the interval, or <strong>-1</strong> if that
     * session is not to be announced.
     */

    synchronized final long getInterval() {
	return interval;
    }

    /**
     * Reset any existing running timer for that session.
     */

    synchronized final void resetTimer() {
	if ( timer != null )
	    engine.eventManager.recallTimer(timer);
	timer = engine.eventManager.registerTimer(interval, engine, this);
    }

    final SapSession getSession() {
	return s;
    }

    /**
     * Transform this annonce into a deletion announcement.
     */

    final void announceDeletion() {
	this.delete = true;
	this.setInterval(SapManager.DELETE_INTERVAL);
	this.dcount = SapManager.DELETE_ANNOUNCE_COUNTER;
    }

    Announce(SapManager engine, SapSession s, long i) {
	this.engine = engine;
	this.s      = s;
	setInterval(i);
    }
}

class SapSessionEnumeration implements Enumeration {
    Enumeration e = null;

    public boolean hasMoreElements() {
	return e.hasMoreElements();
    }

    public Object nextElement() {
	return ((Announce) e.nextElement()).getSession();
    }

    SapSessionEnumeration(Enumeration e) {
	this.e = e;
    }
}

/**
 * The SAP engine will both announce sessions, and handle declared sessions.
 */

public class SapManager extends Thread implements EventHandler {
    /**
     * SAP default multicast socket address.
     */
    public static 
    InetAddress DEFAULT_ADDRESS = null;

    static {
	try {
	    DEFAULT_ADDRESS = InetAddress.getByName("224.2.127.254");
	} catch (Exception ex) {
	    System.err.println("Default address \"224.2.127.254\" unresolved");
	}
    }

    /**
     * SAP default port number.
     */
    public static final int DEFAULT_PORT = 9875;

    /**
     * SAP version handled by this implementation.
     */
    public static final int SAP_VERSION = 1;
    /**
     * Interval between deleion announcement:
     */
    public static final long DELETE_INTERVAL = 60000;
    /**
     * Number of deletion announcement to be sent by default.
     */
    public static final int DELETE_ANNOUNCE_COUNTER = 3;
    /**
     * Minimum announcement interval, in milliseconds.
     */
    public static final long MIN_INTERVAL = 60000;

    /**
     * Turn that class debug mode on.
     */
    private static final boolean debug = true;
    /**
     * Set of registered SAP managers.
     */
    private static Hashtable managers = null;
    /**
     * The SAP multicast socket.
     */
    protected MulticastSocket socket = null;
    /**
     * Our port number.
     */
    protected int port = -1;
    /**
     * List of announcements to make, keyed by session identifier.
     */
    protected Hashtable announces = null;
    /**
     * The event manager for handling announcements.
     */
    protected EventManager eventManager = null;
    /**
     * The announce handler.
     */
    protected SapHandler handler = null;

    // Internal buffer for emitting SAP packets:
    private byte aBuf[] = null;

    private final static int decodeInt(byte buf[], int off) {
	return (((buf[off] & 0xff) << 24)
		+ ((buf[off+1] & 0xff) << 16)
		+ ((buf[off+2] & 0xff) << 8)
		+ ((buf[off+3] & 0xff) << 0));
    }

    /**
     * Decode a SAP packet.
     * @param buf The buffer containing the packet.
     * @param off Offset of packet within above buffer.
     * @param len Length of packet.
     * @return A SapSession instance, or <strong>null</strong> if this was
     * a Session Description Deletion.
     */

    protected SapSession decode(byte buf[], int off, int len) {
	if ( len < 8 ) {
	    System.out.println("incomplete message !");
	    return null;
	}
	int w0 = decodeInt(buf, off); off += 4;
	System.out.println("SAP-header="+w0);
	// Print some w0 infos:
	int version     = ((w0 & 0xe0000000) >>> 29);
	int type        = ((w0 & 0x1c000000) >>> 26);
	boolean hasAuth = (((w0 & 0x02000000) >>> 25) == 1);
	boolean hasEnc  = (((w0 & 0x01000000) >>> 24) == 1);
	int authLen     = ((w0 & 0x00ff0000) >>> 16);
	int hash        = (((w0 & 0xff00) >>> 8)+(w0 &0xff));
	off += 4;
	// Get the text:
	if ( ! hasEnc ) {
	    if ( type == 0 ) {
		// Session Description Text:
		SapSession s = null;
		try {
		    s = new SapSession(buf, off+authLen, len-off-authLen);
		} catch (SapProtocolException ex) {
		    ex.printStackTrace();
		}
		if ( s != null )
		    return s;
	    } else if ( type == 1 ) {
		// Session Description Deletion:
		String str = new String(buf, off+authLen, len-off-authLen);
		// Get straight to the session identifier:
		StringTokenizer st  = new StringTokenizer(str, " ");
		long            sid = -1;
		try {
		    st.nextToken();		// skip username
		    sid = Long.parseLong(st.nextToken());
		} catch (Exception ex) {
		}
		// Mark the session as deleted:
		Announce a = (Announce) announces.get(new Long(sid));
		if ( a != null ) {
		    a.getSession().setDeleted(true);
		    if ( a.getInterval() > 0 ) {
			// Make sure we don't announce a closed session !
			if ( ! a.delete )
			    a.setInterval(-1);
		    }
		    if ( handler != null )
			handler.handleSapDeletion(a.getSession());
		}
	    }
	} else {
	    System.out.println("unsupported: encrypted announce");
	}
	return null;
    }

    /**
     * Get the default SAP manager for this application.
     * @return A SapManager instance.
     * @exception IOException If the manager couldn't create the multicast 
     * socket to join the SAP channel.
     */

    public synchronized static SapManager getManager()
	throws IOException
    {
	if ( DEFAULT_ADDRESS == null )
	    throw new IOException("no default address.");
	return getManager(DEFAULT_ADDRESS, DEFAULT_PORT);
    }

    /**
     * Get a SAP manager for the given address/port pair.
     * The same manager will be returned for the same pairs.
     * @param A The SAP Inet address.
     * @param p The SAP port.
     * @return A SapManager instance.
     * @exception IOException If the manager couldn't create the multicast 
     * socket to join the SAP channel.
     */

    public synchronized static SapManager getManager(InetAddress a , int p)
	throws IOException
    {
	byte b[] = a.getAddress();
	long lk  = ((((long) p) << 32)
		    + (((long) (b[0] & 0xff)) << 24)
		    + (((long) (b[1] & 0xff)) << 16)
		    + (((long) (b[2] & 0xff)) << 8)
		    + (((long) (b[3] & 0xff)) << 0));
	Long key = new Long(lk);
	if ( managers == null ) {
	    managers = new Hashtable(3);
	} else {
	    SapManager m = (SapManager) managers.get(key);
	    if ( m != null )
		return m;
	}	
	SapManager m = new SapManager(a, p);
	managers.put(key, m);
	return m;
    }

    /**
     * Create a blank session, to be filled out by the caller.
     * @param username Owner of the session.
     * @param sid Session identifier.
     * @param sversion Version of that session.
     * @param snettype Session network type.
     * @param saddrtype Session address type.
     * @param saddr Session address.
     * @param name Session name.
     */

    public SapSession createSession(String username,
				    long sid,
				    long sversion,
				    String snettype,
				    String saddrtype,
				    InetAddress saddr,
				    String name) {
	SapSession s = new SapSession();
	s.username  = username;
	s.sid       = sid;
	s.sversion  = sversion;
	s.snettype  = snettype;
	s.saddrtype = saddrtype;
	s.saddr     = saddr;
	s.name      = name;
	return s;
    }
	
    /**
     * EventHandler implementation - An annouce is to be made right now.
     * @param data The handler closure (a Announce instance).
     * @param time Current time.
     */

    public void handleTimerEvent(Object data, long time) {
	// Announce it this time:
	Announce a = (Announce) data;
	if ( debug ) 
	    System.out.println("Time to announce sid="+
			       a.getSession().getIdentifier());
	try {
	    announce(a);
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
	// Prepare for next announcement:
	if ( a.getInterval() > 0 )
	    a.timer = eventManager.registerTimer(a.getInterval()
						 , (EventHandler) this
						 , (Object) a);
	else
	    a.timer = null;
    }

    /**
     * Add the given session to the list of sessions to be announced.
     * @param s The session to be announced.
     * @param interval The interval between two announcements in milliseconds.
     */

    public void startAnnounce(SapSession s, long interval) {
	Long key = new Long(s.getIdentifier());
	// Has this session already been registered ?
	Announce a = (Announce) announces.get(key);
	if ( a != null ) {
	    a.setInterval(interval);
	    return;
	} else {
	    announces = new Hashtable(11);
	}
	a = new Announce(this, s, interval);
	announces.put(key, a);
    }

    /**
     * Delete, and announce deletion of a session.
     * @param s The session description to announce deletion of.
     */

    public void delete(SapSession s) {
	Long     key = new Long(s.getIdentifier());
	Announce a   = (Announce) announces.get(key);
	if ( a == null ) {
	    a = new Announce(this, s, DELETE_INTERVAL);
	    announces.put(new Long(s.getIdentifier()), a);
	    a.announceDeletion();
	} else {
	    a.announceDeletion();
	}
    }

    /**
     * Stop announcing the given session.
     * @param s The session whose periodic announce is to stop.
     */

    public void stopAnnounce(SapSession s) {
	Long key = new Long(s.getIdentifier());
	// Is there some announce running ?
	Announce a = (Announce) announces.get(key);
	if ( a != null )
	    a.setInterval(-1);
    }

    /**
     * Enumerate all registered sessions.
     * @return A Enumeration of SapSession instances.
     */

    public Enumeration enumerateSessions() {
	// We need to convert announce instances to SapSession instances
	return new SapSessionEnumeration(announces.elements());
    }

    /**
     * Make a session announcement.
     * @param a The announcement to be made.
     */

    
    protected void announce(Announce a) 
	throws IOException, UnknownHostException
    {
	// Dump the session to announce to a byte array:
	SapSession s   = a.getSession();
	byte       b[] = (a.delete ? s.getOwner() : s.toByteArray());
	// Allocate the announce buffer if needed (no two announce occur 
	// at same time, since they are serialized by the EvenetManager)
	// Create a datagram packet (no hurry)
	// Make sure the buffer is big enough (we don't support enc and auth):
	int len = 8 + b.length;
	if ((aBuf == null) || (aBuf.length < len))
	    aBuf = new byte[b.length+8];
	// Pack up the wire data:
	long w0 = (((SAP_VERSION & 0x7) << 29)		// SAP version
		   | (((a.delete ? 1 : 0) & 7) << 26)   // MT
		   | ((0 & 1)    << 25)			// Encrypted
		   | ((0 & 1)    << 24)			// Compressed
		   | ((0 & 0xff) << 16)			// AuthLen
		   | ((0 & 0xffff)));
	aBuf[0] = (byte) ((w0 & 0xff000000) >>> 24);
	aBuf[1] = (byte) ((w0 & 0x00ff0000) >>> 16);
	aBuf[2] = (byte) ((w0 & 0x0000ff00) >>> 8);
	aBuf[3] = (byte) ((w0 & 0xff0000ff) >>> 0);
	byte local[] = InetAddress.getLocalHost().getAddress();
	System.arraycopy(local, 0, aBuf, 4, 4);
	System.arraycopy(b, 0, aBuf, 8, b.length);
	DatagramPacket p = new DatagramPacket(aBuf, len
					      , socket.getLocalAddress()
					      , port);
	socket.send(p, (byte) (s.getTimeToLive() & 0xff));
	// If this was a deletion, and the deletion announce count is zero:
	if ( a.delete ) {
	    s.setDeleted(true);
	    System.out.println("delete-counter: "+a.dcount);
	    if ( --a.dcount <= 0 ) {
		a.setInterval(-1);
		announces.remove(new Long(a.getSession().getIdentifier()));
	    }
	}
    }


    public void run() {
	byte buffer[] = new byte[4096];
	try {
	    if ( debug ) 
		System.out.println("*** listening !!");
	    while ( true ) {
		DatagramPacket p = new DatagramPacket(buffer, 4096);
		socket.receive(p);
		if ( debug )
		    System.out.println("*** "+p.getLength()+" bytes:");
		// Parse and handle the announcement:
		SapSession s = decode(p.getData(), 0, p.getLength());
		if ( s == null )
		    continue;
		Long       k = new Long(s.getIdentifier());
		Announce   a = null;
		if ( announces != null ) 
		    a = (Announce) announces.get(k);
		if ( a != null ) {
		    // Reset it's timer, an announce occured in interval
		    a.resetTimer();
		} else {
		    // That's a brand new announce, handle it:
		    if ((handler != null) && handler.handleSapAnnounce(s))
			announces.put(k, new Announce(this, s, -1));
		}
	    }
	} catch (Exception ex) {
	    ex.printStackTrace();
	} finally {
	    System.out.println("Execution aborted !");
	}
    }

    SapManager(InetAddress addr, int port)
	throws IOException
    {
	this.announces = new Hashtable(23);
	// Create the event manager:
	this.eventManager = new EventManager();
	eventManager.start();
	// Connect to the well-known socket:
	this.port   = port;
	this.socket = ((port > 0) 
		       ? new MulticastSocket(port)
		       : new MulticastSocket());
	socket.joinGroup(addr);
	setName("SAP-Listener");
    }

    // Create a session for testing:

    public static SapSession testingSession(SapManager engine)
	throws UnknownHostException
    {
	InetAddress addr    = InetAddress.getByName("224.0.2.67");
	long        curtime = System.currentTimeMillis();
	SapSession s = engine.createSession("abaird"
					    , 12345678L
					    , 12345678L
					    , "IN"
					    , "IP4"
					    , addr
					    , "mICP - testing SAP (private)");
	s.addAttribute("type", "test");
	s.addPhone("04 93 65 79 89");
	s.addMail("abaird@w3.org");
	s.addInfo("Testing Java SAP implementation");
	s.addInfo("About to test mICP");
	s.addURL("http://www.w3.org/pub/WWW/People/Anselm");
	s.setConnectionData("IN", "IP4", addr, 1, 1);
	s.setTimes(curtime, curtime+(24L*3600L*10L*1000L));
	byte buf[] = s.toByteArray();
	System.out.println("Session To Announce:");
	System.out.print(new String(buf));
	return s;
    }

    public static void main(String args[]) {
	try {
	    InetAddress addr = InetAddress.getByName(args[0]);
	    int         port = Integer.parseInt(args[1]);
	    SapManager  sap  = SapManager.getManager();
	    SapSession  s    = testingSession(sap);
	    sap.start();
	    sap.startAnnounce(s, 60000);
	    // Wait for 30 seconds
	    System.out.println("Sleep for 30 seconds...."); System.out.flush();
	    Thread.currentThread().sleep(30000);
	    // Emit deletion announcements
	    System.out.println("deletion"); System.out.flush();
	    sap.delete(s);
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
    }

}
