// sdp.java
// $Id: sdp.java,v 1 1997/07/15 15:00 osofia Exp $
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html


package w3c.rtsp.common;

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

public class Sdp {


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

    /**
     *SDP Media Object.
     */
  
    protected Vector medias=new Vector();

    public Sdp(){
    }

  //create a new String witch represents a SDP description made with the 
  //current value of Sdp Object.

    public String toString(){
	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');
	}
		    
	String s = sb.toString();

	return s;
    }

  //test if the field are in the waned order and return the next index of op.
  //if strict is set to true the order is compelled. 

   private int checkValid(int op, int tag, boolean strict)
	throws SdpException
    {
	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 9;
	  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 SdpException
    {
	return checkValid(op, tag, false);
    }

  //parse the SDP header.

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

    public void parse(String input) 
	throws IOException, SdpException
    {
	BufferedReader  in    = (new BufferedReader(new StringReader(input)));
	String          l     = null;
	StringTokenizer st    = null;
	int             field = -1;
	SdpMedia        media = null;	// The one being parsed
	while ((l = in.readLine()) != null) {
	  if ( l.equals("") )
	    continue;
	  //	  System.out.println("\nDescribe line:"+l+"\n");
	    switch(l.charAt(0)) {
	      case 'v':
		  field = checkValid('v', field);
		  // Parse the SDP 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 {
		      this.username  = st.nextToken();
		      this.sid       = Long.parseLong(st.nextToken());
		      this.sversion  = Long.parseLong(st.nextToken());
		      this.snettype  = st.nextToken();
		      this.saddrtype = st.nextToken();
		      this.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;

      //we have to consider that it could be have few media field
      //so we have to make a array of media.

	      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();
		      }
		      System.out.println("\nMEDIA : "+key+" AND "+val);
		      if ( media == null ) {
			System.out.println("\nIt's a attribute : ");
			attrs.addAttribute(key, val);
		      } else {
			System.out.println("\nIt's a media : ");
			media.addAttribute(key, val);
		      }
		  } catch (Exception ex) {
		    ex.printStackTrace();
		    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 SdpMedia(sm, p, np, tr, farray);
		      addMedia(media);
		  } catch (Exception ex) {
		      error(ex, "unable to parse media \""+l+"\"");
		  }
		  break;
	    }
	}
	return;
    }

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

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

   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;
    }
 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);
   }

  public int getVersion(){
    return this.version;
  }

  public String getUserName(){
    return this.username;
  }
  
  public String getAddressType(){
  return  this.saddrtype;
  }
  
  public InetAddress getAddress(){
  return  this.saddr;
  }

  public String getSessionName(){
    return this.name;
  }

  public String getSessionDescription(){
    if(infos!=null)
      return this.infos[0];
    else
      return new String("");
  }

  public String getSessionUrl(){
    if(urls!=null)
      return this.urls[0];
    else
      return new String("");
  }

  public String getSessionAttribute(){
    if(attrs!=null){
      try{
	return (new String(this.attrs.toString())).substring(2);
      }catch(StringIndexOutOfBoundsException E){
	E.printStackTrace();
	return new String("");
      }
    }
    else
      return new String("");
  }

  public Vector getMedia(){
    return this.medias;
  }

  public void addMedia(SdpMedia media) {
	medias.addElement(media);
    }

  public void removeMedia(SdpMedia media) {
    medias.removeElement(media);
  }

  public void setUserName(String username){
    this.username=username;
  }
  
  public void setAddressType(String saddrtype){
    this.saddrtype=saddrtype;
  }
  
  public void setAddress(String saddr) throws UnknownHostException {
    try{
    this.saddr=InetAddress.getByName(saddr);
    }catch(UnknownHostException E){
      E.printStackTrace();
      throw new UnknownHostException("UnknownHost");
    }
  }

  public void setSessionName(String name){
    this.name=name;
  }

  public void setSessionDescription(String info){
    this.infos[0]=info;
  }

  public void setUrl(String url){
    this.urls=new String[2];
    this.urls[0]=url;
  }

  public void setSessionAttribute(String attr){
   StringTokenizer st = new StringTokenizer(attr, ":");
   String key = st.nextToken();
   String val = st.nextToken();
   attrs.addAttribute(key, val);
  }
}
