/*****************************************************************************
 * 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 the CVS Client Library.
 * The Initial Developer of the Original Code is Robert Greig.
 * Portions created by Robert Greig are Copyright (C) 2000.
 * All Rights Reserved.

 * Contributor(s): Robert Greig.
 *****************************************************************************/
package org.netbeans.lib.cvsclient.connection;

import org.jetbrains.annotations.NonNls;
import org.netbeans.lib.cvsclient.JavaCvsSrcBundle;
import org.netbeans.lib.cvsclient.io.IStreamLogger;
import org.netbeans.lib.cvsclient.io.StreamUtilities;
import org.netbeans.lib.cvsclient.util.BugLog;

import java.io.*;
import java.net.*;

/**
 * Implements a connection to a pserver. See the cvs documents for more
 * information about different connection methods. PServer is popular where
 * security is not an issue. For secure connections, consider using a
 * kserver (Kerberos) or the GSSAPI.
 *
 * @author Robert Greig
 */
public final class PServerConnection
  implements IConnection {

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

  private final String userName;
  private final String encodedPassword;
  private final String repository;
  private final ConnectionSettings connectionSettings;

  private Socket socket;
  private InputStream socketInputStream;
  private OutputStream socketOutputStream;
  @NonNls private static final String ENCODED_PASSWORD_OUTPUT_MESSAGE = "@encodedPassword@";
  @NonNls private static final String SUCCESS_MESSAGE = "I LOVE YOU";
  @NonNls private static final String FAILED_MESSAGE = "I HATE YOU";
  // Setup ==================================================================

  public PServerConnection(ConnectionSettings connectionSettings,
                           String userName,
                           String encodedPassword,
                           String repository) {
    BugLog.getInstance().assertNotNull(userName);
    BugLog.getInstance().assertNotNull(repository);
    BugLog.getInstance().assertTrue(connectionSettings.getConnectionTimeout() >= 0, "Timeout must be >= 0");

    this.userName = userName;
    this.encodedPassword = (encodedPassword != null) ? encodedPassword : "";
    this.repository = repository;
    this.connectionSettings = connectionSettings;
  }

  // Implemented ============================================================)

  public InputStream getInputStream() {
    return socketInputStream;
  }

  public OutputStream getOutputStream() {
    return socketOutputStream;
  }

  public String getRepository() {
    return repository;
  }

  public void verify(IStreamLogger streamLogger) throws AuthenticationException {
    try {
      open(streamLogger);
    }
    finally {
      close();
    }
  }

  public void open(IStreamLogger streamLogger) throws AuthenticationException {
    openConnection("BEGIN AUTH REQUEST", "END AUTH REQUEST", streamLogger);
  }

  public void close() {
    if (socketInputStream != null) {
      try {
        socketInputStream.close();
      }
      catch (IOException ex) {
        BugLog.getInstance().showException(ex);
      }
      finally {
        socketInputStream = null;
      }
    }

    if (socketOutputStream != null) {
      try {
        socketOutputStream.close();
      }
      catch (IOException ex) {
        BugLog.getInstance().showException(ex);
      }
      finally {
        socketOutputStream = null;
      }
    }

    if (socket != null) {
      try {
        socket.close();
      }
      catch (IOException ex) {
        // ignore
      }
      finally {
        socket = null;
      }
    }
  }

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

  /**
   * Authenticate a connection with the server, using the specified
   * postamble and preamble.
   *
   * @param preamble  the preamble to use
   * @param postamble the postamble to use
   * @throws AuthenticationException if an error occurred
   *                                 Return the socket used to make the connection. The socket is
   *                                 guaranteed to be open if an exception has not been thrown
   */
  private void openConnection(@NonNls String preamble, @NonNls String postamble, IStreamLogger streamLogger) throws AuthenticationException {
    boolean error = true;

    try {
      createSocket();

      this.socketOutputStream = new BufferedOutputStream(socket.getOutputStream());
      this.socketInputStream = new BufferedInputStream(socket.getInputStream());

      final OutputStream loggingOutputStream = streamLogger.createLoggingOutputStream(this.socketOutputStream);
      final InputStream loggingInputStream = streamLogger.createLoggingInputStream(this.socketInputStream);

      writeLn(loggingOutputStream, preamble, "US-ASCII");
      writeLn(loggingOutputStream, repository);
      writeLn(loggingOutputStream, userName);

      writeLn(socketOutputStream, encodedPassword, "US-ASCII");
      writeLn(streamLogger.getOutputLogStream(), ENCODED_PASSWORD_OUTPUT_MESSAGE);

      writeLn(loggingOutputStream, postamble, "US-ASCII");
      loggingOutputStream.flush();

      String response = new StreamUtilities(null).readLine(loggingInputStream);
      if (response.equals(SUCCESS_MESSAGE)) {
        error = false;
        return;
      }

      if (response.length() == 0) {
        throw new AuthenticationException(JavaCvsSrcBundle.message("no.response.from.server.error.message"));
      }

      if (response.equals(FAILED_MESSAGE)) {
        throw new UnknownUserException(JavaCvsSrcBundle.message("wrong.password.or.unknown.user.error.message"));
      }

      response = removePrefix(response, "error ");
      response = removePrefix(response, "E ");
      throw new AuthenticationException(JavaCvsSrcBundle.message("authentication.failed.error.message", response));
    }
    catch (ConnectException ex) {
      throw new AuthenticationException(JavaCvsSrcBundle.message("cannot.connect.to.host.error.message",
                                                                 connectionSettings.getHostName()), ex);
    }
    catch (NoRouteToHostException ex) {
      throw new AuthenticationException(JavaCvsSrcBundle.message("no.route.to.host.error.message",
                                                                 connectionSettings.getHostName()), ex);
    }
    catch (UnknownHostException ex) {
      throw new AuthenticationException(JavaCvsSrcBundle.message("unknown.host.error.message",
                                                                 connectionSettings.getHostName()), ex);
    }
    catch (SocketTimeoutException ex) {
      throw new AuthenticationException(JavaCvsSrcBundle.message("timeout.error.message",
                                                                 connectionSettings.getHostName()), ex);
    }
    catch (IOException ex) {
      throw new AuthenticationException(JavaCvsSrcBundle.message("i.o.error.while.connecting.to.host.error.message",
                                                                 connectionSettings.getHostName()), ex);
    }
    finally {
      if (error) {
        close();
      }
    }
  }

  private static void writeLn(final OutputStream outputStream, final String line, @NonNls final String encoding) throws IOException {
    final String line1 = line + "\n";
    outputStream.write(line1.getBytes(encoding));
  }

  private static void writeLn(final OutputStream outputStream, final String line) throws IOException {
    final String line1 = line + "\n";
    outputStream.write(line1.getBytes());
  }

  private void createSocket() throws IOException {
    if (connectionSettings.isUseProxy()) {
      this.socket = connectionSettings.createProxyTransport();
    }
    else {
      this.socket = new Socket();
      final InetSocketAddress address =
        new InetSocketAddress(connectionSettings.getHostName(), connectionSettings.getPort());
      this.socket.connect(address, connectionSettings.getConnectionTimeout());
      this.socket.setSoTimeout(connectionSettings.getConnectionTimeout());
    }
  }

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

  private static String removePrefix(String response, @NonNls String prefix) {
    if (response.length() > prefix.length()
        && response.startsWith(prefix)) {
      return response.substring(prefix.length());
    }
    return response;
  }
}
