/*
 * Copyright (C) 2005 Luca Veltri - University of Parma - Italy
 * 
 * This source code 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 source code 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 source code; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Author(s):
 * Luca Veltri (luca.veltri@unipr.it)
 */

package local.ua;


import local.net.KeepAliveSip;
import org.zoolu.net.SocketAddress;
import org.zoolu.sip.address.*;
import org.zoolu.sip.provider.SipStack;
import org.zoolu.sip.provider.SipProvider;
import org.zoolu.sip.header.*;
import org.zoolu.sip.message.*;
import org.zoolu.sip.transaction.TransactionClient;
import org.zoolu.sip.transaction.TransactionClientListener;
import org.zoolu.sip.authentication.DigestAuthentication;
import org.zoolu.tools.Log;
import org.zoolu.tools.LogLevel;

import java.util.Vector;


/** Register User Agent.
  * It registers (one time or periodically) a contact address with a registrar server.
  */
public class RegisterAgent implements Runnable, TransactionClientListener
{
   /** Max number of registration attempts. */
   static final int MAX_ATTEMPTS=3;

   /** RegisterAgent listener */
   RegisterAgentListener listener;
   
   /** SipProvider */
   SipProvider sip_provider;

   /** User's URI with the fully qualified domain name of the registrar server. */
   NameAddress target;

   /** User name. */
   String username;

   /** User name. */
   String realm;

   /** User's passwd. */
   String passwd;

   /** Nonce for the next authentication. */
   String next_nonce;

   /** Qop for the next authentication. */
   String qop;

   /** User's contact address. */
   NameAddress contact; 

   /** Expiration time. */
   int expire_time;

   /** Renew time. */
   int renew_time;

   /** Whether keep on registering. */
   boolean loop;

   /** Whether the thread is running. */
   boolean is_running;

   /** Event logger. */
   Log log;

   /** Number of registration attempts. */
   int attempts;
   
   /** KeepAliveSip daemon. */
   KeepAliveSip keep_alive;

      
   /** Creates a new RegisterAgent. */
   public RegisterAgent(SipProvider sip_provider, String target_url, String contact_url, RegisterAgentListener listener)
   {  init(sip_provider,target_url,contact_url,listener);
   }
   
   
   /** Creates a new RegisterAgent with authentication credentials (i.e. username, realm, and passwd). */
   public RegisterAgent(SipProvider sip_provider, String target_url, String contact_url, String username, String realm, String passwd, RegisterAgentListener listener)
   {  init(sip_provider,target_url,contact_url,listener);
      // authentication
      this.username=username;
      this.realm=realm;
      this.passwd=passwd;
   }

   /** Inits the RegisterAgent. */
   private void init(SipProvider sip_provider, String target_url, String contact_url, RegisterAgentListener listener)
   {  this.listener=listener;
      this.sip_provider=sip_provider;
      this.log=sip_provider.getLog();
      this.target=new NameAddress(target_url);
      this.contact=new NameAddress(contact_url);
      this.expire_time=SipStack.default_expires;
      this.renew_time=0;
      this.is_running=false;
      this.keep_alive=null;
      // authentication
      this.username=null;
      this.realm=null;
      this.passwd=null;
      this.next_nonce=null;
      this.qop=null;
      this.attempts=0;
   }


   /** Whether it is periodically registering. */
   public boolean isRegistering()
   {  return is_running;
   }


   /** Registers with the registrar server. */
   public void register()
   {  register(expire_time);
   }


   /** Registers with the registrar server for <i>expire_time</i> seconds. */
   public void register(int expire_time)
   {  attempts=0;
      if (expire_time>0) this.expire_time=expire_time;
      Message req=MessageFactory.createRegisterRequest(sip_provider,target,target,contact);
      req.setExpiresHeader(new ExpiresHeader(String.valueOf(expire_time)));
      if (next_nonce!=null)
      {  AuthorizationHeader ah=new AuthorizationHeader("Digest");
         SipURL target_url=target.getAddress();
         ah.addUsernameParam(username);
         ah.addRealmParam(realm);
         ah.addNonceParam(next_nonce);
         ah.addUriParam(req.getRequestLine().getAddress().toString());
         ah.addQopParam(qop);
         String response=(new DigestAuthentication(SipMethods.REGISTER,ah,null,passwd)).getResponse();
         ah.addResponseParam(response);
         req.setAuthorizationHeader(ah);
      }
      if (expire_time>0) printLog("Registering contact "+contact+" (it expires in "+expire_time+" secs)",LogLevel.HIGH);
      else printLog("Unregistering contact "+contact,LogLevel.HIGH);
      TransactionClient t=new TransactionClient(sip_provider,req,this);
      t.request(); 
   }


   /** Unregister with the registrar server */
   public void unregister()
   {  register(0);
   } 


   /** Unregister all contacts with the registrar server */
   public void unregisterall()
   {  attempts=0;
      NameAddress user=new NameAddress(target);
      Message req=MessageFactory.createRegisterRequest(sip_provider,target,target,null);
      //ContactHeader contact_star=new ContactHeader(); // contact is *
      //req.setContactHeader(contact_star);
      req.setExpiresHeader(new ExpiresHeader(String.valueOf(0)));
      printLog("Unregistering all contacts",LogLevel.HIGH);
      TransactionClient t=new TransactionClient(sip_provider,req,this); 
      t.request(); 
   }


   /** Periodically registers with the registrar server.
     * @param expire_time expiration time in seconds
     * @param renew_time renew time in seconds */
   public void loopRegister(int expire_time, int renew_time)
   {  this.expire_time=expire_time;
      this.renew_time=renew_time;
      loop=true;
      if (!is_running) (new Thread(this)).start();
   }


   /** Periodically registers with the registrar server.
     * @param expire_time expiration time in seconds
     * @param renew_time renew time in seconds
     * @param keepalive_time keep-alive packet rate (inter-arrival time) in milliseconds */
   public void loopRegister(int expire_time, int renew_time, long keepalive_time)
   {  loopRegister(expire_time,renew_time);
      // keep-alive
      if (keepalive_time>0)
      {  SipURL target_url=target.getAddress();
         String target_host=target_url.getHost();
         int targe_port=target_url.getPort();
         if (targe_port<0) targe_port=SipStack.default_port;
         new KeepAliveSip(sip_provider,new SocketAddress(target_host,targe_port),null,keepalive_time);
      }
   }


   /** Halts the periodic registration. */
   public void halt()
   {  if (is_running) loop=false;
      if (keep_alive!=null) keep_alive.halt();
   }

   
   // ***************************** run() *****************************

   /** Run method */
   public void run()
   {  
      is_running=true;
      try
      {  while (loop)
         {  register();
            Thread.sleep(renew_time*1000);
         }
      }
      catch (Exception e) {  printException(e,LogLevel.HIGH);  }
      is_running=false;
   }

   
   // **************** Transaction callback functions *****************

   /** Callback function called when client sends back a failure response. */

   /** Callback function called when client sends back a provisional response. */
   public void onTransProvisionalResponse(TransactionClient transaction, Message resp)
   {  // do nothing..
   }

   /** Callback function called when client sends back a success response. */
   public void onTransSuccessResponse(TransactionClient transaction, Message resp)
   {  if (transaction.getTransactionMethod().equals(SipMethods.REGISTER))
      {  if (resp.hasAuthenticationInfoHeader())
         {  next_nonce=resp.getAuthenticationInfoHeader().getNextnonceParam();
         }
         StatusLine status=resp.getStatusLine();
         String result=status.getCode()+" "+status.getReason();
         
         // update the renew_time
         int expires=0;
         if (resp.hasExpiresHeader())
         {  expires=resp.getExpiresHeader().getDeltaSeconds();
         }
         else
         if (resp.hasContactHeader())
         {  Vector contacts=resp.getContacts().getHeaders();
            for (int i=0; i<contacts.size(); i++)
            {  int exp_i=(new ContactHeader((Header)contacts.elementAt(i))).getExpires();
               if (exp_i>0 && (expires==0 || exp_i<expires)) expires=exp_i;
            }    
         }
         if (expires>0 && expires<renew_time) renew_time=expires;
         
         printLog("Registration success: "+result,LogLevel.HIGH);
         if (listener!=null) listener.onUaRegistrationSuccess(this,target,contact,result);
      }
   }

   /** Callback function called when client sends back a failure response. */
   public void onTransFailureResponse(TransactionClient transaction, Message resp)
   {  if (transaction.getTransactionMethod().equals(SipMethods.REGISTER))
      {  StatusLine status=resp.getStatusLine();
         int code=status.getCode();
         if (code==401 && attempts<MAX_ATTEMPTS && resp.hasWwwAuthenticateHeader() && resp.getWwwAuthenticateHeader().getRealmParam().equalsIgnoreCase(realm))
         {  attempts++;
            Message req=transaction.getRequestMessage();
            req.setCSeqHeader(req.getCSeqHeader().incSequenceNumber());
            WwwAuthenticateHeader wah=resp.getWwwAuthenticateHeader();
            String qop_options=wah.getQopOptionsParam();
            //printLog("DEBUG: qop-options: "+qop_options,LogLevel.MEDIUM);
            qop=(qop_options!=null)? "auth" : null;
            AuthorizationHeader ah=(new DigestAuthentication(SipMethods.REGISTER,req.getRequestLine().getAddress().toString(),wah,qop,null,username,passwd)).getAuthorizationHeader();
            req.setAuthorizationHeader(ah);
            TransactionClient t=new TransactionClient(sip_provider,req,this);
            t.request();
         }
         else
         {  String result=code+" "+status.getReason();
            printLog("Registration failure: "+result,LogLevel.HIGH);
            if (listener!=null) listener.onUaRegistrationFailure(this,target,contact,result);
         }
      }
   }

   /** Callback function called when client expires timeout. */
   public void onTransTimeout(TransactionClient transaction)
   {  if (transaction.getTransactionMethod().equals(SipMethods.REGISTER))
      {  printLog("Registration failure: No response from server.",LogLevel.HIGH);
         if (listener!=null) listener.onUaRegistrationFailure(this,target,contact,"Timeout");
      }
   }

   
   // ****************************** Logs *****************************

   /** Adds a new string to the default Log */
   void printLog(String str, int level)
   {  if (log!=null) log.println("RegisterAgent: "+str,level+SipStack.LOG_LEVEL_UA);  
   }

   /** Adds the Exception message to the default Log */
   void printException(Exception e,int level)
   {  if (log!=null) log.printException(e,level+SipStack.LOG_LEVEL_UA);
   }

}
