/*
 *                 Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 *
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.netbeans.lib.cvsclient;

import java.io.*;
import java.util.*;

import org.netbeans.lib.cvsclient.command.*;
import org.netbeans.lib.cvsclient.connection.*;
import org.netbeans.lib.cvsclient.event.*;
import org.netbeans.lib.cvsclient.file.*;
import org.netbeans.lib.cvsclient.io.*;
import org.netbeans.lib.cvsclient.progress.sending.*;
import org.netbeans.lib.cvsclient.request.*;
import org.netbeans.lib.cvsclient.response.*;
import org.netbeans.lib.cvsclient.util.*;

/**
 * @author Thomas Singer
 */
public final class RequestProcessor
		implements IRequestProcessor {

	// Constants ==============================================================

	private static final ILogger LOG = LoggerManager.getLogger("javacvs.requestprocessor");

	// Fields =================================================================

	private final ValidRequests validRequests = new ValidRequests();
	private final IStreamLogger streamLogger;
	private final ICvsCommandStopper commandStopper;
	private final IEventSender eventSender;

	// Setup ==================================================================

	public RequestProcessor(IEventSender eventSender, IStreamLogger streamLogger, ICvsCommandStopper commandStopper) {
		BugLog.assertNotNull(eventSender);
		BugLog.assertNotNull(streamLogger);
		BugLog.assertNotNull(commandStopper);

		this.streamLogger = streamLogger;
		this.commandStopper = commandStopper;
		this.eventSender = eventSender;
	}

	// Implemented ============================================================

	public boolean communicateWithServer(ICommandRequestFactory commandRequestFactory,
	                                     final IRequestsProgressHandler requestsProgressHandler,
	                                     final IClientEnvironment clientEnvironment,
	                                     IGlobalOptions globalOptions) throws CommandException {
		final ResponseExpectingRequest responseExpectingRequest = commandRequestFactory.getResponseExpectingRequest();

		final IConnectionStreams connectionStreams = openConnection(globalOptions, clientEnvironment);
		try {
			if (!validRequests.isValid(responseExpectingRequest.getRequest())) {
				throw new UnsupportedRequestException(responseExpectingRequest.getRequest());
			}

			LOG.debug("Processing command requests");

			commandRequestFactory.sendCommandRequests(new IRequestSender() {
				private final ILocalFileReader localFileReader = clientEnvironment.getLocalFileReader();

				public void sendRequest(IRequest request) throws IOException {
					RequestProcessor.this.sendRequest(request, connectionStreams);

					final FileDetails fileDetails = request.getFileForTransmission();
					if (fileDetails != null) {
						RequestProcessor.this.sendFile(fileDetails, connectionStreams, localFileReader);
					}

					requestsProgressHandler.requestSent(request);
				}
			}, clientEnvironment);
			sendRequest(responseExpectingRequest, connectionStreams);

			return handleResponses(connectionStreams, DefaultResponseHandler.createInstance(clientEnvironment, eventSender));
		}
		catch (IoAbortedException ex) {
			throw new CommandAbortedException();
		}
		catch (IOException ex) {
			throw new IOCommandException(ex);
		}
		finally {
			LOG.debug("Closing connection");
			connectionStreams.close();
		}
	}

	public Set<String> parseCvsRcFile(String command, IClientEnvironment clientEnvironment) throws CommandException {
		final ConnectionStreams connectionStreams = createConnection(clientEnvironment);
		try {
			handleHeaderRequests(connectionStreams, clientEnvironment);
			if (!validRequests.isValid(CommandRequest.READ_CVSRC2.getRequest())) {
				return Collections.emptySet();
			}

			LOG.debug("Processing command requests");
			sendRequest(CommandRequest.READ_CVSRC2, connectionStreams);

			LOG.debug("Flushing output stream");
			connectionStreams.flushForReading();

			final Reader reader = connectionStreams.getLoggedReader();
			final StringBuffer lineBuffer = new StringBuffer(32);

			LOG.debug("Expecting cvsrc2 data");
			final String responseString = readResponse(reader, lineBuffer, true);
			int cvsRcFileLength = toInt(responseString);
			if (cvsRcFileLength <= 0) {
				return Collections.emptySet();
			}

			final Set<String> options = new HashSet<String>();

			lineBuffer.setLength(0);
			for (int chr = reader.read(); chr >= 0; chr = reader.read()) {
				if (chr == '\n' || chr == '\r') {
					handleCvsRcLine(command, lineBuffer, options);
					lineBuffer.setLength(0);
				}
				else {
					lineBuffer.append((char)chr);
				}

				cvsRcFileLength--;
				if (cvsRcFileLength == 0) {
					break;
				}
			}

			handleCvsRcLine(command, lineBuffer, options);

			return options;
		}
		catch (IoAbortedException ex) {
			throw new CommandAbortedException();
		}
		catch (IOException ex) {
			LOG.error("", ex);
			throw new IOCommandException(ex);
		}
		finally {
			LOG.debug("Closing connection");
			connectionStreams.close();
		}
	}

	// Utils ==================================================================

	private void handleCvsRcLine(String command, StringBuffer lineBuffer, Set<String> options) {
		if (!startsWith(lineBuffer, command)) {
			return;
		}

		options.clear();

		for (final StringTokenizer tokenizer = new StringTokenizer(lineBuffer.substring(command.length()), " ", false); tokenizer.hasMoreTokens();) {
			final String token = tokenizer.nextToken();
			options.add(token);
		}
	}

	private boolean startsWith(StringBuffer buffer, String string) {
		if (buffer.length() < string.length()) {
			return false;
		}

		for (int i = 0; i < string.length(); i++) {
			final char chrBuffer = buffer.charAt(i);
			final char chrString = string.charAt(i);
			if (chrBuffer != chrString) {
				return false;
			}
		}

		return true;
	}

	private int toInt(String responseString) {
		try {
			return Integer.parseInt(responseString);
		}
		catch (NumberFormatException e) {
			return 0;
		}
	}

	private IConnectionStreams openConnection(IGlobalOptions globalOptions, IClientEnvironment clientEnvironment) throws CommandException {
		final ConnectionStreams connectionStreams = createConnection(clientEnvironment);
		boolean exceptionOccured = true;
		try {
			handleHeaderRequests(connectionStreams, clientEnvironment);

			if (clientEnvironment.getClientVersion() != null && validRequests.isValid(ClientVersionRequest.REQUEST)) {
				sendRequest(new ClientVersionRequest(clientEnvironment.getClientVersion()), connectionStreams);
				connectionStreams.flushForReading();
				final StringBuffer responseBuffer = new StringBuffer();
				readResponse(connectionStreams.getLoggedReader(), responseBuffer, false);
				LOG.debug("server version = " + responseBuffer.toString());
			}

			// Handle gzip-compression
			if (globalOptions.isUseGzip() && validRequests.isValid(GzipStreamRequest.REQUEST)) {
				sendRequest(new GzipStreamRequest(), connectionStreams);
				LOG.debug("Switching to gzipped transfer");
				connectionStreams.setGzipped();
			}

			//TODO: set variables

			sendGlobalOptionRequests(globalOptions, connectionStreams);

			if (System.getProperty("os.name").startsWith("Windows") && validRequests.isValid(CaseRequest.REQUEST)) {
				sendRequest(new CaseRequest(), connectionStreams);
			}

			if (clientEnvironment.isUtf8TextFileTransmission()) {
				LOG.debug("Enabling UTF-8-transfer");
				connectionStreams.setUtf8();
			}

			exceptionOccured = false;
			return connectionStreams;
		}
		catch (IoAbortedException ex) {
			throw new CommandAbortedException();
		}
		catch (IOException ex) {
			LOG.error("", ex);
			throw new IOCommandException(ex);
		}
		finally {
			if (exceptionOccured) {
				connectionStreams.close();
			}
		}
	}

	private ConnectionStreams createConnection(IClientEnvironment clientEnvironment) throws CommandException {
		LOG.debug("Creating connection");
		final IConnection connection;
		try {
			connection = clientEnvironment.getConnectionFactory().createConnection(commandStopper, streamLogger);
		}
		catch (AuthenticationException ex) {
			throw new CommandException(ex, "Could not establish connection!");
		}

		return new ConnectionStreams(connection, streamLogger);
	}

	private void handleHeaderRequests(ConnectionStreams connectionStreams, IClientEnvironment clientEnvironment) throws IOException, CommandException {
		sendRequest(new ValidResponsesRequest(), connectionStreams);

		updateValidRequests(connectionStreams);

		sendRequest(new UseUnchangedRequest(), connectionStreams);

		sendRequest(new RootRequest(clientEnvironment.getCvsRoot().getRepositoryPath()), connectionStreams);
	}

	private void updateValidRequests(IConnectionStreams connectionStreams) throws CommandException, IOException {
		sendRequest(new ValidRequestsRequest(), connectionStreams);
		handleResponses(connectionStreams, ValidRequestsResponseHandler.createInstance(validRequests));

		if (!validRequests.hasValidRequests()) {
			throw new ValidRequestsExpectedException();
		}
	}

	private void sendGlobalOptionRequests(IGlobalOptions globalOptions, IConnectionStreams connectionStreams) throws IOException {
		if (!validRequests.isValid(GlobalOptionRequest.REQUEST)) {
			return;
		}

		if (globalOptions.isCheckedOutFilesReadOnly()) {
			sendRequest(new GlobalOptionRequest("-r"), connectionStreams);
		}
		if (globalOptions.isDoNoChanges()) {
			sendRequest(new GlobalOptionRequest("-n"), connectionStreams);
		}
		if (globalOptions.isNoHistoryLogging()) {
			sendRequest(new GlobalOptionRequest("-l"), connectionStreams);
		}
		if (globalOptions.isSomeQuiet()) {
			sendRequest(new GlobalOptionRequest("-q"), connectionStreams);
		}
	}

	private void sendRequest(IRequest request, IConnectionStreams connectionStreams) throws IOException {
		checkCanceled();
		LOG.debug("Sending request '" + request.getRequestString() + "'");
		connectionStreams.getLoggedWriter().write(request.getRequestString());
	}

	private void checkCanceled() throws IoAbortedException {
		if (commandStopper.isAborted()) {
			LOG.debug("Cancel detected");
			throw new IoAbortedException();
		}
	}

	private boolean handleResponses(IConnectionStreams connectionStreams, IResponseHandler responseHandler) throws IOException {
		LOG.debug("Flushing output stream");
		connectionStreams.flushForReading();

		LOG.debug("Expecting responses");
		final ResponseParser responseParser = new ResponseParser(responseHandler);
		final StringBuffer responseBuffer = new StringBuffer(32);
		for (; ;) {
			final String responseString = readResponse(connectionStreams.getLoggedReader(), responseBuffer, true);
			LOG.debug("Got response '" + responseString + "'");
			if (responseString.length() == 0) {
				return false;
			}

			final Boolean result = responseParser.processResponse(responseString, connectionStreams);
			if (result != null) {
				return result.booleanValue();
			}

			checkCanceled();
		}
	}

	private void sendFile(FileDetails fileDetails, IConnectionStreams connectionStreams, ILocalFileReader localFileReader) throws IOException {
		final FsFilePath fsFile = fileDetails.getFsFile();

		if (fileDetails.isBinary()) {
			LOG.debug("Sending binary file " + fsFile);
			localFileReader.transmitBinaryFile(fsFile, commandStopper, connectionStreams);
		}
		else {
			LOG.debug("Sending text file " + fsFile);
			localFileReader.transmitTextFile(fsFile, commandStopper, connectionStreams);
		}
	}

	private static String readResponse(Reader reader, StringBuffer responseBuffer, boolean stopAtSpaces) throws IOException {
		responseBuffer.setLength(0);

		for (int chr = reader.read(); chr >= 0; chr = reader.read()) {
			if (chr == '\n') {
				break;
			}

			if (stopAtSpaces && chr == ' ') {
				break;
			}

			responseBuffer.append((char)chr);
		}

		return responseBuffer.toString();
	}
}
