/*
 * ====================================================================
 * 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.wc;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNChecksumInputStream;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.internal.wc.admin.SVNVersionedProperties;
import org.tmatesoft.svn.core.internal.wc.admin.SVNWCAccess;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNCommitItem;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNCommitter implements ISVNCommitPathHandler {

    private Map myCommitItems;
    private Map myModifiedFiles;
    private Collection myTmpFiles;
    private String myRepositoryRoot;
    private SVNDeltaGenerator myDeltaGenerator;

    public SVNCommitter(Map commitItems, String reposRoot, Collection tmpFiles) {
        myCommitItems = commitItems;
        myModifiedFiles = new TreeMap();
        myTmpFiles = tmpFiles;
        myRepositoryRoot = reposRoot;
    }

    public boolean handleCommitPath(String commitPath, ISVNEditor commitEditor) throws SVNException {
        SVNCommitItem item = (SVNCommitItem) myCommitItems.get(commitPath);
        SVNWCAccess wcAccess = item.getWCAccess();
        wcAccess.checkCancelled();
        if (item.isCopied()) {
            if (item.getCopyFromURL() == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Commit item ''{0}'' has copy flag but no copyfrom URL", item.getFile());                    
                SVNErrorManager.error(err, SVNLogType.WC);
            } else if (item.getRevision().getNumber() < 0) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Commit item ''{0}'' has copy flag but an invalid revision", item.getFile());                    
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        SVNEvent event = null;
        boolean closeDir = false;

        File file = null;
        if (item.getFile() != null) {
            file = item.getFile();
        } else if (item.getPath() != null) {
            file = new File(wcAccess.getAnchor(), item.getPath());
        }

        long rev = item.getRevision().getNumber();
        if (item.isAdded() && item.isDeleted()) {
            event = SVNEventFactory.createSVNEvent(file, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_REPLACED, null, null, null);
	        event.setPreviousRevision(rev);
        } else if (item.isAdded()) {
            String mimeType = null;
            if (item.getKind() == SVNNodeKind.FILE && file != null) {
                SVNAdminArea dir = item.getWCAccess().retrieve(file.getParentFile());
                mimeType = dir.getProperties(file.getName()).getStringPropertyValue(SVNProperty.MIME_TYPE);
            }
            event = SVNEventFactory.createSVNEvent(file, item.getKind(), mimeType, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_ADDED, null, null, null);
	        event.setPreviousRevision(item.getCopyFromRevision() != null ? item.getCopyFromRevision().getNumber() : -1);
	        event.setPreviousURL(item.getCopyFromURL());
        } else if (item.isDeleted()) {
            event = SVNEventFactory.createSVNEvent(file, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_DELETED, null, null, null);
	        event.setPreviousRevision(rev);
        } else if (item.isContentsModified() || item.isPropertiesModified()) {
            event = SVNEventFactory.createSVNEvent(file, item.getKind(), null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_MODIFIED, null, null, null);
	        event.setPreviousRevision(rev);
        }
        if (event != null) {
	        event.setURL(item.getURL());
            wcAccess.handleEvent(event, ISVNEventHandler.UNKNOWN);
        }
        long cfRev = item.getCopyFromRevision().getNumber();//item.getCopyFromURL() != null ? rev : -1;
        if (item.isDeleted()) {
            try {
                commitEditor.deleteEntry(commitPath, rev);
            } catch (SVNException e) {
                fixError(commitPath, e, SVNNodeKind.FILE);
            }
        }
        
        boolean fileOpen = false;
        Map outgoingProperties = item.getOutgoingProperties();
        if (item.isAdded()) {
            String copyFromPath = getCopyFromPath(item.getCopyFromURL());
            if (item.getKind() == SVNNodeKind.FILE) {
                commitEditor.addFile(commitPath, copyFromPath, cfRev);
                fileOpen = true;
            } else {
                commitEditor.addDir(commitPath, copyFromPath, cfRev);
                closeDir = true;
            }
            
            if (outgoingProperties != null) {
                for (Iterator propsIter = outgoingProperties.keySet().iterator(); propsIter.hasNext();) {
                    String propName = (String) propsIter.next();
                    SVNPropertyValue propValue = (SVNPropertyValue) outgoingProperties.get(propName);
                    if (item.getKind() == SVNNodeKind.FILE) {
                        commitEditor.changeFileProperty(commitPath, propName, propValue);
                    } else {
                        commitEditor.changeDirProperty(propName, propValue);
                    }
                }
                outgoingProperties = null;
            }
        }
        
        if (item.isPropertiesModified() || (outgoingProperties != null && !outgoingProperties.isEmpty())) {
            if (item.getKind() == SVNNodeKind.FILE) {
                if (!fileOpen) {
                    try {
                        commitEditor.openFile(commitPath, rev);
                    } catch (SVNException e) {
                        fixError(commitPath, e, SVNNodeKind.FILE);
                    }
                }
                fileOpen = true;
            } else if (!item.isAdded()) {
                // do not open dir twice.
                try {
                    if ("".equals(commitPath)) {
                        commitEditor.openRoot(rev);
                    } else {
                        commitEditor.openDir(commitPath, rev);
                    }
                } catch (SVNException svne) {
                    fixError(commitPath, svne, SVNNodeKind.DIR);
                }
                closeDir = true;
            }

            if (item.isPropertiesModified()) {
                try {
                    sendPropertiesDelta(commitPath, item, commitEditor);
                } catch (SVNException e) {
                    fixError(commitPath, e, item.getKind());
                }
            }
            
            if (outgoingProperties != null) {
                for (Iterator propsIter = outgoingProperties.keySet().iterator(); propsIter.hasNext();) {
                    String propName = (String) propsIter.next();
                    SVNPropertyValue propValue = (SVNPropertyValue) outgoingProperties.get(propName);
                    if (item.getKind() == SVNNodeKind.FILE) {
                        commitEditor.changeFileProperty(commitPath, propName, propValue);
                    } else {
                        commitEditor.changeDirProperty(propName, propValue);
                    }
                }
            }
        }
        
        if (item.isContentsModified() && item.getKind() == SVNNodeKind.FILE) {
            if (!fileOpen) {
                try {
                    commitEditor.openFile(commitPath, rev);
                } catch (SVNException e) {
                    fixError(commitPath, e, SVNNodeKind.FILE);
                }
            }
            myModifiedFiles.put(commitPath, item);
        } else if (fileOpen) {
            commitEditor.closeFile(commitPath, null);
        }
        return closeDir;
    }

    public void sendTextDeltas(ISVNEditor editor) throws SVNException {
        for (Iterator paths = myModifiedFiles.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            SVNCommitItem item = (SVNCommitItem) myModifiedFiles.get(path);
            SVNWCAccess wcAccess = item.getWCAccess();
            wcAccess.checkCancelled();

            SVNEvent event = SVNEventFactory.createSVNEvent(new File(wcAccess.getAnchor(), item.getPath()),SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_DELTA_SENT, null, null, null);
            wcAccess.handleEvent(event, ISVNEventHandler.UNKNOWN);

            SVNAdminArea dir = wcAccess.retrieve(item.getFile().getParentFile());
            String name = SVNPathUtil.tail(item.getPath());
            SVNEntry entry = dir.getEntry(name, false);

            File tmpFile = dir.getBaseFile(name, true);
            myTmpFiles.add(tmpFile);

            String expectedChecksum = null;
            String checksum = null;
            String newChecksum = null;

            SVNChecksumInputStream baseChecksummedIS = null;
            InputStream sourceIS = null;
            InputStream targetIS = null;
            OutputStream tmpBaseStream = null;
            File baseFile = dir.getBaseFile(name, false);
            SVNErrorMessage error = null;
            boolean useChecksummedStream = false;
            boolean openSrcStream = false;
            if (!item.isAdded()) {
                openSrcStream = true;
                expectedChecksum = entry.getChecksum();
                if (expectedChecksum != null) {
                    useChecksummedStream = true;
                } else {
                    expectedChecksum = SVNFileUtil.computeChecksum(baseFile);
                }
            } else {
                sourceIS = SVNFileUtil.DUMMY_IN;
            }
  
            editor.applyTextDelta(path, expectedChecksum);
            if (myDeltaGenerator == null) {
                myDeltaGenerator = new SVNDeltaGenerator();
            }

            try {
                sourceIS = openSrcStream ? SVNFileUtil.openFileForReading(baseFile, SVNLogType.WC) : sourceIS;
                if (useChecksummedStream) {
                    baseChecksummedIS = new SVNChecksumInputStream(sourceIS, SVNChecksumInputStream.MD5_ALGORITHM);
                    sourceIS = baseChecksummedIS;
                }
                    
                targetIS = SVNTranslator.getTranslatedStream(dir, name, true, false);
                tmpBaseStream = SVNFileUtil.openFileForWriting(tmpFile);
                CopyingStream localStream = new CopyingStream(tmpBaseStream, targetIS);
                newChecksum = myDeltaGenerator.sendDelta(path, sourceIS, 0, localStream, editor, true);
            } catch (SVNException svne) {
                error = svne.getErrorMessage().wrap("While preparing ''{0}'' for commit", dir.getFile(name));
            } finally {
                SVNFileUtil.closeFile(sourceIS);
                SVNFileUtil.closeFile(targetIS);
                SVNFileUtil.closeFile(tmpBaseStream);
            }

            if (baseChecksummedIS != null) {
                checksum = baseChecksummedIS.getDigest();
            }
            
            if (expectedChecksum != null && checksum != null && !expectedChecksum.equals(checksum)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT_TEXT_BASE, 
                        "Checksum mismatch for ''{0}''; expected: ''{1}'', actual: ''{2}''",
                        new Object[] { dir.getFile(name), expectedChecksum, checksum }); 
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            if (error != null) {
                SVNErrorManager.error(error, SVNLogType.WC);
            }
            
            editor.closeFile(path, newChecksum);
        }
    }

    private void sendPropertiesDelta(String commitPath, SVNCommitItem item, ISVNEditor editor) throws SVNException {
        SVNAdminArea dir;
        String name;
        SVNWCAccess wcAccess = item.getWCAccess();
        if (item.getKind() == SVNNodeKind.DIR) {
            dir = wcAccess.retrieve(item.getFile());
            name = "";
        } else {
            dir = wcAccess.retrieve(item.getFile().getParentFile());
            name = SVNPathUtil.tail(item.getPath());
        }
        
        if (!dir.hasPropModifications(name)) {
            return;
        }
        SVNEntry entry = dir.getEntry(name, false);
        boolean replaced = false;
        if (entry != null) {
            replaced = entry.isScheduledForReplacement();
        }
        SVNVersionedProperties props = dir.getProperties(name);
        SVNVersionedProperties baseProps = replaced ? null : dir.getBaseProperties(name);
        SVNProperties diff = replaced ? props.asMap() : baseProps.compareTo(props).asMap();
        if (diff != null && !diff.isEmpty()) {
            File tmpPropsFile = dir.getPropertiesFile(name, true);
            SVNWCProperties tmpProps = new SVNWCProperties(tmpPropsFile, null);
            for(Iterator propNames = props.getPropertyNames(null).iterator(); propNames.hasNext();) {
                String propName = (String) propNames.next();
                SVNPropertyValue propValue = props.getPropertyValue(propName);
                tmpProps.setPropertyValue(propName, propValue);
            }
            if (!tmpPropsFile.exists()) {
                // create empty tmp (!) file just to make sure it will be used on post-commit.
                SVNFileUtil.createEmptyFile(tmpPropsFile);
            }
            myTmpFiles.add(tmpPropsFile);

            for (Iterator names = diff.nameSet().iterator(); names.hasNext();) {
                String propName = (String) names.next();
                SVNPropertyValue value = diff.getSVNPropertyValue(propName);
                if (item.getKind() == SVNNodeKind.FILE) {
                    editor.changeFileProperty(commitPath, propName, value);
                } else {
                    editor.changeDirProperty(propName, value);
                }
            }
        }
    }

    private String getCopyFromPath(SVNURL url) {
        if (url == null) {
            return null;
        }
        String path = url.getPath();
        if (myRepositoryRoot.equals(path)) {
            return "/";
        }
        return path.substring(myRepositoryRoot.length());
    }
    
    private void fixError(String path, SVNException e, SVNNodeKind kind) throws SVNException {
        SVNErrorMessage err = e.getErrorMessage();
        if (err.getErrorCode() == SVNErrorCode.FS_NOT_FOUND || err.getErrorCode() == SVNErrorCode.RA_DAV_PATH_NOT_FOUND) {
            err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_UP_TO_DATE, kind == SVNNodeKind.DIR ? 
                    "Directory ''{0}'' is out of date" : "File ''{0}'' is out of date", path);
            throw new SVNException(err);
        }
        throw e;
    }

    public static SVNCommitInfo commit(Collection tmpFiles, Map commitItems, SVNURL url, ISVNEditor commitEditor, ISVNEventHandler eventHandler) throws SVNException {
        String repositoryRoot = url.getPath();
        SVNCommitter committer = new SVNCommitter(commitItems, repositoryRoot, tmpFiles);
        SVNCommitUtil.driveCommitEditor(committer, commitItems.keySet(), commitEditor, -1);
        committer.sendTextDeltas(commitEditor);
        SVNEvent event = SVNEventFactory.createSVNEvent(null, SVNNodeKind.UNKNOWN, null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_FINALIZING, SVNEventAction.COMMIT_FINALIZING, null, null);
        event.setURL(url);
        if (eventHandler != null) {
            eventHandler.handleEvent(event, -1);
        }
        return commitEditor.closeEdit();
    }

    private class CopyingStream extends InputStream {
        private InputStream myInput;
        private OutputStream myOutput;
        
        public CopyingStream(OutputStream os, InputStream is) {
            myInput = is;
            myOutput = os;
        }
        
        public int read() throws IOException {
            int r = myInput.read();
            if (r != -1) {
                myOutput.write(r);
            }
            return r;
        }
        
        public int read(byte[] b) throws IOException {
            int r = myInput.read(b);
            if (r != -1) {
                myOutput.write(b, 0, r);
            }
            return r;
        }
        
        public int read(byte[] b, int off, int len) throws IOException {
            int r = myInput.read(b, off, len);
            if (r != -1) {
                myOutput.write(b, off, r);
            }
            return r;
        }
    }
}
