/*
 * ====================================================================
 * 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.internal.io.svn;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManagerExt;
import org.tmatesoft.svn.core.auth.SVNAuthentication;
import org.tmatesoft.svn.core.internal.util.SVNHashSet;
import org.tmatesoft.svn.core.internal.wc.SVNClassLoader;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNConnection {

    private final ISVNConnector myConnector;
    private String myRealm;
    private String myRoot;
    private OutputStream myOutputStream;
    private InputStream myInputStream;
    private SVNRepositoryImpl myRepository;
    private boolean myIsSVNDiff1;
    private boolean myIsCommitRevprops;
    private boolean myIsReopening = false;
    private boolean myIsCredentialsReceived = false;
    private InputStream myLoggingInputStream;
    private Set myCapabilities;
    private byte[] myHandshakeBuffer = new byte[8192];
    private SVNAuthenticator myEncryptor;
    private SVNAuthentication myAuthentication;
    
    private static final String EDIT_PIPELINE = "edit-pipeline";
    private static final String SVNDIFF1 = "svndiff1";
    private static final String ABSENT_ENTRIES = "absent-entries";
    private static final String COMMIT_REVPROPS = "commit-revprops";
    private static final String MERGE_INFO = "mergeinfo";
    private static final String DEPTH = "depth";
    private static final String LOG_REVPROPS = "log-revprops";
//    private static final String PARTIAL_REPLAY = "partial-replay";

    public SVNConnection(ISVNConnector connector, SVNRepositoryImpl repository) {
        myConnector = connector;
        myRepository = repository;
    }

    public void open(SVNRepositoryImpl repository) throws SVNException {
        myIsReopening = true;
        try {
            myIsCredentialsReceived = false;
            myConnector.open(repository);
            myRepository = repository;
            try {
                handshake(repository);
            } catch (SVNException th) {
                myConnector.handleExceptionOnOpen(repository, th);
            }
        } finally {
            myIsReopening = false;
        }
    }

    public String getRealm() {
        return myRealm;
    }
    
    public boolean isSVNDiff1() {
        return myIsSVNDiff1;
    }

    public boolean isCommitRevprops() {
        return myIsCommitRevprops;
    }
    
    private InputStream skipLeadingGrabage(int attempt) throws SVNException {
        byte[] bytes = myHandshakeBuffer;
        int r = 0;
        try {
            r = getInputStream().read(bytes);
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_MALFORMED_DATA, "Handshake failed: ''{0}''", e.getMessage());
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        if (r >= 0) {
            for (int i = 0; i < r; i++) {
                if (bytes[i] == '(' && bytes[i + 1] == ' ') {
                    return new SequenceInputStream(new ByteArrayInputStream(bytes, i, r - i), getInputStream());
                }
            }
        }
        if (r >= 0 && attempt == 0) {
            return skipLeadingGrabage(attempt + 1);
        }
        if (r <= 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_MALFORMED_DATA, "Handshake failed, data stream ended unexpectedly");
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_MALFORMED_DATA, "Handshake failed, received: ''{0}''", new String(bytes, 0, r));
        SVNErrorManager.error(err, SVNLogType.NETWORK);
        return null;
    }

    protected void handshake(SVNRepositoryImpl repository) throws SVNException {
        checkConnection();
        InputStream is = skipLeadingGrabage(0);
        List items = null;
        try {
            items = SVNReader.parse(is, "nnll", null);
        } finally {
            myRepository.getDebugLog().flushStream(myLoggingInputStream);
        }
        Long minVer = (Long) items.get(0);
        Long maxVer = (Long) items.get(1);
        if (minVer.longValue() > 2) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_BAD_VERSION, 
            		"Server requires minimum version {0}", minVer), SVNLogType.NETWORK);
        } else if (maxVer.longValue() < 2) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_BAD_VERSION, 
            		"Server requires maximum version {0}", maxVer), SVNLogType.NETWORK);
        }

        List capabilities = (List) items.get(3);
        addCapabilities(capabilities);
        if (!hasCapability(EDIT_PIPELINE)) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_BAD_VERSION, 
            		"Server does not support edit pipelining"), SVNLogType.NETWORK);
        }
        
        
        myIsSVNDiff1 = SVNReader.hasValue(items, 3, SVNDIFF1);
        myIsCommitRevprops = SVNReader.hasValue(items, 3, COMMIT_REVPROPS);

        write("(n(wwwwww)s)", new Object[]{"2", EDIT_PIPELINE, SVNDIFF1, ABSENT_ENTRIES, DEPTH, MERGE_INFO, LOG_REVPROPS, 
                repository.getLocation().toString()});
    }

    protected boolean hasCapability(String capability) {
    	if (myCapabilities != null) {
    		return myCapabilities.contains(capability);
    	}
    	return false;
    }
    
    public void authenticate(SVNRepositoryImpl repository) throws SVNException {
        ISVNAuthenticationManager authManager = myRepository.getAuthenticationManager();
        List items;
        try {
            items = read("ls", null, true);
        }
        catch (SVNException ex) {
            final SVNErrorMessage errorMessage = ex.getErrorMessage();
            if (errorMessage != null && errorMessage.getErrorCode() == SVNErrorCode.RA_NOT_AUTHORIZED && authManager != null && myAuthentication != null) {
                BasicAuthenticationManager.acknowledgeAuthentication(false, ISVNAuthenticationManager.PASSWORD, myRealm, errorMessage, myAuthentication, repository.getLocation(), authManager);
            }
            throw ex;
        }

        List mechs = SVNReader.getList(items, 0);
        if (mechs == null || mechs.size() == 0) {
            if (authManager instanceof ISVNAuthenticationManagerExt) {
                ((ISVNAuthenticationManagerExt)authManager).acknowledgeConnectionSuccessful(myRepository.getLocation(), "");
            }
            return;
        }
        myRealm = SVNReader.getString(items, 1);

        if (authManager != null && authManager.isAuthenticationForced() && mechs.contains("ANONYMOUS") &&
                (mechs.contains("CRAM-MD5") || mechs.contains("DIGEST-MD5"))) {
            mechs.remove("ANONYMOUS");
        }
        SVNAuthenticator authenticator = createSASLAuthenticator();
        myAuthentication = authenticator.authenticate(mechs, myRealm, repository);
        receiveRepositoryCredentials(repository);
        if (authManager instanceof ISVNAuthenticationManagerExt) {
            ((ISVNAuthenticationManagerExt)authManager).acknowledgeConnectionSuccessful(myRepository.getLocation(), "");
        }
    }
    
    private SVNAuthenticator createSASLAuthenticator() throws SVNException {
        return SVNClassLoader.getSASLAuthenticator(this);
    }
    
    private void addCapabilities(List capabilities) throws SVNException {
        if (myCapabilities == null) {
            myCapabilities = new SVNHashSet();
        }
        if (capabilities == null || capabilities.isEmpty()) {
            return;
        }
        for (Iterator caps = capabilities.iterator(); caps.hasNext();) {
            SVNItem item = (SVNItem) caps.next();
            if (item.getKind() != SVNItem.WORD) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_MALFORMED_DATA, 
                        "Capability entry is not a word"); 
                SVNErrorManager.error(err, SVNLogType.NETWORK);
            }
            myCapabilities.add(item.getWord());
        }
    }
    
    private void receiveRepositoryCredentials(SVNRepositoryImpl repository) throws SVNException {
        if (myIsCredentialsReceived) {
            return;
        }
        List creds = read("s?s?l", null, true);
        myIsCredentialsReceived = true;
        if (creds != null && creds.size() >= 2 && creds.get(0) != null && creds.get(1) != null) {
            SVNURL rootURL = creds.get(1) != null ? SVNURL.parseURIEncoded(SVNReader.getString(creds, 1)) : null;
            if (rootURL != null && rootURL.toString().length() > repository.getLocation().toString().length()) {
                SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_MALFORMED_DATA, "Impossibly long repository root from server"), SVNLogType.NETWORK);
            }
            if (repository != null && repository.getRepositoryRoot(false) == null) {
                repository.updateCredentials(SVNReader.getString(creds, 0), rootURL);
            }
            if (myRealm == null) {
                myRealm = SVNReader.getString(creds, 0);
            }
            if (myRoot == null) {
                myRoot = SVNReader.getString(creds, 1);
            }
            if (creds.size() > 2 && creds.get(2) instanceof List) {
                List capabilities = (List) creds.get(2);
                addCapabilities(capabilities);
            }
        }
    }
    
    public void setEncrypted(SVNAuthenticator encryptor) {
        myEncryptor = encryptor;
    }
    
    public boolean isEncrypted() {
        return myEncryptor != null;
    }

    public void close() throws SVNException {
        if (myEncryptor != null) {
            myEncryptor.dispose();
            myEncryptor = null;
        }
        myInputStream = null;
        myLoggingInputStream = null;
        myOutputStream = null;
        myConnector.close(myRepository);
    }

    public List read(String template, List items, boolean readMalformedData) throws SVNException {
        try {
            checkConnection();
            return SVNReader.parse(getInputStream(), template, items);
        } catch (SVNException e) {
            handleIOError(e, readMalformedData);
            return null;
        } finally {
            myRepository.getDebugLog().flushStream(myLoggingInputStream);
        }
    }

    public List readTuple(String template, boolean readMalformedData) throws SVNException {
        try {
            checkConnection();
            return SVNReader.readTuple(getInputStream(), template);
        } catch (SVNException e) {
            handleIOError(e, readMalformedData);
            return null;
        } finally {
            myRepository.getDebugLog().flushStream(myLoggingInputStream);
        }        
    }

    public SVNItem readItem(boolean readMalformedData) throws SVNException {
        try {
            checkConnection();
            return SVNReader.readItem(getInputStream());
        } catch (SVNException e) {
            handleIOError(e, readMalformedData);
            return null;
        } finally {
            myRepository.getDebugLog().flushStream(myLoggingInputStream);
        }
    }

    private void handleIOError(SVNException e, boolean readMalformedData) throws SVNException {
        if (readMalformedData && e.getErrorMessage().getErrorCode() == SVNErrorCode.RA_SVN_MALFORMED_DATA) {
            byte[] malfored = new byte[1024];
            try {
                getInputStream().read(malfored);
            } catch (IOException e1) {
                //
            }
        }
        throw e;        
    }

    public void writeError(SVNErrorMessage error) throws SVNException {
        Object[] buffer = new Object[]{"failure"};
        write("(w(", buffer);
        for (; error != null; error = error.getChildErrorMessage()) {
            String message = error.getMessage() == null ? "" : error.getMessage();
            buffer = new Object[]{new Long(error.getErrorCode().getCode()), message, "", new Integer(0)};
            write("(nssn)", buffer);
        }
        write(")", null);
    }
    
    public void write(String template, Object[] items) throws SVNException {
        try {
            SVNWriter.write(getOutputStream(), template, items);
        } finally {
            try {
                getOutputStream().flush();
            } catch (IOException e) {
                //
            } catch (SVNException e) {
                //
            }
            myRepository.getDebugLog().flushStream(getOutputStream());
        }
    }
    
    public boolean isConnectionStale() {
        return myConnector.isStale();
    }

    private void checkConnection() throws SVNException {
        if (!myIsReopening && !myConnector.isConnected(myRepository)) {
            myIsReopening = true;
            try {
                close();
                open(myRepository);
            } finally {
                myIsReopening = false;
            }
        }
    }

    public OutputStream getDeltaStream(final String token) {
        return new OutputStream() {
            Object[] myPrefix = new Object[]{"textdelta-chunk", token};

            public void write(byte b[], int off, int len) throws IOException {
                try {
                    SVNConnection.this.write("(w(s", myPrefix);
                    getOutputStream().write((String.valueOf(len)).getBytes("UTF-8"));
                    getOutputStream().write(':');
                    getOutputStream().write(b, off, len);
                    getOutputStream().write(' ');
                    SVNConnection.this.write("))", null);
                } catch (SVNException e) {
                    throw new IOException(e.getMessage());
                }
            }

            public void write(byte[] b) throws IOException {
                write(b, 0, b.length);
            }

            public void write(int b) throws IOException {
                write(new byte[]{(byte) (b & 0xFF)});
            }
        };
    }

    OutputStream getOutputStream() throws SVNException {
        if (myOutputStream == null) {
            try {
                myOutputStream = myRepository.getDebugLog().createLogStream(SVNLogType.NETWORK, myConnector.getOutputStream());
            } catch (IOException e) {
                SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_IO_ERROR, e.getMessage()), e, SVNLogType.NETWORK);
            }
        }
        return myOutputStream;
    }

    InputStream getInputStream() throws SVNException {
        if (myInputStream == null) {
            try {
                InputStream is = myConnector.getInputStream();
                myInputStream = myRepository.getDebugLog().createLogStream(SVNLogType.NETWORK, is); 
                myLoggingInputStream = myInputStream;
            } catch (IOException e) {
                SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_IO_ERROR, e.getMessage()), e, SVNLogType.NETWORK);
            }
        }
        return myInputStream;
    }
    
    void setOutputStream(OutputStream os) {
        if (myOutputStream != null) {
            myRepository.getDebugLog().flushStream(myOutputStream);
        }
        myOutputStream = os;
    }

    void setInputStream(InputStream is) {
        if (myLoggingInputStream != null) {            
            myRepository.getDebugLog().flushStream(myLoggingInputStream);
        }
        myInputStream = is;
        myLoggingInputStream = is;
    }

    ISVNConnector getConnector() {
        return myConnector;
    }
}