import java.io.*;
import java.util.Hashtable;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import DE.*;
import Geometry.*;

/**
 * Hashtables to keep track of run-time parameters and their defaults for
 * the FindPoints program.
 * Written by Chris Studholme
 * Licenced for redistribution under the GNU General Public Licence
 */
class RunParameters {
    /** Hashtable for parameters.
     */
    private Hashtable ht;
    /** Hashtable for default values.
     */
    private static Hashtable defaults;
    
    /**
     * Static code populates the defaults hashtable.
     */
    static {
	defaults = new Hashtable();
	// DE
	defaults.put("F",new Double(0.5));
	defaults.put("CR",new Float(0.5));
	defaults.put("NP",new Integer(100));
	// PointParameters
	defaults.put("width",new Double(5e-4));
	defaults.put("widthstdev",new Double(0));
	// run parameters
	defaults.put("maxgen",new Integer(1000));
	defaults.put("mingen",new Integer(0));
	defaults.put("mindiv",new Double(0.1));
	defaults.put("maxerror",new Double(0));
	// population manipulation
	defaults.put("randomize",new Double(0));
	defaults.put("threads",new Integer(2));
    }

    /**
     * Default constructor.
     */    
    public RunParameters() {
	ht = new Hashtable();
    }
    /**
     * Create new parameters object that is an exact copy of another object.
     * @param other object to copy
     */
    public RunParameters(RunParameters other) {
	ht = new Hashtable(other.ht);
    }
    /**
     * Get the value for a parameter.  If the parameter has not been set and
     * a default value is available, it is returned.
     * @param var parameter to get
     * @return value of parameter
     */
    public Object get(String var) {
	Object result = ht.get(var);
	return result!=null ? result : defaults.get(var);
    }
    /**
     * Get the value for a parameter.  If the parameter has not been set, the
     * specified default object is returned.
     * @param var parameter to get
     * @param def default object to return if parameter is not set
     * @return value of parameter
     */
    public Object get(String var, Object def) {
	Object result = ht.get(var);
	return result!=null ? result : def;
    }
    /**
     * Set a parameter.
     * @param var parameter to set
     * @param value value to set
     */
    public void put(String var, Object value) {
	ht.put(var,value);
    }
}

/**
 * Program to find the location of the camera for a series of photos taken
 * of an object given a collection of points identified on the surface of
 * the object.  
 */
public class FindPoints {

    /** Number of generations between each output of the results file 
     * (just in case the optimization problem is never solved).
     */
    public static final int OUTPUT_INTERVAL=1000;

    /** Name of input file.
     */
    String inputfile;

    /** Graphical view object.
     */
    GraphicalView view;

    /** Observed data object.
     */
    ObservedPoints data;

    /** Differential evolution object.
     */
    DE de;

    /** Run-time parameters for all but last photo.
     */
    RunParameters params;

    /** Run-time parameters to use once the last photo has been added.
     */
    RunParameters params_last;

    /**
     * The constructor's job is to parse the parameters and create the params
     * and params_last objects.
     * @param args run-time parameters
     */
    public FindPoints(String[] args) {
	params = new RunParameters();
	RunParameters current = params;
	for (int i=0; i<args.length; ++i) {
	    if (args[i].startsWith("-")) {
		if (args[i].equals("-F")||
		    args[i].equals("-CR")||
		    args[i].equals("-mindiv")||
		    args[i].equals("-maxerror")||
		    args[i].equals("-randomize")||
		    args[i].equals("-width")||
		    args[i].equals("-widthstdev"))
		    current.put(args[i].substring(1),new Double(args[++i]));
		else if (args[i].equals("-mingen")||
			 args[i].equals("-maxgen")||
			 args[i].equals("-threads")||
			 args[i].equals("-NP"))
		    current.put(args[i].substring(1),new Integer(args[++i]));
		else if (args[i].equals("-last")) {
		    params_last = new RunParameters(params);
		    current = params_last;
		    // don't inherit these values
		    current.put("randomize",new Double(0));
		}
		else {
		    throw new IllegalArgumentException("Invalid parameter: "+
						       args[i]);
		}
	    }
	    else if (inputfile==null)
		inputfile = args[i];
	    else {
		throw new IllegalArgumentException("Invalid parameter: "+
						   args[i]);
	    }
	}
	if (params_last==null)
	    params_last = params;
    }

    /**
     * Compute the diversity of the population represented by the DE object.
     * The diversity is the mean ratio of the standard deviation in the 
     * position of a camera to the standard deviation in the position of
     * all cameras.
     * @return diversity of population between 0 and 1
     */
    public double computeDiversity() {
	ParameterSet[] vectors = de.getAllVectors();
	// compute overall variation in x,y,z
	Stats[] all = new Stats[3];
	for (int i=0; i<3; ++i)
	    all[i] = new Stats();
	// and the variation in each individual camera's position
	int nphotos = ((PointParameters)vectors[0]).nphotos;
	Stats[][] photo = new Stats[nphotos][3];
	for (int k=0; k<nphotos; ++k)
	    for (int i=0; i<3; ++i)
		photo[k][i] = new Stats();
	// add up all camera positions
	for (int j=0; j<vectors.length; ++j) {
	    PointParameters pp = (PointParameters)vectors[j];
	    for (int k=0; k<pp.nphotos; ++k)
		for (int i=0; i<3; ++i) {
		    all[i].add(pp.photos[k].pos.component[i]);
		    photo[k][i].add(pp.photos[k].pos.component[i]);
		}
	}
	// find mean standard deviation per component divided by mean overall
	double diversity=0;
	for (int i=0; i<3; ++i)
	    for (int k=0; k<nphotos; ++k)
		diversity += Math.sqrt(photo[k][i].getVariance()
				       / all[i].getVariance());
	return diversity/nphotos/3;
    }

    /**
     * Write an ascii version of the results represented by a PointParameters
     * object.
     * @param pp result set to output
     * @param out print stream to send output to
     */
    public void writeResults(PointParameters pp, PrintStream out) {
	// write out point positions
	for (int i=0; i<pp.npoints; ++i) 
	    if (pp.points[i]!=null)
		out.println(data.points[i].id+" "+
			    pp.points[i].component[0]+" "+
			    pp.points[i].component[1]+" "+
			    pp.points[i].component[2]);
	out.println();
	// write out camera positions
	for (int i=0; i<pp.nphotos; ++i) {
	    out.println(data.photos[i].id+" "+
			pp.photos[i].pos.component[0]+" "+
			pp.photos[i].pos.component[1]+" "+
			pp.photos[i].pos.component[2]+" "+
			pp.photos[i].dir.component[0]+" "+
			pp.photos[i].dir.component[1]+" "+
			pp.photos[i].dir.component[2]+" "+
			pp.photos[i].up.component[0]+" "+
			pp.photos[i].up.component[1]+" "+
			pp.photos[i].up.component[2]);
	}
	out.println();
	// write out width for information purposes
	out.println(pp.getWidth());
    }

    /**
     * Write both binary and ascii versions of results to files whose names
     * are determined from the name of the input file.  The binary file has
     * a file name with the extension ".bin".  The ascii file has an extension
     * of ".out".  
     */
    public void writeOutput() {
	String binfile;
	String txtfile;
	int d = inputfile.lastIndexOf('.');
	int s = inputfile.lastIndexOf('/');
	if ((d==-1)||(d<s)) {
	    binfile = inputfile+".bin";
	    txtfile = inputfile+".out";
	}
	else {
	    binfile = inputfile.substring(0,d)+".bin";
	    txtfile = inputfile.substring(0,d)+".out";
	}
	try {
	    // binary version
	    FileOutputStream stream = new FileOutputStream(binfile);
	    ObjectOutputStream binout = new ObjectOutputStream(stream);
	    binout.writeObject(de);
	    // text version
	    stream = new FileOutputStream(txtfile);
	    PrintStream txtout = new PrintStream(stream);
	    PointParameters pp = (PointParameters)de.getParameterSet();
	    writeResults(pp,txtout);
	    txtout.println("error: "+de.getError());
	    txtout.println("diversity: "+computeDiversity());
	    txtout.println("generations: "+de.getNumberOfGenerations());
	}
	catch (FileNotFoundException e) {
	    System.err.println(e);
	}
	catch (IOException e) {
	    System.err.println(e);
	}
    }

    /**
     * Compute generations of DE.  The generations will definately not
     * stop until mingen generations are compute.  After that, computation
     * will cease if either maxgen generations are done, the error threshold
     * is reached, or the diversity threshold is reached.
     * @param mingen minimum number of generations (or 0)
     * @param maxgen maximum number of generations (or 0)
     * @param mindiv minimum allowed diversity (or 0)
     * @param maxerror maximum allowed error (or 0)
     */
    public void solveProblem(int mingen, int maxgen,
			     double mindiv, double maxerror) {
	if (maxgen<=0) 
	    maxgen=Integer.MAX_VALUE;
	int gen=0;
	double error;
	double lasterror=0;
	double div;
	System.out.println("F="+de.F+
			   "  CR="+de.CR+
			   "  NP="+de.getAllVectors().length);
	do {
	    ++gen;
	    error = de.doItteration();
	    div = computeDiversity();
	    int rep = de.getNumberOfReplacements();	
    
	    // status output
	    System.out.print(gen+": "+error+" ("+div+") ("+rep+")    \r");
	    if (error!=lasterror) {
		System.out.println();
		view.update((PointParameters)de.getParameterSet());
		lasterror=error;
	    }

	    // in case we never finish
	    if (gen%OUTPUT_INTERVAL==0)
		writeOutput();
	    
	    if (gen<mingen)
		continue;
	} while ((gen<maxgen)&&(error>maxerror)&&(div>mindiv));
	System.out.println();
    }

    /**
     * Read binary DE object previously streamed out.  
     * @return true if successful
     */
    public boolean readDEFile()
	throws FileNotFoundException, IOException {

	try {
	    ObjectInputStream stream =
		new ObjectInputStream(new FileInputStream(inputfile));
	    de=(DE)stream.readObject();
	    de.setNumberOfThreads(((Number)params.get("threads")).intValue());
	    data=(ObservedPoints)de.getDataSet();
	}
	catch (StreamCorruptedException e) {
	    return false;
	}
	catch (ClassNotFoundException e) {
	    throw new StreamCorruptedException(e.toString());
	}

	System.out.println("points: "+data.npoints);
	System.out.println("photos: "+data.nphotos);
	return true;
    }

    /**
     * Read text file containing point names, photo names, and observation
     * data.  Creates data object, de object, and populates de object with
     * an initial population.
     * @return true if successful
     */
    public boolean readTextDataFile()
	throws FileNotFoundException, IOException {
	BufferedReader reader =
	    new BufferedReader(new FileReader(inputfile));
	data = new ObservedPoints();
	int nobservations = data.readData(reader);
	System.out.println("points: "+data.npoints);
	System.out.println("photos: "+data.nphotos);
	System.out.println("observations: "+nobservations);
	
	// sort dataset
	data.sort();

	// create DE object
	de = new DE(data);
	de.setNumberOfThreads(((Number)params.get("threads")).intValue());
	de.F = ((Number)params.get("F")).doubleValue();
	de.CR = ((Number)params.get("CR")).floatValue();

	// create initial population
	PointParameters template = new PointParameters(data.npoints,2);
	template.setWidth(((Number)params.get("width")).doubleValue());
	double stdev = ((Number)params.get("widthstdev")).doubleValue();
	template.varywidth=(stdev>0);
	int NP = ((Number)params.get("NP")).intValue();
	for (int i=0; i<NP; ++i) {
	    PointParameters pp = new PointParameters(template);
	    pp.randomize(stdev);
	    de.addVector(pp);
	}

	return true;
    }

    /**
     * Create JFrame object (popup window) and GraphicalView canvas within
     * to display progress of optimization algorithm.
     */
    public void createGraphicalView() {
	// open graphical window
	JFrame frame = new JFrame("Graphical View");
        frame.addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    System.exit(0);
                }
            });
        view = new GraphicalView();
	view.data = data;
        frame.getContentPane().setLayout(new BorderLayout());
	frame.getContentPane().add("Center", view);
        frame.pack();
        frame.setVisible(true);
    }

    /**
     * Compute an array of 3 Stats objects representing the distribution of
     * camera position over all photos in 3 dimensions.
     * @return array of 3 Stats objects (one for each dimension)
     */
    public Stats[] populationStats() {
	ParameterSet[] vectors = de.getAllVectors();
	Stats[] s = new Stats[3];
	for (int i=0; i<3; ++i)
	    s[i] = new Stats();
	for (int j=0; j<vectors.length; ++j) {
	    PointParameters pp = (PointParameters)vectors[j];
	    for (int k=0; k<pp.nphotos; ++k)
		for (int i=0; i<3; ++i)
		    s[i].add(pp.photos[k].pos.component[i]);
	}
	return s;
    }

    /**
     * Add a new photo with a random location to each member of the current
     * DE object's population and create a new DE object with this new
     * population.
     * @param params run-time parameters to control behaviour
     */
    public void addPhoto(RunParameters params) {
	// find mean camera position and standard deviation
	Stats[] s = populationStats();
	NVector center = new NVector(s[0].getMean(),
				     s[1].getMean(),
				     s[2].getMean());
	double stdev = Math.sqrt((s[0].getVariance() +
				  s[1].getVariance() +
				  s[2].getVariance())/3);

	// fetch existing vectors
	ParameterSet[] vectors = de.getAllVectors();
	
	// create new DE object
	DE newde = new DE(data);
	newde.setNumberOfThreads(((Number)params.get("threads")).intValue());
	newde.F = ((Number)params.get("F",new Double(de.F))).doubleValue();
	newde.CR = ((Number)params.get("CR",new Float(de.CR))).floatValue();
	de=newde;
	
	int NP = ((Number)params.get("NP",new Integer(0))).intValue();
	if (NP<vectors.length) NP=vectors.length;
	
	// create population
	for (int j=0; j<NP; ++j) {
	    PointParameters template = 
		(PointParameters)vectors[j%vectors.length];
	    PointParameters pp = new PointParameters(template);
	    pp.addRandomPhoto(center,stdev);
	    de.addVector(pp);
	}
    }

    /**
     * Check if the current population of the DE object needs to have either
     * their width parameters or the positions of the camera for each photo
     * varied by some random amount.
     * @param params run-time parameters to control behaviour
     */
    public void checkPopulation(RunParameters params) {
	de.F = ((Number)params.get("F",new Double(de.F))).doubleValue();
	de.CR = ((Number)params.get("CR",new Double(de.CR))).floatValue();

	ParameterSet[] vectors = de.getAllVectors();
	PointParameters pp = (PointParameters)vectors[0];
	boolean recompute=false;

	// check if width should be varied
	double stdev = ((Number)params.get("widthstdev")).doubleValue();
	if ((pp.varywidth==false)&&(stdev>0)) {
	    // make width variable
	    for (int i=0; i<vectors.length; ++i) {
		pp = (PointParameters)vectors[i];
		pp.varywidth=true;
		if (pp!=de.getParameterSet()) {
		    double width = ((Number)params.
				    get("width",new Double(pp.getWidth()))).
			doubleValue();
		    pp.setWidthRandom(width,stdev);
		}
	    }
	    recompute=true;
	}

	// check if photos should be randomized (except best one)
	stdev = ((Number)params.get("randomize")).doubleValue();
	if (stdev>0) {
	    Stats[] s = populationStats();
	    
	    ParameterSet best = de.getParameterSet();
	    for (int i=0; i<vectors.length; ++i) 
		if (vectors[i]!=best) {
		    pp = (PointParameters)vectors[i];
		    pp.randomizePhotos(s[0].getStandardDeviation()*stdev/2,
				       s[1].getStandardDeviation()*stdev/2,
				       s[2].getStandardDeviation()*stdev/2,
				       Math.PI*stdev/2);
		}
	    
	    // make sure we don't do it again
	    params.put("randomize",new Double(0));
	    recompute=true;
	}

	if (recompute)
	    de.recomputeError();
    }

    /**
     * Main loop for optimization problem.  Photos are added one at a time
     * in the predetermined order (see ObservedData.sort()) and each sub-
     * problem is run for some number of generations as specified by the
     * run-time parameters.
     */
    public void run() {
	System.out.println("Photo order:");
	for (int i=0; i<data.nphotos; ++i)
	    System.out.println("  "+data.photos[i]);
	System.out.println("Point order:");
	for (int i=0; i<data.npoints; ++i)
	    System.out.println("  "+data.points[i]);

	createGraphicalView();

	int nphotos = ((PointParameters)de.getAllVectors()[0]).nphotos;
	RunParameters current = nphotos<data.nphotos ? params : params_last;

	do {
	    // check if population needs to be randomized
	    checkPopulation(params);

	    // solve
	    int mingen = ((Number)current.get("mingen")).intValue();
	    int maxgen = ((Number)current.get("maxgen")).intValue();
	    double mindiv = ((Number)current.get("mindiv")).doubleValue();
	    double maxerr = ((Number)current.get("maxerror")).doubleValue();
	    solveProblem(mingen,maxgen,mindiv,maxerr);
	    System.out.println();

	    // write output files
	    writeOutput();

	    // are we done?
	    if (nphotos>=data.nphotos)
		break;

	    // no, add another photo and continue
	    if (++nphotos==data.nphotos)
		current = params_last;
	    addPhoto(current);

	} while (true);
    }

    /**
     * Print out usage information to the standard output stream.
     */
    public static void usage() {
	System.out.println();
	System.out.println("Usage:\n  FindPoints [ parameters ] datafile");

	System.out.println();
	System.out.println("\t-width     width parameter  ");
	System.out.println("\t-widthstdev variation in width (can be 0)");
	System.out.println("\t-randomize randomize photo positions  ");

	System.out.println();
	System.out.println("\t-F         mutation factor");
	System.out.println("\t-CR        crossover rate");
	System.out.println("\t-NP        size of population");

	System.out.println();
	System.out.println("\t-mingen    minimum number of generations");
	System.out.println("\t-maxgen    maximum number of generations");
	System.out.println("\t-mindiv    minimum allowed divseristy");
	System.out.println("\t-maxerror  maximum allowed error per observation");
	System.out.println("\t-threads   number of worker threads");

	System.out.println();
	System.out.println("  *Note: all parameters take a number argument to immediately follow");
    }

    /**
     * Create an instance of FindPoints class to parse the parameters, then
     * attempt to read the input file, and finally, call the run() method
     * to solve the optimization problem.
     * @param args run-time arguments
     */
    public static void main(String[] args) throws IOException {
	if (args.length==0) {
	    usage();
	    return;
	}

	// create an instance and proceed there
	FindPoints fp = null;
	try {
	    fp = new FindPoints(args);
	}
	catch (IllegalArgumentException e) {
	    System.err.println(e);
	    usage();
	    return;
	}

	if (fp.inputfile==null) {
	    System.err.println("Must specify an input file.");
	    return;
	}

	try {
	    if ((!fp.readDEFile())&&(!fp.readTextDataFile())) {
		System.err.println("Failed to read data file!");
		return;
	    }
	}
	catch (FileNotFoundException e) {
	    System.err.println(e);
	    return;
	}

	fp.run();
    }
}
