// SapSession.java
// $Id: SapSession.java,v 1.1 1997/02/25 11:02:46 abaird Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.www.sap;

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

public class SapSession {
    private final static long ntp_offset = 2208988800L;
    /**
     * The SDP version understood by this class.
     */
    public static final int SDP_VERSION = 0;
    /**
     * SDP version currently used.
     */
    protected int version = SDP_VERSION;
    /**
     * SAP user name.
     */
    String username = null;
    /**
     * SAP session identifier.
     */
    protected long sid = -1;
    /**
     * SAP session version.
     */
    protected long sversion = -1;
    /**
     * SAP session network type.
     */
    protected String snettype = null;
    /**
     * SAP session address type.
     */
    protected String saddrtype = null;
    /**
     * SAP session address.
     */
    protected InetAddress saddr = null;
    /**
     * SAP session name.
     */
    protected String name = null;
    /**
     * SAP session informations.
     */
    protected String infos[] = null;
    /**
     * SAP associated URL.
     */
    protected String urls[] = null;
    /**
     * SAP owner email address.
     */
    protected String emails[] = null;
    /**
     * SAP owner email address.
     */
    protected String phones[] = null;
    /**
     * SAP connection network type.
     */
    String cnettype = null;
    /**
     * Does this SAP session has asssociated connection data ?
     */
    protected boolean hasConnection = false;
    /**
     * SAP connection address type.
     */
    protected String caddrtype = null;
    /**
     * SAP connection address.
     */
    protected InetAddress caddr = null;
    /**
     * SAP connection time to live.
     */
    protected int cttl = 1;
    /**
     * SAP number of multicast groups.
     */
    protected int cnaddr = 1;
    /**
     * SAP Conference Total bandwidth in kb per sec.
     */
    protected int ctbw = -1;
    /**
     * SAP Application Specific bandwidth in kb per sec.
     */
    protected int asbw = -1;
    /**
     * SAP session start time (NTP).
     */
    protected long startTime = -1;
    /**
     * SAP session stop time.
     */
    protected long stopTime = -1;
    /**
     * SAP attributes.
     */
    protected SapAttributes attrs = null;
    /**
     * SAP medias.
     */
    protected Vector medias = null;
    /**
     * Has this session been deleted ?
     */
    protected boolean deleted = false;

    /**
     * Convert that session description to a String.
     * @return A String instance
     */

    public String toString() {
	byte b[] = toByteArray();
	return new String(b);
    }

    /**
     * Add a media to that session.
     * @param media The media to add to this session.
     */
    
    protected void addMedia(SapMedia media) {
	medias.addElement(media);
    }

    /**
     * Generate a buffer to announce that session.
     * @return A byte array containing the formatted data to be emited on the
     * wire.
     */

    public byte[] toByteArray() {
	StringBuffer sb = new StringBuffer();
	// Version:
	sb.append("v="); sb.append(version); 
	sb.append('\n');
	// Owner ship:
	sb.append("o="); 
	sb.append(username); 
	sb.append(' '); sb.append(sid);
	sb.append(' '); sb.append(sversion);
	sb.append(' '); sb.append(snettype);
	sb.append(' '); sb.append(saddrtype);
	sb.append(' '); sb.append(saddr.getHostAddress());
	sb.append('\n');
	// Session name:
	sb.append("s="); sb.append(name);
	sb.append('\n');
	// Session infos:
	if ( infos != null ) {
	    for (int i = 0 ; i < infos.length ; i++) {
		sb.append("i="); sb.append(infos[i]);
		sb.append('\n');
	    }
	}
	// URI for session
	if ( urls != null ) {
	    for (int i = 0 ; i < urls.length ; i++) {
		sb.append("u="); sb.append(urls[i]);
		sb.append('\n');
	    }
	}
	// Owner's email 
	if ( emails != null ) {
	    for (int i = 0 ; i < emails.length ; i++) {
		sb.append("e="); sb.append(emails[i]); 
		sb.append('\n');
	    }
	}
	// Owner's phone number:
	if ( phones != null ) {
	    for (int i = 0 ; i < phones.length ; i++) {
		sb.append("p="); sb.append(phones[i]); 
		sb.append('\n');
	    }
	}
	// Connection data:
	if ( hasConnection ) {
	    sb.append("c="); 
	    sb.append(cnettype);
	    sb.append(' '); sb.append(caddrtype);
	    sb.append(' '); sb.append(caddr.getHostAddress()); 
	    sb.append('/'); sb.append(cttl);
	    if ( cnaddr > 1 ) {
		sb.append('/'); sb.append(cnaddr);
	    }
	    sb.append('\n');
	}
	// Bandwidth:
	if ( ctbw > 0 ) {
	    sb.append("b=CT:"); sb.append(ctbw); 
	    sb.append('\n');
	} 
	if ( asbw > 0 ) {
	    sb.append("b=AS:"); sb.append(asbw);
	    sb.append('\n');
	}
	    
	
	// Attributes:
	attrs.dump(sb);

	// Time:
	if ((startTime >= 0) && (stopTime >= 0)) {
	    sb.append("t="); 
	    sb.append((startTime/1000)+ntp_offset);
	    sb.append(' '); sb.append((stopTime/1000)+ntp_offset);
	    sb.append('\n');
	}
		    
	// Dump all available medias:
	for (int i = 0 ; i < medias.size() ; i++) {
	    byte bits[] = ((SapMedia) medias.elementAt(i)).toByteArray();
	    sb.append(new String(bits));
	}
	// Convert that to byte array
	String s = sb.toString();
	return s.getBytes();
    }

    private void warning(String msg) {
	System.err.println(msg);
    }

    private void error(String msg) 
	throws SapProtocolException
    {
	throw new SapProtocolException(msg);
    }
    
    private void error(Exception ex, String msg) 
	throws SapProtocolException
    {
	error(msg+" \""+ex.getMessage()+"\"");
    }


    private String[] aggregate(String into[], String value) {
	String s[]    = null;
	int    offset = 0;
	if ( into != null ) {
	    offset = into.length;
	    s      = new String[offset+1];
	    System.arraycopy(into, 0, s, 0, offset);
	} else {
	    s = new String[1];
	}
	s[offset] = value;
	return s;
    }

    private int checkValid(int op, int tag, boolean strict)
	throws SapProtocolException
    {
	switch(op) {
	  case 'v':
	      if (strict && (tag >= 0))
		  error("unexpected 'v' attribute");
	      return 0;
	  case 'o':
	      if (strict && (tag >= 1))
		  error("unexpected 'o' attribute");
	      return 1;
	  case 's':
	      if (strict && (tag >= 2))
		  error("unexpected 's' attribute");
	      return 2;
	  case 'i':
	      if (strict && (tag > 3))
		  error("unexpected 'i' attribute");
	      return 3;
	  case 'u':
	      if (strict && (tag > 4))
		  error("unexpected 'u' attribute");
	      return 4;
	  case 'e':
	      if (strict && (tag > 5))
		  error("unexpected 'e' attribute");
	      return 5;
	  case 'p':
	      if (strict && (tag > 6))
		  error("unexpected 'p' attribute");
	      return 6;
	  case 'c':
	      if (strict && (tag > 7))
		  error("unexpected 'c' attribute");
	      return 7;
	  case 'b':
	      if (strict && (tag > 8))
		  error("unexpected 'b' attribute");
	      return 8;
	  case 'z':
	      if (strict && (tag > 9))
		  error("unexpected 'z' attribute");
	      return 3;
	  case 'k':
	      if (strict && (tag > 10))
		  error("unexpected 'k' attribute");
	      return 10;
	  case 'a':
	      if (strict && (tag > 11))
		  error("unexpected 'a' attribute");
	      return 11;
	  default:
	      return tag;
	}
    }

    private int checkValid(int op, int tag)
	throws SapProtocolException
    {
	return checkValid(op, tag, false);
    }

    /**
     * Read next line of SAP stream.
     * @return A String containing the next line of input.
     * @exception IOException If IO error when reading from stream.
     */

    public void parse(InputStream input) 
	throws IOException, SapProtocolException
    {
	BufferedReader  in    = (new BufferedReader
				 (new InputStreamReader(input)));
	String          l     = null;
	StringTokenizer st    = null;
	int             field = -1;
	SapMedia        media = null;	// The one being parsed

	while ((l = in.readLine()) != null) {
	    switch(l.charAt(0)) {
	      case 'v':
		  field = checkValid('v', field);
		  // Parse the SAP version number:
		  try {
		      this.version = Integer.parseInt(l.substring(2));
		  } catch (NumberFormatException ex) {
		      error(ex, "invalid version number \""+l+"\"");
		  }
		  break;
	      case 'o':
		  field = checkValid('o', field);
		  st = new StringTokenizer(l.substring(2), " ");
		  try {
		      username  = st.nextToken();
		      sid       = Long.parseLong(st.nextToken());
		      sversion  = Long.parseLong(st.nextToken());
		      snettype  = st.nextToken();
		      saddrtype = st.nextToken();
		      saddr     = InetAddress.getByName(st.nextToken());
		  } catch (Exception ex) {
		      error(ex, "invalid owner ship \""+l+"\"");
		  }
		  break;
	      case 's':
		  field = checkValid('s', field);
		  name = l.substring(2);
		  break;
	      case 'i':
		  if ( media != null ) {
		      media.addInfos(l.substring(2));
		  } else {
		      field = checkValid('i', field);
		      addInfo(l.substring(2));
		  }
		  break;
	      case 'u':
		  field = checkValid('u', field);
		  addURL(l.substring(2));
		  break;
	      case 'e':
		  field  = checkValid('e', field);
		  addMail(l.substring(2));
		  break;
	      case 'p':
		  field  = checkValid('p', field);
		  addPhone(l.substring(2));
		  break;
	      case 'c':
		  field = checkValid('c', field);
		  st = new StringTokenizer(l.substring(2), " /");
		  try {
		      String      ctype = st.nextToken();
		      String      atype = st.nextToken();
		      InetAddress addr = InetAddress.getByName(st.nextToken());
		      int cttl         = Integer.parseInt(st.nextToken());
		      int cnaddr       = (st.hasMoreTokens()
					  ? Integer.parseInt(st.nextToken())
					  : 1);
		      if ( media == null ) {
			  hasConnection = true;
			  this.cnettype  = ctype;
			  this.caddrtype = atype;
			  this.caddr     = addr;
			  this.cttl      = cttl;
			  this.cnaddr    = cnaddr;
		      } else {
			  media.setConnectionData(ctype, atype, addr
						  , cttl, cnaddr);
		      }
		  } catch (Exception ex) {
		      error(ex, "invalid connection data \""+l+"\"");
		  }
		  break;
	      case 'b':
		  field = checkValid('b', field);
		  st = new StringTokenizer(l.substring(2), " :");
		  try {
		      while (st.hasMoreTokens()) {
			  String modifier = st.nextToken();
			  int    bw       = Integer.parseInt(st.nextToken());
			  if ( modifier.equalsIgnoreCase("CT") ) {
			      if ( media == null )
				  this.ctbw = bw;
			      else
				  media.setConferenceTotalBandwidth(bw);
			  } else if ( modifier.equalsIgnoreCase("AS")) {
			      if ( media == null ) {
				  this.asbw = bw;
			      } else {
				  media.setApplicationSpecificBandwidth(bw);
			      }
			  } else {
			      // Ignore
			  }
		      } 
		  } catch (Exception ex) {
		      error(ex, "invalid bandwidth spec \""+l+"\"");
		  }
		  break;		      
	      case 'z':
		  // FIXME
		  warning(l+" ignored");
		  break;
	      case 'k':
		  warning(l+" ignored");
		  break;
	      case 'a':
		  field = checkValid('a', field);
		  st = new StringTokenizer(l.substring(2), ":");
		  try {
		      String key = st.nextToken();
		      String val = null;
		      if ( st.hasMoreTokens() )
			  val = st.nextToken();
		      if ( media == null ) {
			  attrs.addAttribute(key, val);
		      } else {
			  media.addAttribute(key, val);
		      }
		  } catch (Exception ex) {
		      error(ex, "unable to parse attribute \""+l+"\"");
		  }
		  break;
	      case 't':
		  st = new StringTokenizer(l.substring(2), " ");
		  try {
		      startTime = Long.parseLong(st.nextToken());
		      stopTime  = Long.parseLong(st.nextToken());
		      // Normalize values:
		      startTime = (startTime-ntp_offset)*1000;
		      stopTime  = (stopTime-ntp_offset)*1000;
		  } catch (Exception ex) {
		      error(ex, "invalid time spec \""+l+"\"");
		  }
		  break;
	      case 'r':
		  warning(l+" ignored");
		  break;
	      case 'm':
		  st = new StringTokenizer(l.substring(2), " ");
		  try {
		      // Top-level parsing:
		      String sm = st.nextToken();
		      String sp = st.nextToken();
		      String tr = st.nextToken();
		      Vector vf = new Vector();
		      while ( st.hasMoreTokens() )
			  vf.addElement(st.nextToken());
		      // Copy vector into array:
		      String farray[] = new String[vf.size()];
		      vf.copyInto(farray);
		      // Port parsing:
		      st     = new StringTokenizer(sp, "/");
		      int p  = Integer.parseInt(st.nextToken());
		      int np = (st.hasMoreTokens()
				? Integer.parseInt(st.nextToken())
				: 1);
		      media = new SapMedia(this, sm, p, np, tr, farray);
		      addMedia(media);
		  } catch (Exception ex) {
		      error(ex, "unable to parse media \""+l+"\"");
		  }
		  break;
	    }
	}
	return;
    }

    /**
     * Parse the given packet as a single session announcement.
     * @param p The DatagramPacket to parse.
     * @exception SapProtocolException If given data doesn't conform to SDP.
     * @see SDP
     */

    protected void parse(byte buf[], int off, int len) 
	throws SapProtocolException
    {
	ByteArrayInputStream in = new ByteArrayInputStream(buf, off, len);
	try {
	    parse(in);
	} catch (IOException ex) {
	    System.err.println("IO problems against byte stream !");
	}
    }

    /**
     * Set this session deletion status.
     * @param onoff The toggle value.
     */

    protected void setDeleted(boolean onoff) {
	this.deleted = onoff;
    }

    /**
     * Get that session uniq identifier.
     * @return A long uniq session identifier.
     */

    public long getIdentifier() {
	return sid;
    }

    /**
     * Enumerate all medias available on that session.
     * @return An Enumeration instance.
     */

    public Enumeration enumerateMedias() {
	return medias.elements();
    }

    /**
     * Add a media description to that session.
     * @param media The media name.
     * @param port The port number.
     * @param nport The number of ports used.
     * @param transport The name of the transport used.
     * @param formats List of formats used.
     * @return A SapMedia instance.
     */

    public SapMedia createMedia(String media
				, int port
				, int nport
				, String transport
				, String formats[]) {
	SapMedia m = new SapMedia(this
				  , media
				  , port, nport
				  , transport
				  , formats);
	addMedia(m);
	return m;
    }

    /**
     * Remove that media from the session.
     * @param m The SapMedia to remove from the session description.
     */

    public void removeMedia(SapMedia m) {
	medias.removeElement(m);
    }

    /**
     * Add an attribute to that media description.
     * @param key The name of the attribute.
     * @param val The value for that attribute.
     */

    public void addAttribute(String key, String val) {
	attrs.addAttribute(key, val);
    }

    /**
     * Remove an attribute definition.
     * Removes all values for that attribute.
     * @param key The attribute name.
     */

    public void removeAttribute(String key) {
	attrs.removeAttribute(key);
    }

    /**
     * Get the first value of an attribute.
     * @param key The attribute name.
     * @return A String instance, or <strong>null</strong>.
     */

    public String getAttributeValue(String key) {
	return attrs.getValue(key);
    }

    /**
     * Get all mappings of an attribute.
     * @param key The name of the attribute.
     * @return A String array, which might have a zero length (to indicate
     * a <i>flag</i>), or <strong>null</strong>.
     */

    public String[] getAttributeValues(String key) {
	return attrs.getValues(key);
    }

    /**
     * Add a phone location.
     * @param phone The new phone location.
     */

    public void addPhone(String phone) {		  
	phones = aggregate(phones, phone);
    }

    /**
     * Add an email location.
     * @param email The new email location.
     */

    public void addMail(String email) {
	emails = aggregate(emails, email);
    }

    /**
     * Add more info.
     * @param info The info to be added.
     */

    public void addInfo(String info) {
	infos = aggregate(infos, info);
    }

    /**
     * Add an URL for the session.
     * @param url The URL.
     */

    public void addURL(String url) {
	urls  = aggregate(urls, url);
   }

    /**
     * Set this session's connection data.
     * @param ctype The connection network type.
     * @param atype The address type.
     * @param addr The address for the session itself.
     * @param ttl The TTL for the session.
     * @param naddr Number of following reserved address.
     */

    public void setConnectionData(String ctype, String atype
				  , InetAddress addr
				  , int ttl, int naddr) {
	hasConnection = true;
	this.cnettype  = ctype;
	this.caddrtype = atype;
	this.caddr     = addr;
	this.cnaddr    = cnaddr;
    }

    /**
     * Get the max TTL for that session.
     * @return A integer ttl value.
     */

    public int getTimeToLive() {
	// Start with the session wide ttl:
	int ttl = this.cttl;
	// Check against all medias (if any):
	Enumeration e = enumerateMedias();
	while ( e.hasMoreElements() ) {
	    SapMedia m = (SapMedia) e.nextElement();
	    ttl = Math.max(ttl, m.getTimeToLive());
	}
	return Math.min(ttl, 255);
    }

    /**
     * Get the ownership field, formatted for SDP.
     * @return A byte array containing only the owner info (used for session
     * deletion announcements).
     */

    public byte[] getOwner() {
	StringBuffer sb = new StringBuffer(64);
    	sb.append("o="); 
	sb.append(username); 
	sb.append(' '); sb.append(sid);
	sb.append(' '); sb.append(sversion);
	sb.append(' '); sb.append(snettype);
	sb.append(' '); sb.append(saddrtype);
	sb.append(' '); sb.append(saddr.getHostAddress());
	sb.append('\n');
	return sb.toString().getBytes();
    }

    /**
     * Has this session been deleted ?
     * @return A boolean, <strong>true</strong> if a deletion message has
     * been received or emitted concerning that session.
     */

    public boolean isDeleted() {
	return deleted;
    }

    /**
     * Set <i>start</i> and <i>stop</i> time for the session.
     * @param start The start time, in milliseconds (Java format).
     * @param stop The stop time, in milliseconds (Java format).
     */

    public void setTimes(long start, long stop) {
	this.startTime = start;
	this.stopTime  = stop;
    }

    /**
     * Create a SAP session by parsing the given data.
     * @param buf The bytes to parse (compliant to SDP).
     * @param off Offset of data within above buffer.
     * @param len Length of data within above buffer.
     * @exception SapProtocolException If data couldn't be parsed according 
     * to SAP/SDP.
     */

    SapSession(byte buf[], int off, int len)
	throws SapProtocolException
    {
	this();
	parse(buf, off, len);
    }

    SapSession() {
	this.attrs  = new SapAttributes();
	this.medias = new Vector(2);
    }

}
