package bsearch.app ;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.AttributesImpl;

import bsearch.util.GeneralUtils;



public strictfp class SearchProtocol
{
	//TODO: Add a "documentation" field to store text/comments about the search protocol.
	public final double bsearchVersionNumber;
	public final String modelFile ;
	public final String modelStepCommands ;
	public final String modelSetupCommands ;
	public final String modelStopCondition ;
	public final int modelStepLimit ;  
	public final String modelMetricReporter ;
	public final String modelMeasureIf;
	
	public final List<String> paramSpecStrings;

	public final boolean fitnessMinimized ;
	public final int fitnessSamplingReplications ; // if == 0, use adaptive sampling.
	public final FITNESS_COLLECTING fitnessCollecting;
	public final FITNESS_COMBINE_REPLICATIONS fitnessCombineReplications;
	public final String fitnessDerivativeParameter;
	public final double fitnessDerivativeDelta;	
	public final boolean fitnessDerivativeUseAbs;	

	public String searchMethodType ;
	public final HashMap<String,String> searchMethodParams;

	public final String chromosomeType;
	public final boolean caching;
	public final int evaluationLimit;
	
	public final int bestCheckingNumReplications; // if == 0, no best checking is done


	/**
	 * A SearchProtocol stores the information about what is being searched, and how it should be done.
	 * These fields map to the BehaviorSearch GUI for designing an experiment/protocol in a 
	 * relatively straightforward fashion. 
	 */
	public SearchProtocol(String modelFile, List<String> paramSpecStrings,
			String modelStepCommands, String modelSetupCommands, String modelStopCondition,
			int modelStepLimit, String modelMetricReporter, String modelMeasureIf, 
			boolean fitnessMinimized,
			int fitnessSamplingRepetitions,
			FITNESS_COLLECTING fitnessCollecting,
			FITNESS_COMBINE_REPLICATIONS fitnessCombineReplications,
			String fitnessDerivativeParameter,
			double fitnessDerivativeDelta,
			boolean fitnessDerivativeUseAbs,
			String searchMethodType,
			HashMap<String, String> searchMethodParams,
			String chromosomeType,
			boolean caching,
			int evaluationLimit, 
			int bestCheckingNumReplications) {
		super();
		this.bsearchVersionNumber = GeneralUtils.getVersionNumber();
		this.modelFile = modelFile;
		this.paramSpecStrings = paramSpecStrings;
		this.modelStepCommands = modelStepCommands;
		this.modelSetupCommands = modelSetupCommands;
		this.modelStopCondition = modelStopCondition;
		this.modelStepLimit = modelStepLimit;
		this.modelMetricReporter = modelMetricReporter;
		this.modelMeasureIf = modelMeasureIf;
		this.fitnessMinimized = fitnessMinimized;
		this.fitnessSamplingReplications = fitnessSamplingRepetitions;
		this.fitnessCollecting = fitnessCollecting;
		this.fitnessCombineReplications = fitnessCombineReplications;
		this.fitnessDerivativeParameter = fitnessDerivativeParameter;
		this.fitnessDerivativeDelta = fitnessDerivativeDelta;
		this.fitnessDerivativeUseAbs = fitnessDerivativeUseAbs;
		this.searchMethodType = searchMethodType;
		this.searchMethodParams = searchMethodParams;
		this.chromosomeType = chromosomeType;
		this.caching = caching;
		this.evaluationLimit = evaluationLimit;
		this.bestCheckingNumReplications = bestCheckingNumReplications;
	}
	private static String loadOrGetDefault(XPath xpath, Document xmlDoc, String path, String defaultVal)
	{
		try {
			String s = (String) xpath.evaluate(path , xmlDoc , XPathConstants.STRING );
			if (s.equals(""))
			{ 
				return defaultVal; 
			}
			return s;
		} catch (XPathExpressionException e) {
			return defaultVal;
		}
	}
	private static int loadOrGetDefaultInt(XPath xpath, Document xmlDoc, String path, int defaultVal)
	{
		try {
			String s = (String) xpath.evaluate(path , xmlDoc , XPathConstants.STRING );
			if (s.equals(""))
			{ 
				return defaultVal; 
			}
			return Integer.valueOf(s.trim());
		} catch (XPathExpressionException e) {
			return defaultVal;
		} catch (NumberFormatException e) {
			return defaultVal;
		}
	}
	private static double loadOrGetDefaultDouble(XPath xpath, Document xmlDoc, String path, double defaultVal)
	{
		try {
			String s = (String) xpath.evaluate(path , xmlDoc , XPathConstants.STRING );
			if (s.equals(""))
			{ 
				return defaultVal; 
			}
			return Double.valueOf(s.trim());
		} catch (XPathExpressionException e) {
			return defaultVal;
		} catch (NumberFormatException e) {
			return defaultVal;
		}
	}

	private SearchProtocol(Document xmlDoc) throws XPathExpressionException
	{
		// Admittedly, XPath isn't the most efficient approach, but this code isn't performance critical, 
		// and I find XPath nice and readable. ~Forrest (10/28/2008)
		XPathFactory factory = XPathFactory.newInstance();
		XPath xpath = factory.newXPath();
		bsearchVersionNumber = loadOrGetDefaultDouble(xpath,  xmlDoc, "/search/bsearchVersionNumber/text()" , 0.71); // 0.71 since that was the version before we added this XML tag
		modelFile = loadOrGetDefault(xpath,  xmlDoc, "/search/modelInfo/modelFile/text()" , "");
		modelSetupCommands = loadOrGetDefault(xpath,  xmlDoc, "/search/modelInfo/modelSetupCommands/text()", "");
		modelStepCommands = loadOrGetDefault(xpath,  xmlDoc, "/search/modelInfo/modelStepCommands/text()" , "" );
		modelStopCondition = loadOrGetDefault(xpath,  xmlDoc, "/search/modelInfo/modelStopCondition/text()", "");
		modelStepLimit = loadOrGetDefaultInt(xpath,  xmlDoc, "/search/modelInfo/modelStepLimit/text()", 100);
		modelMetricReporter = loadOrGetDefault(xpath,  xmlDoc, "/search/modelInfo/modelMetricReporter/text()" , "");
		modelMeasureIf = loadOrGetDefault(xpath,  xmlDoc, "/search/modelInfo/modelMeasureIf/text()" , "true");
		fitnessMinimized = Boolean.valueOf(loadOrGetDefault(xpath,  xmlDoc, "/search/fitnessInfo/fitnessMinimized/text()" , "false").trim());
		String fcollect = loadOrGetDefault(xpath,  xmlDoc, "/search/fitnessInfo/fitnessCollecting/text()" , FITNESS_COLLECTING.AT_FINAL_STEP.toString() );
		fitnessCollecting = FITNESS_COLLECTING.valueOf( fcollect.trim() );
		fitnessSamplingReplications = loadOrGetDefaultInt(xpath,  xmlDoc, "/search/fitnessInfo/fitnessSamplingReplications/text()" , 10);
		String fcombine = loadOrGetDefault(xpath, xmlDoc,"/search/fitnessInfo/fitnessCombineReplications/text()" , FITNESS_COMBINE_REPLICATIONS.MEAN.toString());
		fitnessCombineReplications = FITNESS_COMBINE_REPLICATIONS.valueOf( fcombine.trim() );
		fitnessDerivativeParameter = loadOrGetDefault(xpath, xmlDoc,"/search/fitnessInfo/fitnessDerivative/@parameter" , ""); 
		fitnessDerivativeDelta = loadOrGetDefaultDouble(xpath, xmlDoc,"/search/fitnessInfo/fitnessDerivative/@delta" , 0.0); 
		fitnessDerivativeUseAbs = Boolean.valueOf(loadOrGetDefault(xpath,  xmlDoc, "/search/fitnessInfo/fitnessDerivative/@useabs" , "false").trim()); 
		
		NodeList paramSpecNodes = (NodeList) xpath.evaluate("/search/searchSpace/paramSpec/text()" , xmlDoc , XPathConstants.NODESET );
		paramSpecStrings = new LinkedList<String>();
		for (int i = 0; i < paramSpecNodes.getLength(); i++)
		{
			Node n = paramSpecNodes.item( i );
			paramSpecStrings.add( n.getNodeValue().trim() );
		}
		
		searchMethodType = loadOrGetDefault(xpath,  xmlDoc, "/search/searchMethod/@type" , "RandomSearch" ); 

		NodeList methodParamNodes = (NodeList) xpath.evaluate("/search/searchMethod/searchMethodParameter" , xmlDoc , XPathConstants.NODESET );
		searchMethodParams = new LinkedHashMap<String,String>();
		for (int i = 0; i < methodParamNodes.getLength(); i++)
		{
			Element n = (Element) methodParamNodes.item( i );
			searchMethodParams.put( n.getAttribute( "name" ) , n.getAttribute("value") );
		}
		chromosomeType = loadOrGetDefault(xpath,  xmlDoc, "/search/chromosomeRepresentation/@type" , "MixedTypeChromosome" );
		caching = Boolean.valueOf(loadOrGetDefault(xpath,  xmlDoc, "/search/caching/text()" , "true"));
		evaluationLimit = loadOrGetDefaultInt(xpath,  xmlDoc, "/search/evaluationLimit/text()" , 300);		
		bestCheckingNumReplications = loadOrGetDefaultInt(xpath,  xmlDoc, "/search/bestCheckingNumReplications/text()" , 0);
		
		updateForVersionChanges();
	}
	/*
	 * Do a bit of processing so that old protocol files can be loaded...  
	 */
	private void updateForVersionChanges()
	{
		if (bsearchVersionNumber < 0.72)
		{
			if (searchMethodType.equals("GenerationalGA"))
			{
				searchMethodType = "StandardGA";
			}			
		}
	}
	
	// convenience method...
	private static void xmlElementNoAtts(TransformerHandler hd, String elementName, String cData) throws SAXException
	{
		xmlElementWithAtts(hd,elementName,new AttributesImpl(), cData);
	}
	private static void xmlElementWithAtts(TransformerHandler hd, String elementName, AttributesImpl atts) throws SAXException
	{
		xmlElementWithAtts(hd,elementName,atts, "");
	}
	// another convenience method...
	private static void xmlElementWithAtts(TransformerHandler hd, String elementName, AttributesImpl atts, String cData) throws SAXException
	{
		hd.startElement( "" , "" , elementName , atts);
		if (cData.length() > 0) { 
			hd.characters( cData.toCharArray() , 0 , cData.length() );
		}
		hd.endElement( "" , "" , elementName );
	}

	/** Note: This method was adapted from 
	 *     http://www.javazoom.net/services/newsletter/xmlgeneration.html
	 * 
	 * @param out PrintWriter to send the xml data to.
	 */
	public void save(Writer out)
	{
		try
		{
			StreamResult streamResult = new StreamResult( out ) ;
			SAXTransformerFactory tf = (SAXTransformerFactory) TransformerFactory.newInstance() ;
			// SAX2.0 ContentHandler.
			TransformerHandler hd = tf.newTransformerHandler() ;
			Transformer serializer = hd.getTransformer() ;
			serializer.setOutputProperty( OutputKeys.ENCODING , "us-ascii" ) ;
			serializer.setOutputProperty( OutputKeys.DOCTYPE_SYSTEM ,
					"behaviorsearch.dtd" ) ;
			serializer.setOutputProperty( OutputKeys.INDENT , "yes" ) ;
			hd.setResult( streamResult ) ;
			hd.startDocument() ;
			AttributesImpl noAtts = new AttributesImpl() ;	
			AttributesImpl atts = new AttributesImpl() ;
		
			hd.startElement( "" , "" , "search" , noAtts ) ;
				xmlElementNoAtts(hd, "bsearchVersionNumber", String.format("%.2f", GeneralUtils.getVersionNumber()));
				hd.startElement( "" , "" , "modelInfo" , noAtts );
					xmlElementNoAtts(hd, "modelFile", modelFile ) ;
					xmlElementNoAtts(hd, "modelSetupCommands", modelSetupCommands ) ;
					xmlElementNoAtts(hd, "modelStepCommands", modelStepCommands ) ;
					xmlElementNoAtts(hd, "modelStopCondition", modelStopCondition ) ;
					xmlElementNoAtts(hd, "modelStepLimit", Integer.toString( modelStepLimit )) ;
					xmlElementNoAtts(hd, "modelMetricReporter", modelMetricReporter) ;
					xmlElementNoAtts(hd, "modelMeasureIf", modelMeasureIf) ;					
				hd.endElement( "" , "" , "modelInfo" );
				hd.startElement( "" , "" , "fitnessInfo" , noAtts );
					xmlElementNoAtts(hd, "fitnessMinimized", Boolean.toString(fitnessMinimized)) ;
					xmlElementNoAtts(hd, "fitnessCollecting", fitnessCollecting.toString()) ;
					xmlElementNoAtts(hd, "fitnessSamplingReplications", Integer.toString( fitnessSamplingReplications )) ;
					xmlElementNoAtts(hd, "fitnessCombineReplications", fitnessCombineReplications.toString()) ;
					if (fitnessDerivativeParameter.length() > 0)
					{
						atts.clear();
						atts.addAttribute( "" , "" , "parameter" , "CDATA" , fitnessDerivativeParameter);
						atts.addAttribute( "" , "" , "delta" , "CDATA" , Double.toString(fitnessDerivativeDelta));
						atts.addAttribute( "" , "" , "useabs" , "CDATA" , Boolean.toString(fitnessDerivativeUseAbs));
						xmlElementWithAtts(hd, "fitnessDerivative", atts);
					}
				hd.endElement( "" , "" , "fitnessInfo" );
				hd.startElement( "" , "" , "searchSpace" , noAtts );
					for (String ps: paramSpecStrings)
					{
						xmlElementNoAtts( hd , "paramSpec" , ps );
					}
				hd.endElement( "" , "" , "searchSpace");
				atts.clear();
				atts.addAttribute( "" , "" , "type" , "CDATA" , searchMethodType);
				hd.startElement( "" , "" , "searchMethod" , atts );
					for (String pName : searchMethodParams.keySet())
					{
						atts.clear();
						atts.addAttribute( "" , "" , "name" , "CDATA" , pName );
						atts.addAttribute( "" , "" , "value" , "CDATA" , searchMethodParams.get( pName ) );
						hd.startElement( "" , "" , "searchMethodParameter" , atts );
						hd.endElement( "" , "" , "searchMethodParameter" );
					}
				hd.endElement( "" , "" , "searchMethod" );
				atts.clear();
				atts.addAttribute( "" , "" , "type" , "CDATA" ,chromosomeType);
				xmlElementWithAtts(hd, "chromosomeRepresentation", atts);
				
				xmlElementNoAtts(hd, "caching", Boolean.toString(caching));
				xmlElementNoAtts(hd, "evaluationLimit", Integer.toString(evaluationLimit));
				xmlElementNoAtts(hd, "bestCheckingNumReplications", Integer.toString(bestCheckingNumReplications));
				
			hd.endElement( "" , "" , "search" ) ;
			hd.endDocument() ;
		}
		catch( SAXException e )
		{
			e.printStackTrace() ;
		}
		catch( TransformerConfigurationException e )
		{
			e.printStackTrace() ;
		}
		
	}
	
	public String toXMLString()
	{
		java.io.StringWriter sw = new java.io.StringWriter();
		save(sw);
		sw.flush();
		return sw.toString();
	}
	
	public static SearchProtocol load(String xmlStr)
		throws java.io.IOException , SAXException
	{
		InputSource inputSource = new InputSource(
				new java.io.StringReader(//"<!DOCTYPE search SYSTEM \"behaviorsearch.dtd\">\n" +  
						xmlStr));
		return load(inputSource);
	}
	public static SearchProtocol load(InputSource inputSource)
		throws java.io.IOException , SAXException
	{
		File resourcesDir = new File( GeneralUtils.attemptResolvePathFromBSearchRoot("resources/" ));
		inputSource.setSystemId(resourcesDir.toURI().toString() ) ;

		DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance() ;
		Document doc = null ;
		try
		{
			// To validate, we may need to help it find the DTD file...
			// inputSource.setSystemId( getClass().getResource( "/" ).toString() ) ;
			fac = DocumentBuilderFactory.newInstance() ;
			//TODO: Perhaps turn xml DTD validation back on?  Or is that too strict?  Is the DTD up to date?
			//fac.setValidating( true ) ;
			DocumentBuilder builder = fac.newDocumentBuilder() ;
			// Choke on any error or warning during parsing, not just fatal errors.
			builder.setErrorHandler( new ErrorHandler()
			{
				public void error( SAXParseException ex ) throws SAXException
				{	throw ex ;	}

				public void fatalError( SAXParseException ex ) throws SAXException
				{  	throw ex ;	}

				public void warning( SAXParseException ex ) throws SAXException
				{	throw ex ;	}
			} ) ;
			doc = builder.parse( inputSource ) ;
			return new SearchProtocol(doc);
		}
		catch( ParserConfigurationException ex )
		{
			ex.printStackTrace() ;
			System.exit( 1 ) ;
		}
		catch( XPathExpressionException e )
		{
			e.printStackTrace();
			System.exit( 1 ) ;
		}
		throw new IllegalStateException ("Unreachable code.");
	}

	public static SearchProtocol loadFile(String fullPathFilename) throws IOException, SAXException 
	{
		return loadFile(new java.io.File(fullPathFilename));
	}
	public static SearchProtocol loadFile(java.io.File file) throws IOException, SAXException 
	{
		BufferedReader reader = new BufferedReader(new FileReader(file));
		try { return load(new InputSource(reader)); } 
		finally { reader.close(); }		
	}


	
	public static enum FITNESS_COLLECTING { 
		AT_FINAL_STEP {
			@Override
			public double collectFrom(List<Double> results) { 	return results.get(results.size() - 1);		}
			@Override 
			public boolean requiresAllSteps() { return false; }
			},
		MEAN_ACROSS_STEPS {
			@Override 
			public double collectFrom(List<Double> results) { 
				return bsearch.util.Stats.mean(results);
			}},
		MEDIAN_ACROSS_STEPS {
			@Override 
			public double collectFrom(List<Double> results) { 
				return bsearch.util.Stats.median(results);
			}}, 
		MIN_ACROSS_STEPS {
			@Override 
			public double collectFrom(List<Double> results) { 
				return Collections.min(results);
			}}, 
		MAX_ACROSS_STEPS {
			@Override 
			public double collectFrom(List<Double> results) { 
				return Collections.max(results);
			}},
		VARIANCE_ACROSS_STEPS {
			@Override 
			public double collectFrom(List<Double> results) { 
				return bsearch.util.Stats.variance(results);
			}},
		SUM_ACROSS_STEPS {
			@Override 
			public double collectFrom(List<Double> results) { 
				return bsearch.util.Stats.sum(results);
			}}
		;
        public abstract double collectFrom(List<Double> stepResults);
        public boolean requiresAllSteps() { return true; }
	}
	
	public static enum FITNESS_COMBINE_REPLICATIONS { 
		MEAN {
			@Override 
			public double combine(List<Double> results) { 
				return bsearch.util.Stats.mean(results);
			}},
		MEDIAN {
			@Override 
			public double combine(List<Double> results) { 
				return bsearch.util.Stats.median(results);
			}},
		MIN {
			@Override 
			public double combine(List<Double> results) { 
				return Collections.min(results);
			}},
		MAX {
			@Override 
			public double combine(List<Double> results) { 
				return Collections.max(results);
			}},
		VARIANCE {
			@Override 
			public double combine(List<Double> results) { 
				return bsearch.util.Stats.variance(results);
			}},
		STDEV {
			@Override 
			public double combine(List<Double> results) { 
				return bsearch.util.Stats.stdev(results);
			}}
		;
        public abstract double combine(List<Double> replicationResults);
	}

	public boolean useBestChecking() {
		return bestCheckingNumReplications > 0;
	}

}
