package com.ericsson.ssa.router;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.logging.Level;

import javax.servlet.sip.ServletParseException;
import javax.servlet.sip.SipApplicationRouter;
import javax.servlet.sip.SipApplicationRouterInfo;
import javax.servlet.sip.SipApplicationRoutingDirective;
import javax.servlet.sip.SipApplicationRoutingRegion;
import javax.servlet.sip.SipApplicationRoutingRegionType;
import javax.servlet.sip.SipRouteModifier;
import javax.servlet.sip.SipServletReadOnlyRequest;

import org.jvnet.glassfish.comms.util.LogUtil;

/**
 * Default Application Router implementation according to JSR289.
 * 
 * TODO:
 *  - adapt to latest JSR289 interfaces from Sankar
 *  - implement destroy method
 *  - investigate if getNextApplication may return null
 *  - currently only config files which are accessible by getResourceAsStream
 *    are supported, is this enough?
 *  - internationalized logging 
 * 
 * @author elnyvbo
 */
public class DefaultApplicationRouter implements SipApplicationRouter {

	private static LogUtil theirLog = LogUtil.AR_LOGGER;
	private static final String LOGPREFIX = 
		"com.ericsson.ssa.router.DefaultApplicationRouter.";
	
	// Application Router info is a cached double HashMap
	// 
	// key: SIP request method name
	// value: hashMap which maps stateinfo to router info
	//
	// TODO OK to use HashMap, memorywise?
	private HashMap<String, HashMap<Integer, RouterInfoBean>> itsRouterInfoMap = 
		new HashMap<String, HashMap<Integer, RouterInfoBean>>();
	
	private static final String CONFIGFILE_PROPERTY =
		"javax.servlet.sip.dar.configuration";
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.sip.SipApplicationRouter#deployedApplications(java.util.List)
	 */
	public void applicationDeployed(List<String> applications) {
		if (theirLog.isLoggable(Level.INFO)) {
			for (String app : applications) {
				theirLog.info(LOGPREFIX + "applicationdeployed", app);
			}
		}

		// An application was added to the chain, this typically implies 
		// that application router configuration has also changed. So refresh.
		this.readConfiguration();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.sip.SipApplicationRouter#destroy()
	 */
	public void destroy() {
		if (theirLog.isLoggable(Level.INFO)) {
			theirLog.info(LOGPREFIX + "destroy");
		}
		
		// TODO what? when is this called? Let's just clean up some memory...
		itsRouterInfoMap.clear();
	}

	/*
	 * TODO improve method performance
	 * 
	 * (non-Javadoc)
	 * @see javax.servlet.sip.SipApplicationRouter#getNextApplication(javax.servlet.sip.SipServletReadOnlyRequest, boolean, javax.servlet.sip.SipApplicationRoutingDirective, java.lang.Object)
	 */
	public SipApplicationRouterInfo getNextApplication(
			SipServletReadOnlyRequest initialRequest,
			SipApplicationRoutingRegion region,
			SipApplicationRoutingDirective directive, Serializable stateInfo) {
		
		if (theirLog.isLoggable(Level.FINEST)) {
    		theirLog.log(Level.FINEST, LOGPREFIX + "getNextApplication");
		}
		
		Integer order = new Integer(0);
		// Upon first invocation, stateInfo will not yet be set.
		if (stateInfo != null){
			// only Integer stateInfo supported
			try {
				order = new Integer(stateInfo.toString());
			}
			catch (NumberFormatException e){
				theirLog.severe(LOGPREFIX + "wrongstateinfo");
				return null;
			}
		}
		
		// lookup request method in cache  
		String method = initialRequest.getMethod();
		theirLog.info(LOGPREFIX + "infolookup", method, order);
		
		// lookup routing info in cache for this method
		RouterInfoBean infoBean = null;
		HashMap<Integer, RouterInfoBean> infoMap = itsRouterInfoMap.get(method);
		
		if (infoMap == null){
			theirLog.info(LOGPREFIX + "noinfofound", method);
			return null; // TODO is this allowed?
		} else {
			// stateinfo contains the Integer we last put in. Increase the 
			// Integer by one, lookup the next routing info in cache.
			order++;
			infoBean = infoMap.get(order);
			if (infoBean == null){
				theirLog.info(
					LOGPREFIX + "noinfofoundforthisorder", method, order);
				return null; // TODO is this allowed?  
				
				// TODO return appname=null 
				
			}
			theirLog.info(LOGPREFIX + "infofound");
		}

		// Determine subscriber URI
		String subId = infoBean.getSubscriberId();
		String subUri = null; 
		if (subId.startsWith("DAR:")) {
			// subscriber URI is to be retrieved from request
			String header = subId.substring(4).trim(); 
			// (IndexOutOfBounds exception was checked at config-time)
			try {
				subUri = initialRequest.getAddressHeader(header).getURI().toString();
			} catch (ServletParseException e){
				theirLog.severe(
					LOGPREFIX + "invalidsubscriberuri", header, e.getMessage());
			}
		}
		
		// Determine route info
		String route = infoBean.getRouteUri();
		SipRouteModifier modifier =
			SipRouteModifier.valueOf(infoBean.getRouteModifier());
		
		// Determine routing region
		String regionOutStr = infoBean.getRegion();
		SipApplicationRoutingRegionType regionOutType =
			SipApplicationRoutingRegionType.valueOf(regionOutStr);
		SipApplicationRoutingRegion regionOut = 
			new SipApplicationRoutingRegion(regionOutStr, regionOutType);
		
		SipApplicationRouterInfo info = 
			new SipApplicationRouterInfo(
				infoBean.getApplicationName(),
				regionOut,
				subUri,
				route,
				modifier,
				"" + infoBean.getStateInfo());
				
		
		if (theirLog.isLoggable(Level.FINEST)) {
    		theirLog.log(Level.FINEST, LOGPREFIX + "returninfo", info);
		}
		return info;
	}

	/*
	 * (non-Javadoc)
	 * @see javax.servlet.sip.SipApplicationRouter#init()
	 */
	public void init(){
		theirLog.info(LOGPREFIX + "initempty");
		this.readConfiguration();
	}
	
	/*
	 * (non-Javadoc)
	 * @see javax.servlet.sip.SipApplicationRouter#init(java.util.List)
	 */
	public void init(List<String> applications) {
        if (theirLog.isLoggable(Level.FINE)) {
        	theirLog.log(Level.FINE, LOGPREFIX + "init");		
        }
		this.readConfiguration();
	}

	/*
	 * (non-Javadoc)
	 * @see javax.servlet.sip.SipApplicationRouter#undeployedApplications(java.util.List)
	 */
	public void applicationUndeployed(List<String> applications) {
		for (String app : applications) {
			if (theirLog.isLoggable(Level.FINE)) {
				theirLog.log(Level.FINE, LOGPREFIX + "applicationundeployed", app);	
			}
		}
		
		// An application was removed from the chain, this typically implies 
		// that application router configuration has also changed. So refresh.
		this.readConfiguration();
	}
	
	private void readConfiguration(){
		
		itsRouterInfoMap.clear();
		
		// read configuration file as specified in system property
		String filename = System.getProperty(CONFIGFILE_PROPERTY);
		
		if (filename==null || "".equals(filename)){
			theirLog.warning(
				LOGPREFIX + "noconfigurationfound", CONFIGFILE_PROPERTY);
			return;
		}
		else {
			theirLog.info(LOGPREFIX + "configurationfound", filename);
		}

		// TODO what kind of file references are supported? 
		// Could this be a URL as well? 
		// For now assuming local file system
		
		// open config file
		try {
			FileReader input = new FileReader(filename);
			BufferedReader bufRead = new BufferedReader(input);

			// parse file and populate itsRouterInfoMap based on each line.
			String line = bufRead.readLine().trim();
			int linenr = 1;
			while (line != null) {
				if (theirLog.isLoggable(Level.FINE)) {
					theirLog.log(Level.FINE, LOGPREFIX + "lineread", line);
				}
				try {
					// skip empty lines
					String trimmedLine = line.trim();
					if (!("".equals(trimmedLine))){
						this.parseLine(trimmedLine);
					}
					line = bufRead.readLine();
				}
				catch (Exception e){
					theirLog.severe(LOGPREFIX + "lineparsingfailed", linenr, e);
					break;
				}
				linenr++;
			}
			bufRead.close();
		} catch (IOException e) {
			theirLog.severe(LOGPREFIX + "configfileopenfailed", e);
		}
	}
	
	/**
	 * Helper method for parsing lines in the DAR configuration
	 * file, as specified in Appendix C of JSR289. Each line shall be
	 * of the form:
	 * 
	 *  METHOD: (sip-router-info-1), (sip-router-info-2), ...etcetera...   
	 *  
	 * Any deviations in this format will result in the line being ignored.
	 *
	 * TODO adapt to latest JSR289 code from Sankar
	 * 
	 * @param aLine line to parse
	 */
	private void parseLine(String aLine) throws RouterConfigException {
		// Find first colon to determine method
		int colonindex = aLine.indexOf(':');
		String method = aLine.substring(0, colonindex);
		if (theirLog.isLoggable(Level.FINE)) {	
			theirLog.log(Level.FINE, LOGPREFIX + "parsingformethod", method);
		}
		HashMap<Integer, RouterInfoBean> infoMap = 
			new HashMap<Integer, RouterInfoBean>();
		
		// Look for '(' occurances to determine next sip-router-info String
		StringTokenizer st = 
			new StringTokenizer(aLine.substring(colonindex + 1).trim(), "(");
		while (st.hasMoreElements()){
			String s_tmp= st.nextToken();
			// only consider what is between brackets
			String s = s_tmp.substring(0, s_tmp.indexOf(')'));
			// s should be a comma separated list where each 
			// element should be enclosed in double quotes
			StringTokenizer st2 = new StringTokenizer(s, ",");
			Vector<String> tokens = new Vector<String>();
			while (st2.hasMoreElements()){
				String quotedToken = st2.nextToken().trim();
				// find closing quotes
				String token = quotedToken.substring(
					1, quotedToken.lastIndexOf('\"'));
				tokens.add(token);
			}
			
			RouterInfoBean infoBean = new RouterInfoBean();
			infoBean.setApplicationName(tokens.get(0));
			infoBean.setSubscriberId(tokens.get(1));
			infoBean.setRegion(tokens.get(2));
			infoBean.setRouteUri(tokens.get(3));
			infoBean.setRouteModifier(tokens.get(4));
			
			// stateinfo should be parsed  
			String stateInfo = tokens.get(5);
			infoBean.setStateInfo(stateInfo);
			infoMap.put(infoBean.getStateInfo(), infoBean);
		}		
		itsRouterInfoMap.put(method, infoMap);
	}
	
	/**
	 * RouterInfoBean introduced to hold routerInfo as parsed from the DAR
	 * config file. Actual SipApplicationRouterInfo objects will only be 
	 * generated when a request is passed in. 
	 * 
	 * @author elnyvbo
	 */
	private class RouterInfoBean {
		private String applicationName;
		private String subscriberId;
		private String region;
		private String routeUri; 
		private String routeModifier;
		private Integer stateInfo;
		
		public String getApplicationName() {
			return applicationName;
		}
		public void setApplicationName(String applicationName) {
			this.applicationName = applicationName;
		}
		public String getRegion() {
			return region;
		}
		public void setRegion(String region) {
			this.region = region;
		}
		public String getRouteModifier() {
			return routeModifier;
		}
		public void setRouteModifier(String routeModifier) {
			this.routeModifier = routeModifier;
		}
		public String getRouteUri() {
			return routeUri;
		}
		public void setRouteUri(String routeUri) {
			this.routeUri = routeUri;
		}
		public Integer getStateInfo() {
			return stateInfo;
		}
		public void setStateInfo(String stateInfo) 
			throws RouterConfigException {
			// stateInfo should be an integer number
			try {
				this.stateInfo = new Integer(stateInfo);
			}
			catch (NumberFormatException e) {
				throw new RouterConfigException(
					"Malformed stateInfo in DAR configuration: " 
					+ stateInfo, e);
			}
		}
		public String getSubscriberId() {
			return subscriberId;
		}
		public void setSubscriberId(String subscriberId) 
			throws RouterConfigException {
			// verify correct format in case of DAR: directive
			if (subscriberId.startsWith("DAR:")){
				try {
					subscriberId.substring(4);
				}
				catch (IndexOutOfBoundsException e){
					throw new RouterConfigException (
						"Malformed subscriberId in DAR configuration: " 
						+ subscriberId, e);
				}
			}
			this.subscriberId = subscriberId;
		}
		
		public String toString() {
			return "appName=" + applicationName + 
				", subId=" + subscriberId + 
				", region=" + region + 
				", routeUri=" + routeUri + 
				", routeModifier=" + routeModifier + 
				", stateInfo=" + stateInfo;
		}
	}

	public void configurationChanged(InputStream configInputStream) {
		// TODO Auto-generated method stub
		
	}
}
