/* Copyright 2009-2014 David Hadka
 *
 * This file is part of the MOEA Framework.
 *
 * The MOEA Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * The MOEA Framework 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 Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the MOEA Framework.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.moeaframework.problem;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.InetAddress;
import java.net.UnknownHostException;

import org.moeaframework.core.Problem;
import org.moeaframework.core.Solution;
import org.moeaframework.core.Variable;
import org.moeaframework.core.variable.BinaryVariable;
import org.moeaframework.core.variable.Permutation;
import org.moeaframework.core.variable.RealVariable;
import org.moeaframework.util.io.RedirectStream;

/**
 * Evaluate solutions using an externally-defined problem.  Two modes of
 * operation are supported: standard I/O and sockets.
 * 
 * <h4>Standard I/O Mode</h4>
 * Standard I/O is the easiest mode to setup and run.  First, an executable
 * program on the computer is launched by invoking the constructor with the
 * program name (and any optional arguments):
 * <pre>
 *   new ExternalProblem("./problem.exe", "arg1", "arg2") { ... }
 * </pre>
 * Then, solutions are sent to the process on its standard input (stdin) stream,
 * and the objectives and constraints are read from its standard output (stdout)
 * stream.
 * <p>
 * The program can not use the standard I/O for any other purpose.  Programs
 * which read from or write to the standard I/O streams should instead use
 * sockets, as discussed below.
 * 
 * <h4>Socket Mode</h4>
 * Socket mode is more complicated to setup, but is more flexible and robust.
 * It has the ability to not only evaluate the problem on the host computer,
 * but can be spread across a computer network.  To use sockets, use either the
 * {@link #ExternalProblem(String, int)} or
 * {@link #ExternalProblem(InetAddress, int)} constructor.
 * 
 * <h4>C/C++ Interface</h4>
 * A C/C++ interface is provided for implementing problems.  This interface
 * supports both modes of communication, depending on which initialization
 * routine is invoked.  See the {@code moeaframework.c} and
 * {@code moeaframework.h} files in the {@code examples/} folder for details.
 * This interface conforms to the communication protocol described below.
 * <p>
 * The communication protocol consists of sending decision variables to the
 * external process, and the process responding with the objectives and
 * constraints.  The decision variables line consists of one or more variables
 * separated by whitespace and terminated by a newline. The process evaluates
 * the problem for the given variables and outputs the objectives separated by
 * whitespace and terminated by a newline. If the problem also has constraints,
 * each constraint is returned after the objectives on the same line.  The
 * process must only terminate when the end of stream is reached. In addition,
 * the process should flush the output stream to ensure the output is processed
 * immediately.
 * <p>
 * Whitespace is one or more spaces, tabs or any combination thereof. The
 * newline is either the line feed ('\n'), carriage return ('\r') or a carriage
 * return followed immediately by a line feed ("\r\n"). 
 * <p>
 * <b>It is critical that the {@link #close()} method be invoked to ensure the
 * external process is shutdown cleanly.</b>
 */
public abstract class ExternalProblem implements Problem {
	
	/**
	 * The default port used by the MOEA Framework to connect to remote
	 * evaluation processes via sockets.
	 */
	public static final int DEFAULT_PORT = 16801;

	/**
	 * Reader connected to the process' standard output.
	 */
	private final BufferedReader reader;

	/**
	 * Writer connected to the process' standard input.
	 */
	private final BufferedWriter writer;

	/**
	 * Constructs an external problem using {@code new
	 * ProcessBuilder(command).start()}.  If the command contains arguments,
	 * the arguments should be passed in as separate strings, such as
	 * <pre>
	 *   new ExternalProblem("command", "arg1", "arg2");
	 * </pre>
	 * 
	 * @param command a specified system command
	 * @throws IOException if an I/O error occured
	 */
	public ExternalProblem(String... command) throws IOException {
		this(new ProcessBuilder(command).start());
	}
	
	/**
	 * Constructs an external problem that connects to a remote process via
	 * sockets.  The remote process should be instantiated and already
	 * listening to the designated port number prior to invoking this 
	 * constructor.
	 * 
	 * @param host the host name of the remote system; or {@code null} to use
	 *        the local host
	 * @param port the port number
	 * @throws UnknownHostException if the IP address of the specified host
	 *         could not be determined
	 * @throws IOException if an I/O error occurred
	 */
	public ExternalProblem(String host, int port) throws IOException, 
	UnknownHostException {
		this(new Socket(host, port));
	}
	
	/**
	 * Constructs an external problem that connects to a remote process via
	 * sockets.  The remote process should be instantiated and already
	 * listening to the designated port number prior to invoking this
	 * constructor.
	 * 
	 * @param address the IP address of the remote system
	 * @param port the port number
	 * @throws IOException if an I/O error occurred
	 */
	public ExternalProblem(InetAddress address, int port) throws IOException {
		this(new Socket(address, port));
	}
	
	/**
	 * Constructs an external problem using the specified socket.
	 * 
	 * @param socket the socket used to send solutions to be evaluated
	 * @throws IOException if an I/O error occurred
	 */
	ExternalProblem(Socket socket) throws IOException {
		this(socket.getInputStream(), socket.getOutputStream());
	}

	/**
	 * Constructs an external problem using the specified process.
	 * 
	 * @param process the process used to evaluate solutions
	 */
	ExternalProblem(Process process) {
		this(process.getInputStream(), process.getOutputStream());
		RedirectStream.redirect(process.getErrorStream(), System.err);
	}
	
	/**
	 * Constructs an external problem using the specified input and output 
	 * streams.
	 * 
	 * @param input the input stream
	 * @param output the output stream
	 */
	ExternalProblem(InputStream input, OutputStream output) {
		super();
		reader = new BufferedReader(new InputStreamReader(input));
		writer = new BufferedWriter(new OutputStreamWriter(output));
	}

	/**
	 * Closes the connection to the process. No further invocations of
	 * {@code evaluate} are permitted.
	 */
	@Override
	public synchronized void close() {
		try {
			writer.close();
		} catch (IOException e) {
			throw new ProblemException(this, e);
		} finally {
			try {
				reader.close();
			} catch (IOException e) {
				throw new ProblemException(this, e);
			}
		}
	}

	/**
	 * Evaluates the specified solution using the process defined by this class'
	 * constructor.
	 * 
	 * @param solution the solution to evaluate
	 */
	@Override
	public synchronized void evaluate(Solution solution) 
	throws ProblemException {
		// send variables to external process
		try {
			writer.write(encode(solution.getVariable(0)));
			for (int i = 1; i < solution.getNumberOfVariables(); i++) {
				writer.write(" ");
				writer.write(encode(solution.getVariable(i)));
			}
			writer.newLine();
			writer.flush();
		} catch (IOException e) {
			throw new ProblemException(this, "error sending variables to external process", e);
		}

		// receive objectives from external process
		try {
			String line = reader.readLine();

			if (line == null) {
				throw new ProblemException(this, "end of stream reached when response expected");
			}

			String[] tokens = line.split("\\s+");

			if (tokens.length != (solution.getNumberOfObjectives() + 
					solution.getNumberOfConstraints())) {
				throw new ProblemException(this, "response contained fewer tokens than expected");
			}
			
			int index = 0;

			for (int i = 0; i < solution.getNumberOfObjectives(); i++) {
				solution.setObjective(i, Double.parseDouble(tokens[index]));
				index++;
			}
			
			for (int i = 0; i < solution.getNumberOfConstraints(); i++) {
				solution.setConstraint(i, Double.parseDouble(tokens[index]));
				index++;
			}
		} catch (IOException e) {
			throw new ProblemException(this, "error receiving variables from external process", e);
		} catch (NumberFormatException e) {
			throw new ProblemException(this, "error receiving variables from external process", e);
		}
	}

	/**
	 * Serializes a variable to a string form.
	 * 
	 * @param variable the variable whose value is serialized
	 * @return the serialized version of the variable
	 * @throws IOException if an error occurs during serialization
	 */
	private String encode(Variable variable) throws IOException {
		StringBuilder sb = new StringBuilder();
		
		if (variable instanceof RealVariable) {
			RealVariable rv = (RealVariable)variable;
			sb.append(rv.getValue());
		} else if (variable instanceof BinaryVariable) {
			BinaryVariable bv = (BinaryVariable)variable;
			
			for (int i=0; i<bv.getNumberOfBits(); i++) {
				sb.append(bv.get(i) ? "1" : "0");
			}
		} else if (variable instanceof Permutation) {
			Permutation p = (Permutation)variable;

			for (int i=0; i<p.size(); i++) {
				if (i > 0) {
					sb.append(',');
				}
				
				sb.append(p.get(i));
			}
		} else {
			throw new IOException("unable to serialize variable");
		}
		
		return sb.toString();
	}

}
