/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.auth;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.TrustManager;

import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.io.SVNRepository;


/**
 * The <b>BasicAuthenticationManager</b> is a simple implementation of
 * <b>ISVNAuthenticationManager</b> for storing and providing credentials without
 * using auth providers. A basic manager simply keeps the user credentials provided. 
 * Also this manager may store a single proxy server options context (for HHTP requests 
 * to go through a particular proxy server).
 * 
 * <p>
 * This manager does not use authentication providers (<b>ISVNAuthenticationProvider</b>) but only 
 * those credentials that was supplied to its constructor. Also this manager never 
 * caches credentials.
 * 
 * <p>
 * This manager is not used in SVNKit internals. You may use a default 
 * manager (how to get it read javadoc for {@link ISVNAuthenticationManager}), 
 * this basic manager or implement your own one.  
 * 
 * @version 1.3
 * @author  TMate Software Ltd.
 * @since   1.2
 * @see     ISVNAuthenticationProvider
 */
public class BasicAuthenticationManager implements ISVNAuthenticationManager, ISVNProxyManagerEx, ISVNSSHHostVerifier {

    /**
     * Creates an auth manager given a user credential - a username 
     * and password. 
     * 
     * @param userName  a username
     * @param password  a password
     * 
     * @since 1.8.9
     */
    public static BasicAuthenticationManager newInstance(String userName, char[] password) {
        return newInstance(new SVNAuthentication[] {
            SVNPasswordAuthentication.newInstance(userName, password, false, null, false),
            SVNSSHAuthentication.newInstance(userName, password, -1, false, null, false),
            SVNUserNameAuthentication.newInstance(userName, false, null, false),
        });        
    }
    
    /**
     * Creates an auth manager given a user credential - a username and 
     * an ssh private key.
     * 
     * @param userName    a username
     * @param keyFile     a private key file
     * @param passphrase  a password to the private key
     * @param portNumber  a port number over which an ssh tunnel is established
     * 
     * @since 1.8.9
     */
    public static BasicAuthenticationManager newInstance(String userName, File keyFile, char[] passphrase, int portNumber) {
        return newInstance(new SVNAuthentication[] {
            SVNSSHAuthentication.newInstance(userName, keyFile, passphrase, portNumber, false, null, false),
            SVNUserNameAuthentication.newInstance(userName, false, null, false),
        });        
    }
    
    /**
     * Creates an auth manager given user credentials to use.
     * 
     * @param credentials user credentials
     * 
     * @since 1.8.9
     */
    public static BasicAuthenticationManager newInstance(SVNAuthentication[] credentials) {
        return new BasicAuthenticationManager(credentials);        
    }

    /**
     * Utility method to acknowledge successful or failed authentication attempt
     */
	public static void acknowledgeAuthentication(boolean accepted, String kind, String realm, SVNErrorMessage errorMessage, SVNAuthentication authentication, SVNURL accessedURL, ISVNAuthenticationManager authManager) throws SVNException {
		if (authManager instanceof ISVNAuthenticationManagerExt) {
			((ISVNAuthenticationManagerExt)authManager).acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication, accessedURL);
		}
		else {
			authManager.acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication);
		}
	}

    private List<SVNAuthentication> myPasswordAuthentications;
    private List<SVNAuthentication> mySSHAuthentications;
    private List<SVNAuthentication> myUserNameAuthentications;
    private List<SVNAuthentication> mySSLAuthentications;
    
    private int mySSHIndex;
    private int myPasswordIndex;
    private int myUserNameIndex;
    private int mySSLIndex;

    private String myProxyHost;
    private int myProxyPort;
    private String myProxyUserName;
    private char[] myProxyPassword;
    private boolean myIsAuthenticationForced;
    
    /**
     * Creates an auth manager given a user credential - a username 
     * and password. 
     * 
     * @deprecated Use {@link #newInstance(String, char[])} method
     * 
     * @param userName  a username
     * @param password  a password
     */
    public BasicAuthenticationManager(String userName, String password) {
        this(new SVNAuthentication[] {
                SVNPasswordAuthentication.newInstance(userName, password != null ? password.toCharArray() : null, false, null, false),
                SVNSSHAuthentication.newInstance(userName, password != null ? password.toCharArray() : null, -1, false, null, false),
                SVNUserNameAuthentication.newInstance(userName, false, null, false),
        });        
    }
    
    /**
     * Creates an auth manager given a user credential - a username and 
     * an ssh private key.
     * 
     * @deprecated Use {@link #newInstance(String, File, char[], int)} method
     * 
     * @param userName    a username
     * @param keyFile     a private key file
     * @param passphrase  a password to the private key
     * @param portNumber  a port number over which an ssh tunnel is established
     */
    public BasicAuthenticationManager(String userName, File keyFile, String passphrase, int portNumber) {
        this(new SVNAuthentication[] {
                SVNSSHAuthentication.newInstance(userName, keyFile, passphrase != null ? passphrase.toCharArray() : null, portNumber, false, null, false),
                SVNUserNameAuthentication.newInstance(userName, false, null, false),
        });        
    }
    
    /**
     * Creates an auth manager given user credentials to use.
     * 
     * @param authentications user credentials
     */
    public BasicAuthenticationManager(SVNAuthentication[] authentications) {
        setAuthentications(authentications);
    }
    
    /**
     * Sets the given user credentials to this manager.
     * 
     * @param authentications user credentials
     */
    public void setAuthentications(SVNAuthentication[] authentications) {
        dismissSensitiveData();

        myPasswordAuthentications = new ArrayList<SVNAuthentication>();
        mySSHAuthentications = new ArrayList<SVNAuthentication>();
        myUserNameAuthentications = new ArrayList<SVNAuthentication>();
        mySSLAuthentications = new ArrayList<SVNAuthentication>();
        
        myPasswordIndex = 0;
        mySSHIndex = 0;
        mySSLIndex = 0;
        myUserNameIndex = 0;
        for (int i = 0; authentications != null && i < authentications.length; i++) {
            SVNAuthentication auth = authentications[i];
            if (auth instanceof SVNPasswordAuthentication) {
                myPasswordAuthentications.add(auth);                
            } else if (auth instanceof SVNSSHAuthentication) {
                mySSHAuthentications.add(auth);                
            } else if (auth instanceof SVNUserNameAuthentication) {
                myUserNameAuthentications.add(auth);                
            } else if (auth instanceof SVNSSLAuthentication) {
                mySSLAuthentications.add(auth);                
            }
        }
    }
    
    /**
     * Sets a proxy server context to this manager.
     * 
     * @deprecated
     * 
     * @param proxyHost        a proxy server hostname
     * @param proxyPort        a proxy server port
     * @param proxyUserName    a username to supply to a proxy machine
     * @param proxyPassword    a password to supply to a proxy machine
     */
    public void setProxy(String proxyHost, int proxyPort, String proxyUserName, String proxyPassword) {
        setProxy(proxyHost, proxyPort, proxyUserName, proxyPassword != null ? proxyPassword.toCharArray() : null);
    }

    /**
     * Sets a proxy server context to this manager.
     * 
     * @param proxyHost        a proxy server hostname
     * @param proxyPort        a proxy server port
     * @param proxyUserName    a username to supply to a proxy machine
     * @param proxyPassword    a password to supply to a proxy machine
     */
    public void setProxy(String proxyHost, int proxyPort, String proxyUserName, char[] proxyPassword) {
        myProxyHost = proxyHost;
        myProxyPort = proxyPort >= 0 ? proxyPort : 3128;
        myProxyUserName = proxyUserName;
        myProxyPassword = proxyPassword;
    }

    /**
     * Returns the first user's authentication credentials.
     * 
     * @param  kind          credentials kind; valid kinds are {@link ISVNAuthenticationManager#SSH}, 
     *                       {@link ISVNAuthenticationManager#PASSWORD}, {@link ISVNAuthenticationManager#USERNAME}, 
     *                       {@link ISVNAuthenticationManager#SSL}, {@link ISVNAuthenticationManager#USERNAME}  
     * @param  realm         authentication realm
     * @param  url           repository url
     * @return               first user's credentials
     * @throws SVNException  exception with {@link org.tmatesoft.svn.core.SVNErrorCode#RA_NOT_AUTHORIZED} 
     *                       error code - in case of invalid <code>kind</code>
     */
    public SVNAuthentication getFirstAuthentication(String kind, String realm, SVNURL url) throws SVNException {
        if (ISVNAuthenticationManager.SSH.equals(kind) && mySSHAuthentications.size() > 0) {
            mySSHIndex = 0; 
            return (SVNAuthentication) mySSHAuthentications.get(0);
        } else if (ISVNAuthenticationManager.PASSWORD.equals(kind) && myPasswordAuthentications.size() > 0) {
            myPasswordIndex = 0; 
            return (SVNAuthentication) myPasswordAuthentications.get(0);
        } else if (ISVNAuthenticationManager.USERNAME.equals(kind) && myUserNameAuthentications.size() > 0) {
            myUserNameIndex = 0; 
            return (SVNAuthentication) myUserNameAuthentications.get(0);
        } else if (ISVNAuthenticationManager.SSL.equals(kind) && mySSLAuthentications.size() > 0) {
            mySSLIndex = 0; 
            return (SVNAuthentication) mySSLAuthentications.get(0);
        }
        if (ISVNAuthenticationManager.USERNAME.equals(kind)) {
            if (url.getUserInfo() != null && !"".equals(url.getUserInfo())) {
                return SVNUserNameAuthentication.newInstance(url.getUserInfo(), false, url, false);
            }
            // client will use default.
            return SVNUserNameAuthentication.newInstance(null, false, url, false);
        }
        SVNErrorManager.authenticationFailed("Authentication required for ''{0}''", realm);
        return null;
    } 

    /**
     * Returns next user authentication credentials. This method is called whenever the first credentials 
     * returned by {@link #getFirstAuthentication(String, String, SVNURL)} failed to authenticate the user. 
     * 
     * @param  kind          credentials kind; valid kinds are {@link ISVNAuthenticationManager#SSH}, 
     *                       {@link ISVNAuthenticationManager#PASSWORD}, {@link ISVNAuthenticationManager#USERNAME}, 
     *                       {@link ISVNAuthenticationManager#SSL}, {@link ISVNAuthenticationManager#USERNAME}  
     * @param  realm         authentication realm
     * @param  url           repository url
     * @return               next user's authentication credentials                
     * @throws SVNException  exception with {@link org.tmatesoft.svn.core.SVNErrorCode#RA_NOT_AUTHORIZED} 
     *                       error code - in case of invalid <code>kind</code>
     */
    public SVNAuthentication getNextAuthentication(String kind, String realm, SVNURL url) throws SVNException {
        if (ISVNAuthenticationManager.SSH.equals(kind) && mySSHIndex + 1 < mySSHAuthentications.size()) {
            mySSHIndex++; 
            return (SVNAuthentication) mySSHAuthentications.get(mySSHIndex);
        } else if (ISVNAuthenticationManager.PASSWORD.equals(kind) && myPasswordIndex + 1 < myPasswordAuthentications.size()) {
            myPasswordIndex++; 
            return (SVNAuthentication) myPasswordAuthentications.get(myPasswordIndex);
        } else if (ISVNAuthenticationManager.USERNAME.equals(kind) && myUserNameIndex + 1 < myUserNameAuthentications.size()) {
            myUserNameIndex++; 
            return (SVNAuthentication) myUserNameAuthentications.get(myUserNameIndex);
        } else if (ISVNAuthenticationManager.SSL.equals(kind) && mySSLIndex + 1 < mySSLAuthentications.size()) {
            mySSLIndex++; 
            return (SVNAuthentication) mySSLAuthentications.get(mySSLIndex);
        } 
        SVNErrorManager.authenticationFailed("Authentication required for ''{0}''", realm);
        return null;
    }
    
    /**
     * Does nothing.
     *  
     * @param provider
     */
    public void setAuthenticationProvider(ISVNAuthenticationProvider provider) {        
    }
    
    /**
     * Returns itself as a proxy manager.
     * 
     * @param  url            a repository location that will be accessed 
     *                        over the proxy server for which a manager is needed
     * @return                a proxy manager
     * @throws SVNException   
     */
    public ISVNProxyManager getProxyManager(SVNURL url) throws SVNException {
        return this;
    }

    /**
     * Returns <span class="javakeyword">null</span>. 
     * 
     * @param  url             repository url
     * @return                 <span class="javakeyword">null</span>
     * @throws SVNException
     * @since                  1.2.0       
     */
	public TrustManager getTrustManager(SVNURL url) throws SVNException {
		return null;
	}

	/**
     * Does nothing.
     * 
     * @param accepted
     * @param kind
     * @param realm
     * @param errorMessage
     * @param authentication
     */
    public void acknowledgeAuthentication(boolean accepted, String kind, String realm, SVNErrorMessage errorMessage, SVNAuthentication authentication) {
    }

    /**
     * Does nothing.
     * 
     * @param manager
     * @since          1.2.0 
     */
	public void acknowledgeTrustManager(TrustManager manager) {
	}

	/**
	 * Tells whether authentication should be tried despite not being challenged from the server yet.
	 * 
	 * <p/>
	 * By default the return value is always <span class="javakeyword">false</span> until this behavior is 
	 * changed via a call to {@link #setAuthenticationForced(boolean)}. 
	 * 
	 * @return  authentication force flag 
	 */
	public boolean isAuthenticationForced() {
        return myIsAuthenticationForced;
    }
    
	/**
	 * Sets whether authentication should be forced or not.
	 * 
	 * @param forced  authentication force flag
	 * @see           #isAuthenticationForced() 
	 */
    public void setAuthenticationForced(boolean forced) {
        myIsAuthenticationForced = forced;
    }

    /**
     * Returns the proxy host name.
     * 
     * @return the proxy host argument value specified via the {@link #setProxy(String, int, String, String)} 
     *         method 
     */
    public String getProxyHost() {
        return myProxyHost;
    }

    /**
     * Returns the proxy port number.
     * @return the proxy port argument value specified via the {@link #setProxy(String, int, String, String)} 
     *         method 
     */
    public int getProxyPort() {
        return myProxyPort;
    }

    /**
     * Returns the proxy user name.
     * @return the proxy user name argument value specified via the {@link #setProxy(String, int, String, String)} 
     *         method 
     */
    public String getProxyUserName() {
        return myProxyUserName;
    }

    /**
     * Returns the password to authenticate against the proxy server.
     * 
     * @deprecated Use {@link #getProxyPasswordValue()}
     * 
     * @return the proxy password argument value specified via the {@link #setProxy(String, int, String, String)} 
     *         method 
     */
    public String getProxyPassword() {
        return myProxyPassword != null ? new String(myProxyPassword) : null;
    }
    
    /**
     * Returns the password to authenticate against the proxy server.
     *
     * @since 1.8.9
     * 
     * @return the proxy password argument value specified via the {@link #setProxy(String, int, String, String)} 
     *         method 
     */
    public char[] getProxyPasswordValue() {
        return myProxyPassword;
    }
    
    /**
     * Does nothing.
     * 
     * @param accepted
     * @param errorMessage
     */
    public void acknowledgeProxyContext(boolean accepted, SVNErrorMessage errorMessage) {
    }

    /**
     * Returns connection timeout value.
     * 
     * <p/>
     * This implementation returns a read timeout value equal to 3600 seconds for <code>http</code> 
     * or <code>https</code> access operations. If <code>repository</code> uses a different access protocol,
     * the return value will be 0.
     * 
     * @param  repository  repository access object 
     * @return             read timeout value in milliseconds
     * @since              1.2.0
     */
    public int getReadTimeout(SVNRepository repository) {
        String protocol = repository.getLocation().getProtocol();
        if ("http".equals(protocol) || "https".equals(protocol)) {
            return 3600*1000;
        }
        return 0;
    }

    /**
     * Returns connection timeout value.
     * 
     * <p/>
     * This implementation returns a connection timeout value equal to 60 seconds for <code>http</code> 
     * or <code>https</code> access operations. If <code>repository</code> uses a different access protocol,
     * the return value will be 0.
     * 
     * @param  repository  repository access object 
     * @return             connection timeout value in milliseconds
     * @since              1.2.0
     */
    public int getConnectTimeout(SVNRepository repository) {
        String protocol = repository.getLocation().getProtocol();
        if ("http".equals(protocol) || "https".equals(protocol)) {
            return 60*1000;
        }
        return 0; 
    }

    public void verifyHostKey(String hostName, int port, String keyAlgorithm, byte[] hostKey) throws SVNException {
    }

    /**
     * Dismiss cached sensitive data (e.g. password)
     * 
     * Calling this method clears and removes all credentials stored in this authentication manager.
     * 
     * @since 1.8.9
     */
    public void dismissSensitiveData() {
        dismissSensitiveData(myPasswordAuthentications);
        dismissSensitiveData(mySSHAuthentications);
        dismissSensitiveData(myUserNameAuthentications);
        dismissSensitiveData(mySSLAuthentications);

        myPasswordIndex = 0;
        mySSHIndex = 0;
        mySSLIndex = 0;
        myUserNameIndex = 0;
    }
    
    private void dismissSensitiveData(List<SVNAuthentication> auths) {
        if (auths == null) {
            return;
        }
        for (SVNAuthentication auth : auths) {
            auth.dismissSensitiveData();
        }
        auths.clear();
    }

}
