package net.pterodactylus.util.telnet;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.pterodactylus.util.io.Closer;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.service.AbstractService;
import net.pterodactylus.util.telnet.Command.Reply;
import net.pterodactylus.util.text.StringEscaper;
import net.pterodactylus.util.text.TextException;

/**
 * Handles a single client connection.
 *
 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
 */
public class ControlConnection extends AbstractService {

	/** The logger. */
	private static final Logger logger = Logging.getLogger(ControlConnection.class.getName());

	/** The line break. */
	private static final String LINEFEED = "\r\n";

	/** The client’s input stream. */
	private final InputStream clientInputStream;

	/** The client’s output stream. */
	private final OutputStream clientOutputStream;

	/** The output stream writer. */
	private final PrintWriter outputStreamWriter;

	/** Mapping from command names to commands. */
	Map<String, Command> commands = new HashMap<String, Command>();

	/** Mapping from internal command names to commands. */
	Map<String, Command> internalCommands = new HashMap<String, Command>();

	/**
	 * Creates a new connection handler for a client on the given socket.
	 *
	 * @param clientInputStream
	 *            The client input stream
	 * @param clientOutputStream
	 *            The client output stream
	 */
	public ControlConnection(InputStream clientInputStream, OutputStream clientOutputStream) {
		this.clientInputStream = clientInputStream;
		this.clientOutputStream = clientOutputStream;
		this.outputStreamWriter = new PrintWriter(clientOutputStream);
		addCommand(new QuitCommand());
	}

	//
	// ACCESSORS
	//

	/**
	 * Adds the given command to this control.
	 *
	 * @param command
	 *            The command to add
	 */
	public void addCommand(Command command) {
		commands.put(command.getName().toLowerCase(), command);
		internalCommands.put("help", new HelpCommand(commands.values()));
	}

	//
	// ACTIONS
	//

	/**
	 * Prints the given line to the output stream.
	 *
	 * @param line
	 *            The line to print
	 */
	public void addOutputLine(String line) {
		addOutputLines(line);
	}

	/**
	 * Prints the given lines to the output stream.
	 *
	 * @param lines
	 *            The lines to print
	 */
	public void addOutputLines(String... lines) {
		synchronized (outputStreamWriter) {
			for (String line : lines) {
				outputStreamWriter.println(line);
			}
			outputStreamWriter.flush();
		}
	}

	//
	// SERVICE METHODS
	//

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void serviceRun() {
		InputStreamReader inputStreamReader = null;
		BufferedReader bufferedReader = null;
		try {
			inputStreamReader = new InputStreamReader(clientInputStream);
			bufferedReader = new BufferedReader(inputStreamReader);
			String line;
			boolean finished = false;
			while (!finished && ((line = bufferedReader.readLine()) != null)) {
				line = line.trim();
				if (line.length() == 0) {
					continue;
				}
				List<String> words;
				try {
					words = StringEscaper.parseLine(line);
				} catch (TextException te1) {
					writeReply(new Reply(Reply.BAD_REQUEST).addLine("Syntax error."));
					continue;
				}
				if (words.isEmpty()) {
					continue;
				}
				String commandName = words.remove(0).toLowerCase();
				List<Command> foundCommands = findCommand(commandName);
				if (foundCommands.isEmpty()) {
					writeReply(new Reply(Reply.NOT_FOUND).addLine("Command not found."));
				} else if (foundCommands.size() == 1) {
					Command command = foundCommands.get(0);
					try {
						Reply commandReply = command.execute(words);
						writeReply(commandReply);
					} catch (IOException ioe1) {
						throw ioe1;
					} catch (Throwable t1) {
						writeReply(new Reply(Reply.INTERNAL_SERVER_ERROR).addLine("Internal server error: " + t1.getMessage()));
					}
					if (command instanceof QuitCommand) {
						finished = true;
					}
				} else {
					Reply reply = new Reply(Reply.MULTIPLE_CHOICES, "Multiple choices found:");
					for (Command command : foundCommands) {
						reply.addLine(command.getName());
					}
					writeReply(reply);
				}
			}
		} catch (IOException ioe1) {
			logger.log(Level.INFO, "could not handle connection", ioe1);
		} finally {
			Closer.close(outputStreamWriter);
			Closer.close(bufferedReader);
			Closer.close(inputStreamReader);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void serviceStop() {
		Closer.close(clientInputStream);
		Closer.close(clientOutputStream);
	}

	//
	// PRIVATE METHODS
	//

	/**
	 * Searches both internal and user commands for a command. A command must
	 * have a name that equals or starts with the given name to be a match.
	 *
	 * @param name
	 *            The name of the command
	 * @return All found commands
	 */
	private List<Command> findCommand(String name) {
		List<Command> foundCommands = new ArrayList<Command>();
		for (Command command : internalCommands.values()) {
			if (command.getName().toLowerCase().startsWith(name.toLowerCase())) {
				foundCommands.add(command);
			}
		}
		for (Command command : commands.values()) {
			if (command.getName().toLowerCase().startsWith(name.toLowerCase())) {
				foundCommands.add(command);
			}
		}
		return foundCommands;
	}

	/**
	 * Writes the given reply to the client’s output stream. The
	 * <code>reply</code> may be <code>null</code> in which case an appropriate
	 * error message is written.
	 *
	 * @param reply
	 *            The reply to send
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	private void writeReply(Reply reply) throws IOException {
		synchronized (outputStreamWriter) {
			if (reply == null) {
				outputStreamWriter.write("500 Internal server error." + LINEFEED);
				outputStreamWriter.flush();
				return;
			}
			int status = reply.getStatus();
			List<String> lines = reply.getLines();
			for (int lineIndex = 0, lineCount = lines.size(); lineIndex < lineCount; lineIndex++) {
				outputStreamWriter.write(status + ((lineIndex < (lineCount - 1)) ? "-" : " ") + lines.get(lineIndex) + LINEFEED);
			}
			if (lines.size() == 0) {
				outputStreamWriter.write("200 OK." + LINEFEED);
			}
			outputStreamWriter.flush();
		}
	}

}