/*******************************************************************************
 * Copyright (c) 2000, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Atsuhiko Yamanaka, JCraft,Inc. - initial API and implementation.
 *     IBM Corporation - ongoing maintenance
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.ssh2;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.NoRouteToHostException;
import java.net.UnknownHostException;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.internal.ccvs.core.ICVSRepositoryLocation;
import org.eclipse.team.internal.ccvs.core.IServerConnection;
import org.eclipse.team.internal.ccvs.core.connection.CVSAuthenticationException;
import org.eclipse.team.internal.core.streams.PollingInputStream;
import org.eclipse.team.internal.core.streams.PollingOutputStream;
import org.eclipse.team.internal.core.streams.TimeoutInputStream;
import org.eclipse.team.internal.core.streams.TimeoutOutputStream;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;

/**
 * SSH2 connection method. Has the property of defaulting to SSH1 if the server
 * doesn't support SSH2. 
 */
public class CVSSSH2ServerConnection implements IServerConnection {
	
	private static final String SSH1_COMPATIBILITY_CLASS = "org.eclipse.team.internal.ccvs.ssh.SSHServerConnection"; //$NON-NLS-1$
	
	private final class SSH2IOException extends IOException {
        private static final long serialVersionUID = 1L;

        private final JSchException e;

        SSH2IOException(String s, JSchException e) {
            super(s);
            this.e = e;
        }

        public Throwable getCause() {
            return e;
        }
    }
    private static final String COMMAND = "cvs server"; //$NON-NLS-1$
	private ICVSRepositoryLocation location;
	private String password;
	private InputStream inputStream;
	private OutputStream outputStream;
	private JSchSession session;
	private Channel channel;
	private IServerConnection ssh1;
	
	protected CVSSSH2ServerConnection(ICVSRepositoryLocation location, String password) {
		this.location = location;
		this.password = password;
	}
	public void close() throws IOException {
		if (ssh1 != null) {
			ssh1.close();
			ssh1 = null;
			return;
		}
		try {
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (IOException e) {
					// Ignore I/O Exception on close
				}
			}
		} finally {
			try {
				if (outputStream != null) {
					try {
						outputStream.close();
					} catch (IOException e) {
						// Ignore I/O Exception on close
					}
				}
			} finally {
				if (channel != null)
					channel.disconnect();
			}
		} 
	}
	public InputStream getInputStream() {
		if (ssh1 != null) {
			return ssh1.getInputStream();
		}
		return inputStream;
	}
	public OutputStream getOutputStream() {
		if (ssh1 != null) {
			return ssh1.getOutputStream();
		}
		return outputStream;
	}
	public void open(IProgressMonitor monitor) throws IOException, CVSAuthenticationException {
		if (ssh1 != null) {
			ssh1.open(monitor);
			return;
		}
		monitor.subTask(NLS.bind(CVSSSH2Messages.CVSSSH2ServerConnection_open, new String[] { location.getHost() })); 
		monitor.worked(1);
		internalOpen(monitor);
	}
	/**
	 * @param monitor
	 * @throws IOException
	 * @throws CVSAuthenticationException
	 */
	private void internalOpen(IProgressMonitor monitor) throws IOException, CVSAuthenticationException {
		try {
			OutputStream channel_out = null;
			InputStream channel_in = null;
            boolean firstTime = true;
            boolean tryAgain = false;
			while (firstTime || tryAgain) {
                tryAgain = false; // reset the try again flag
				session = JSchSession.getSession(location, location.getUsername(), password, location.getHost(), location.getPort(), monitor);
				channel = session.getSession().openChannel("exec"); //$NON-NLS-1$
				((ChannelExec) channel).setCommand(COMMAND);
				channel_out = channel.getOutputStream();
				channel_in = channel.getInputStream();
				try {
					channel.connect();
				} catch (JSchException ee) {
                    // This strange logic is here due to how the JSch client shares sessions.
                    // It is possible that we have obtained a session that thinks it is connected
                    // but is not. Channel connection only works if the session is connected so the
                    // above channel connect may fail because the session is down. For this reason,
                    // we want to retry if the connection fails.
                    try {
                        if (firstTime && (isSessionDownError(ee) || isChannelNotOpenError(ee))) {
                            tryAgain = true;
                        }
                        if (!tryAgain) {
                            throw ee;
                        }
                    } finally {
                        // Always dispose of the current session when a failure occurs so we can start from scratch
                        session.dispose();
                    }
				}
                firstTime = false; // the first time is done
			}
			int timeout = location.getTimeout();
			inputStream = new PollingInputStream(new TimeoutInputStream(new FilterInputStream(channel_in) {
						public void close() {
							// Don't close the underlying stream as it belongs to the session
						}
					},
					8192 /*bufferSize*/, (timeout>0 ? 1000 : 0) /*readTimeout*/, -1 /*closeTimeout*/, true /* growWhenFull */), timeout > 0 ? timeout : 1, monitor);
			outputStream = new PollingOutputStream(new TimeoutOutputStream(new FilterOutputStream(channel_out) {
						public void close() {
							// Don't close the underlying stream as it belongs to the session
						}
					},
					8192 /*buffersize*/, (timeout>0 ? 1000 : 0) /*writeTimeout*/, (timeout>0 ? 1000 : 0) /*closeTimeout*/), timeout > 0 ? timeout : 1, monitor);
		} catch (final JSchException e) {
			if (isSSH2Unsupported(e)) {
				ssh1 = createSSH1Connection();
				if (ssh1 == null) {
					throw new SSH2IOException(
							CVSSSH2Messages.CVSSSH2ServerConnection_4, e);
				}
				ssh1.open(monitor);
            } else {
			    String message = e.getMessage();
			    if (JSchSession.isAuthenticationFailure(e)) {
                    // Do not retry as the Jsh library has it's own retry logic
                    throw new CVSAuthenticationException(CVSSSH2Messages.CVSSSH2ServerConnection_0, CVSAuthenticationException.NO_RETRY,location, e); 
			    } else if (message.startsWith("Session.connect: ")) { //$NON-NLS-1$
			        // Jsh has messages formatted like "Session.connect: java.net.NoRouteToHostException: ..."
			        // Strip of the exception and try to convert it to a more meaningfull string
			        int start = message.indexOf(": ") + 1; //$NON-NLS-1$
			        if (start != -1) {
				        int end = message.indexOf(": ", start); //$NON-NLS-1$
				        if (end != -1) {
				            String exception = message.substring(start, end).trim();
				            if (exception.indexOf("NoRouteToHostException") != -1) { //$NON-NLS-1$
				                message = NLS.bind(CVSSSH2Messages.CVSSSH2ServerConnection_1, new String[] { location.getHost() }); 
				                throw new NoRouteToHostException(message);
				            } else if (exception.indexOf("java.net.UnknownHostException") != -1) { //$NON-NLS-1$
				                throw new UnknownHostException(location.getHost());
				            } else {
				                message = message.substring(end + 1).trim();
				            }
				        }
			        }
			    }
 				throw new SSH2IOException(message, e);
			}
		}
	}
	
	/**
	 * Returns SSH-1 connection.
	 * 
	 * @return a connection or <code>null</code>, if SSH-1 is not supported
	 */
	private IServerConnection createSSH1Connection() {
		try {
			return (IServerConnection) Class.forName(SSH1_COMPATIBILITY_CLASS)
					.getConstructor(
							new Class[] { ICVSRepositoryLocation.class,
									String.class }).newInstance(
							new Object[] { location, password });
		} catch (IllegalArgumentException e1) {
			if (Policy.DEBUG)
				e1.printStackTrace();
		} catch (SecurityException e1) {
			if (Policy.DEBUG)
				e1.printStackTrace();
		} catch (InstantiationException e1) {
			if (Policy.DEBUG)
				e1.printStackTrace();
		} catch (IllegalAccessException e1) {
			if (Policy.DEBUG)
				e1.printStackTrace();
		} catch (InvocationTargetException e1) {
			if (Policy.DEBUG)
				e1.printStackTrace();
		} catch (NoSuchMethodException e1) {
			if (Policy.DEBUG)
				e1.printStackTrace();
		} catch (ClassNotFoundException e1) {
			if (Policy.DEBUG)
				e1.printStackTrace();
		}
		return null;
	}
    
    private boolean isChannelNotOpenError(JSchException ee) {
        return ee.getMessage().indexOf("channel is not opened") != -1; //$NON-NLS-1$
    }
    private boolean isSessionDownError(JSchException ee) {
        return ee.getMessage().equals("session is down"); //$NON-NLS-1$
    }
    private boolean isSSH2Unsupported(JSchException e) {
        return e.toString().indexOf("invalid server's version string") != -1; //$NON-NLS-1$
    }
}
