/*
 * ====================================================================
 * 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.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNMergeRange;
import org.tmatesoft.svn.core.SVNMergeRangeList;
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.SVNRevisionProperty;
import org.tmatesoft.svn.core.internal.delta.SVNDeltaReader;
import org.tmatesoft.svn.core.internal.io.fs.FSCommitter;
import org.tmatesoft.svn.core.internal.io.fs.FSDeltaConsumer;
import org.tmatesoft.svn.core.internal.io.fs.FSFS;
import org.tmatesoft.svn.core.internal.io.fs.FSRevisionNode;
import org.tmatesoft.svn.core.internal.io.fs.FSRevisionRoot;
import org.tmatesoft.svn.core.internal.io.fs.FSTransactionInfo;
import org.tmatesoft.svn.core.internal.io.fs.FSTransactionRoot;
import org.tmatesoft.svn.core.internal.util.FixedSizeInputStream;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.admin.ISVNAdminEventHandler;
import org.tmatesoft.svn.core.wc.admin.SVNAdminEvent;
import org.tmatesoft.svn.core.wc.admin.SVNAdminEventAction;
import org.tmatesoft.svn.core.wc.admin.SVNUUIDAction;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class DefaultLoadHandler implements ISVNLoadHandler {
    
    private FSFS myFSFS;
    private RevisionBaton myCurrentRevisionBaton;
    private NodeBaton myCurrentNodeBaton;
    private boolean myIsUsePreCommitHook;
    private boolean myIsUsePostCommitHook;
    private Map myRevisionsMap;
    private String myParentDir;
    private SVNUUIDAction myUUIDAction;
    private SVNDeltaReader myDeltaReader;
    private SVNDeltaGenerator myDeltaGenerator;
    private ISVNAdminEventHandler myProgressHandler;
    
    public DefaultLoadHandler(boolean usePreCommitHook, boolean usePostCommitHook, SVNUUIDAction uuidAction, 
            String parentDir, ISVNAdminEventHandler progressHandler) {
        myProgressHandler = progressHandler;
        myIsUsePreCommitHook = usePreCommitHook;
        myIsUsePostCommitHook = usePostCommitHook;
        myUUIDAction = uuidAction;
        myParentDir = SVNPathUtil.canonicalizePath(parentDir);
        myRevisionsMap = new SVNHashMap();
    }
    
    public void setFSFS(FSFS fsfs) {
        myFSFS = fsfs;
    }
    
    public void closeRevision() throws SVNException {
        if (myCurrentRevisionBaton != null) {
            myCurrentRevisionBaton.getConsumer().close();
            
            RevisionBaton baton = myCurrentRevisionBaton;
            myCurrentRevisionBaton = null;
            
            if (baton.myRevision <= 0) {
                return;
            }
            
            long oldRevision = baton.myRevision;
            long newRevision = -1;
            try {
                newRevision = baton.getCommitter().commitTxn(myIsUsePreCommitHook, myIsUsePostCommitHook, null, null);
            } catch (SVNException svne) {
                try {
                    FSCommitter.abortTransaction(myFSFS, baton.myTxn.getTxnId());
                } catch (SVNException svne2) {
                    //
                }
                throw svne;
            }
            
            if (baton.myDatestamp == null) {
                myFSFS.setRevisionProperty(baton.myRevision, SVNRevisionProperty.DATE, null);
            }
            File revProps = myFSFS.getRevisionPropertiesFile(baton.myRevision, true);
            if (!revProps.exists()) {
                OutputStream os = SVNFileUtil.openFileForWriting(revProps);
                try {
                    SVNWCProperties.setProperties(new SVNProperties(), os, SVNWCProperties.SVN_HASH_TERMINATOR);
                } finally {
                    SVNFileUtil.closeFile(os);
                }
            }

            myRevisionsMap.put(new Long(oldRevision), new Long(newRevision));
            if (baton.myDatestamp != null) {
                myFSFS.setRevisionProperty(newRevision, SVNRevisionProperty.DATE, baton.myDatestamp);
            }
            
            String message;
            if (newRevision == baton.myRevision) {
                message = "\n------- Committed revision " + newRevision + " >>>";
            } else {
                message = "\n------- Committed new rev " + newRevision + " (loaded from original rev " + baton.myRevision + ") >>>";
            }
            if (myProgressHandler != null) {
                SVNAdminEvent event = new SVNAdminEvent(newRevision, baton.myRevision, SVNAdminEventAction.REVISION_LOADED, message); 
                myProgressHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
            }
        }
    }

    public void openRevision(Map headers) throws SVNException {
        myCurrentRevisionBaton = new RevisionBaton();
        long revision = -1;
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_REVISION_NUMBER)) {
            try {
                revision = Long.parseLong((String) headers.get(SVNAdminHelper.DUMPFILE_REVISION_NUMBER)); 
            } catch (NumberFormatException nfe) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Cannot parse revision ({0}) in dump file", headers.get(SVNAdminHelper.DUMPFILE_REVISION_NUMBER));
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }
        }
        
        myCurrentRevisionBaton.myRevision = revision;
        long headRevision = myFSFS.getYoungestRevision();
        myCurrentRevisionBaton.myRevisionOffset = revision - (headRevision + 1);
        
        if (revision > 0) {
            myCurrentRevisionBaton.myTxn = FSTransactionRoot.beginTransaction(headRevision, 0, myFSFS);
            myCurrentRevisionBaton.myTxnRoot = myFSFS.createTransactionRoot(myCurrentRevisionBaton.myTxn);
            String message = "<<< Started new transaction, based on original revision " + revision;
            if (myProgressHandler != null) {
                SVNAdminEvent event = new SVNAdminEvent(revision, SVNAdminEventAction.REVISION_LOAD, message); 
                myProgressHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
            }
        }
    }

    public void openNode(Map headers) throws SVNException {
        if (myCurrentRevisionBaton.myRevision == 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Malformed dumpstream: Revision 0 must not contain node records");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        
        myCurrentNodeBaton = createNodeBaton(headers);
        String message;
        switch (myCurrentNodeBaton.myAction) {
            case SVNAdminHelper.NODE_ACTION_CHANGE:
                message = "     * editing path : " + myCurrentNodeBaton.myPath + " ...";
                if (myProgressHandler != null) {
                    SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.REVISION_LOAD_EDIT_PATH, myCurrentNodeBaton.myPath, message); 
                    myProgressHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                }
                break;
            case SVNAdminHelper.NODE_ACTION_DELETE:
                message = "     * deleting path : " + myCurrentNodeBaton.myPath + " ...";
                if (myProgressHandler != null) {
                    SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.REVISION_LOAD_DELETE_PATH, myCurrentNodeBaton.myPath, message); 
                    myProgressHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                }
                myCurrentRevisionBaton.getCommitter().deleteNode(myCurrentNodeBaton.myPath);
                break;
            case SVNAdminHelper.NODE_ACTION_ADD:
                message = "     * adding path : " + myCurrentNodeBaton.myPath + " ...";
                if (maybeAddWithHistory(myCurrentNodeBaton)) {
                    message += "COPIED...";
                }
                if (myProgressHandler != null) {
                    SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.REVISION_LOAD_ADD_PATH, myCurrentNodeBaton.myPath, message); 
                    myProgressHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                }
                break;
            case SVNAdminHelper.NODE_ACTION_REPLACE:
                message = "     * replacing path : " + myCurrentNodeBaton.myPath + " ...";
                myCurrentRevisionBaton.getCommitter().deleteNode(myCurrentNodeBaton.myPath);
                if (maybeAddWithHistory(myCurrentNodeBaton)) {
                    message += "COPIED...";
                }
                if (myProgressHandler != null) {
                    SVNAdminEvent event = new SVNAdminEvent(SVNAdminEventAction.REVISION_LOAD_REPLACE_PATH, myCurrentNodeBaton.myPath, message); 
                    myProgressHandler.handleAdminEvent(event, ISVNEventHandler.UNKNOWN);
                }
                break;
            default:
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_UNRECOGNIZED_DATA, "Unrecognized node-action on node ''{0}''", myCurrentNodeBaton.myPath);
                SVNErrorManager.error(err, SVNLogType.FSFS);
        }
    }

    public void parseUUID(String uuid) throws SVNException {
        if (myUUIDAction == SVNUUIDAction.IGNORE_UUID) {
            return;
        }
        
        if (myUUIDAction != SVNUUIDAction.FORCE_UUID) {
            long latestRevision = myFSFS.getYoungestRevision();
            if (latestRevision != 0) {
                return;
            }
        }

        myFSFS.setUUID(uuid);
    }

    public void closeNode() throws SVNException {
        myCurrentNodeBaton = null;
    }

    public void applyTextDelta() throws SVNException {
        FSDeltaConsumer fsConsumer = myCurrentRevisionBaton.getConsumer();
        fsConsumer.applyTextDelta(myCurrentNodeBaton.myPath, myCurrentNodeBaton.myBaseChecksum);
    }

    public void setFullText() throws SVNException {
        FSDeltaConsumer fsConsumer = myCurrentRevisionBaton.getConsumer();
        fsConsumer.applyText(myCurrentNodeBaton.myPath);
    }

    public void parseTextBlock(InputStream dumpStream, long contentLength, boolean isDelta) throws SVNException {
        FSDeltaConsumer fsConsumer = myCurrentRevisionBaton.getConsumer();

        try {
            if (isDelta) {
                applyTextDelta();
            } else {
                setFullText();
            }
            
            String checksum = null;
            byte[] buffer = null;
            if (contentLength == 0) {
                getDeltaGenerator().sendDelta(myCurrentNodeBaton.myPath, SVNFileUtil.DUMMY_IN, fsConsumer, false);
            } else {
                if (!isDelta) {
                    // 
                    InputStream tgt = new FixedSizeInputStream(dumpStream, contentLength);
                    checksum = getDeltaGenerator().sendDelta(myCurrentNodeBaton.myPath, tgt, fsConsumer, true);
                } else {
                    buffer = new byte[SVNFileUtil.STREAM_CHUNK_SIZE];
                    SVNDeltaReader deltaReader = null;
                    try {
                        while (contentLength > 0) {
                            int numToRead = contentLength > SVNFileUtil.STREAM_CHUNK_SIZE ? SVNFileUtil.STREAM_CHUNK_SIZE : (int) contentLength;
                            int read = 0;
                            while(numToRead > 0) {
                                int numRead = dumpStream.read(buffer, read, numToRead);
                                if (numRead < 0) {
                                    SVNAdminHelper.generateIncompleteDataError();
                                }
                                read += numRead;
                                numToRead -= numRead;
                            }
                            deltaReader = getDeltaReader();
                            deltaReader.nextWindow(buffer, 0, read, myCurrentNodeBaton.myPath, fsConsumer);
                            contentLength -= read;
                        }
                    } catch (IOException ioe) {
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getMessage());
                        SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
                    }
                    if (deltaReader != null) {
                        deltaReader.reset(myCurrentNodeBaton.myPath, fsConsumer);
                    }
                    fsConsumer.textDeltaEnd(myCurrentNodeBaton.myPath);
                    checksum = fsConsumer.getChecksum();
                }
            }
            
            if (checksum != null && myCurrentNodeBaton.myResultChecksum != null) {
                if (!checksum.equals(myCurrentNodeBaton.myResultChecksum)) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH, "Checksum mismatch for ''{0}'':\n   expected:  {1}\n     actual:  {2}\n", 
                            new Object[] { myCurrentNodeBaton.myPath, myCurrentNodeBaton.myResultChecksum, checksum });
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                }
            }
        } catch (SVNException svne) {
            fsConsumer.abort();
            throw svne;
        }
    }

    public void removeNodeProperties() throws SVNException {
        FSTransactionRoot txnRoot = myCurrentRevisionBaton.myTxnRoot;
        FSRevisionNode node = txnRoot.getRevisionNode(myCurrentNodeBaton.myPath);
        SVNProperties props = node.getProperties(myFSFS);
        
        for (Iterator propNames = props.nameSet().iterator(); propNames.hasNext();) {
            String propName = (String) propNames.next();
            myCurrentRevisionBaton.getCommitter().changeNodeProperty(myCurrentNodeBaton.myPath, propName, null);
        }
    }

    public void setRevisionProperty(String propertyName, SVNPropertyValue propertyValue) throws SVNException {
        if (myCurrentRevisionBaton.myRevision > 0) {
            myFSFS.setTransactionProperty(myCurrentRevisionBaton.myTxn.getTxnId(), propertyName, propertyValue);
            if (SVNRevisionProperty.DATE.equals(propertyName)) {
                myCurrentRevisionBaton.myDatestamp = propertyValue;
            }
        } else if (myCurrentRevisionBaton.myRevision == 0) {
            long youngestRevision = myFSFS.getYoungestRevision();
            if (youngestRevision == 0) {
                myFSFS.setRevisionProperty(0, propertyName, propertyValue);
            }
        }
    }

    public void setUsePreCommitHook(boolean use) {
        myIsUsePreCommitHook = use;
    }
    
    public void setUsePostCommitHook(boolean use) {
        myIsUsePostCommitHook = use;
    }
    
    public void setParentDir(String parentDir) {
        myParentDir = parentDir;
    }

    public void setUUIDAction(SVNUUIDAction action) {
        myUUIDAction = action;
    }
    
    public void deleteNodeProperty(String propertyName) throws SVNException {
        myCurrentRevisionBaton.getCommitter().changeNodeProperty(myCurrentNodeBaton.myPath, propertyName, null);
    }
    
    public void setNodeProperty(String propertyName, SVNPropertyValue propertyValue) throws SVNException {
        if (SVNProperty.MERGE_INFO.equals(propertyName)) {
            Map mergeInfo = renumberMergeInfoRevisions(propertyValue);
            if (myParentDir != null) {
                mergeInfo = prefixMergeInfoPaths(mergeInfo);
            }
            String mergeInfoString = SVNMergeInfoUtil.formatMergeInfoToString(mergeInfo, null);
            propertyValue = SVNPropertyValue.create(mergeInfoString);
        }
        myCurrentRevisionBaton.getCommitter().changeNodeProperty(myCurrentNodeBaton.myPath, propertyName, 
                propertyValue);
    }

    private SVNDeltaReader getDeltaReader() {
        if (myDeltaReader == null) {
            myDeltaReader = new SVNDeltaReader();
        } 
        return myDeltaReader;
    }

    private SVNDeltaGenerator getDeltaGenerator() {
        if (myDeltaGenerator == null) {
            myDeltaGenerator = new SVNDeltaGenerator();
        }
        return myDeltaGenerator;
    }

    private boolean maybeAddWithHistory(NodeBaton nodeBaton) throws SVNException {
        if (nodeBaton.myCopyFromPath == null) {
            if (nodeBaton.myKind == SVNNodeKind.FILE) {
                myCurrentRevisionBaton.getCommitter().makeFile(nodeBaton.myPath);
            } else if (nodeBaton.myKind == SVNNodeKind.DIR) {
                myCurrentRevisionBaton.getCommitter().makeDir(nodeBaton.myPath);
            }
            return false;
        } 
        long srcRevision = nodeBaton.myCopyFromRevision - myCurrentRevisionBaton.myRevisionOffset;
        Long copyFromRevision = new Long(nodeBaton.myCopyFromRevision);
        
        if (myRevisionsMap.containsKey(copyFromRevision)) {
            Long revision = (Long) myRevisionsMap.get(copyFromRevision);
            srcRevision = revision.longValue();
        }
        
        if (!SVNRevision.isValidRevisionNumber(srcRevision)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NO_SUCH_REVISION, "Relative source revision {0} is not available in current repository", new Long(srcRevision));
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        
        FSRevisionRoot copyRoot = myFSFS.createRevisionRoot(srcRevision);
        if (nodeBaton.myCopySourceChecksum != null) {
            FSRevisionNode revNode = copyRoot.getRevisionNode(nodeBaton.myCopyFromPath);
            String hexDigest = revNode.getFileMD5Checksum();
            if (hexDigest != null && !hexDigest.equals(nodeBaton.myCopySourceChecksum)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH, 
                        "Copy source checksum mismatch on copy from ''{0}''@{1}\n" +
                        " to ''{2}'' in rev based on r{3}:\n" +
                        "   expected:  {4}\n" + 
                        "     actual:  {5}\n", new Object[] { nodeBaton.myCopyFromPath, 
                        String.valueOf(srcRevision), nodeBaton.myPath, 
                        String.valueOf(myCurrentRevisionBaton.myRevision), 
                        nodeBaton.myCopySourceChecksum, hexDigest });
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }
        }
        myCurrentRevisionBaton.getCommitter().makeCopy(copyRoot, nodeBaton.myCopyFromPath, nodeBaton.myPath, true);
        
        return true;
    }
    
    private NodeBaton createNodeBaton(Map headers) throws SVNException {
        NodeBaton baton = new NodeBaton();
        baton.myKind = SVNNodeKind.UNKNOWN;
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_NODE_PATH)) {
            String nodePath = (String) headers.get(SVNAdminHelper.DUMPFILE_NODE_PATH); 
            if (myParentDir != null) {
                baton.myPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myParentDir, nodePath));
            } else {
                baton.myPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.canonicalizePath(nodePath));
            }
        }
        
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_NODE_KIND)) {
            baton.myKind = SVNNodeKind.parseKind((String) headers.get(SVNAdminHelper.DUMPFILE_NODE_KIND));
        }
        
        baton.myAction = SVNAdminHelper.NODE_ACTION_UNKNOWN;
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_NODE_ACTION)) {
            String action = (String) headers.get(SVNAdminHelper.DUMPFILE_NODE_ACTION);
            if ("change".equals(action)) {
                baton.myAction = SVNAdminHelper.NODE_ACTION_CHANGE;
            } else if ("add".equals(action)) {
                baton.myAction = SVNAdminHelper.NODE_ACTION_ADD;
            } else if ("delete".equals(action)) {
                baton.myAction = SVNAdminHelper.NODE_ACTION_DELETE;
            } else if ("replace".equals(action)) {
                baton.myAction = SVNAdminHelper.NODE_ACTION_REPLACE;
            }
        }
        
        baton.myCopyFromRevision = -1;
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_NODE_COPYFROM_REVISION)) {
            try {
                baton.myCopyFromRevision = Long.parseLong((String) headers.get(SVNAdminHelper.DUMPFILE_NODE_COPYFROM_REVISION)); 
            } catch (NumberFormatException nfe) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Cannot parse revision ({0}) in dump file", headers.get(SVNAdminHelper.DUMPFILE_NODE_COPYFROM_REVISION));
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }
        }
        
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_NODE_COPYFROM_PATH)) {
            String copyFromPath = (String) headers.get(SVNAdminHelper.DUMPFILE_NODE_COPYFROM_PATH);
            if (myParentDir != null) {
                baton.myCopyFromPath = SVNPathUtil.append(myParentDir, copyFromPath);
            } else {
                baton.myCopyFromPath = SVNPathUtil.canonicalizePath(copyFromPath);
            }
            baton.myCopyFromPath = SVNPathUtil.getAbsolutePath(baton.myCopyFromPath);
        }
        
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_TEXT_CONTENT_MD5)) {
            baton.myResultChecksum = (String) headers.get(SVNAdminHelper.DUMPFILE_TEXT_CONTENT_MD5);
        }        
        
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_TEXT_DELTA_BASE_MD5)) {
            baton.myBaseChecksum = (String) headers.get(SVNAdminHelper.DUMPFILE_TEXT_DELTA_BASE_MD5);
        }
        
        if (headers.containsKey(SVNAdminHelper.DUMPFILE_TEXT_COPY_SOURCE_MD5)) {
            baton.myCopySourceChecksum = (String) headers.get(SVNAdminHelper.DUMPFILE_TEXT_COPY_SOURCE_MD5);
        }
        return baton;
    }
    
    private Map renumberMergeInfoRevisions(SVNPropertyValue mergeInfoProp) throws SVNException {
        String mergeInfoString = SVNPropertyValue.getPropertyAsString(mergeInfoProp);
        Map mergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(mergeInfoString), null);
        for (Iterator mergeInfoIter = mergeInfo.keySet().iterator(); mergeInfoIter.hasNext();) {
            String mergeSource = (String) mergeInfoIter.next();
            SVNMergeRangeList rangeList = (SVNMergeRangeList) mergeInfo.get(mergeSource);
            SVNMergeRange[] ranges = rangeList.getRanges();
            for (int i = 0; i < ranges.length; i++) {
                SVNMergeRange range = ranges[i];
                Long revFromMap = (Long) myRevisionsMap.get(new Long(range.getStartRevision()));
                if (revFromMap != null && SVNRevision.isValidRevisionNumber(revFromMap.longValue())) {
                    range.setStartRevision(revFromMap.longValue());
                }
                revFromMap = (Long) myRevisionsMap.get(new Long(range.getEndRevision()));
                if (revFromMap != null && SVNRevision.isValidRevisionNumber(revFromMap.longValue())) {
                    range.setEndRevision(revFromMap.longValue());
                }
            }
            Arrays.sort(ranges);
        }
        return mergeInfo;
    }
    
    private Map prefixMergeInfoPaths(Map mergeInfo) {
        Map prefixedMergeInfo = new TreeMap();
        for (Iterator mergeInfoIter = mergeInfo.keySet().iterator(); mergeInfoIter.hasNext();) {
            String mergeSource = (String) mergeInfoIter.next();
            SVNMergeRangeList rangeList = (SVNMergeRangeList) mergeInfo.get(mergeSource);
            mergeSource = mergeSource.startsWith("/") ? mergeSource.substring(1) : mergeSource;
            mergeSource = SVNPathUtil.append(myParentDir, mergeSource);
            if (mergeSource.charAt(0) != '/') {
                mergeSource = '/' + mergeSource;
            } 
            prefixedMergeInfo.put(mergeSource, rangeList);
        }
        return prefixedMergeInfo;
    }
    
    private class RevisionBaton {
        FSTransactionInfo myTxn;
        FSTransactionRoot myTxnRoot;
        long myRevision;
        long myRevisionOffset;
        SVNPropertyValue myDatestamp;
        
        private FSCommitter myCommitter;
        private FSDeltaConsumer myDeltaConsumer;
        
        public FSDeltaConsumer getConsumer() {
            if (myDeltaConsumer == null) {
                myDeltaConsumer = new FSDeltaConsumer("", myTxnRoot, myFSFS, getCommitter(), null, null);
            }
            return myDeltaConsumer;
        }
        
        public FSCommitter getCommitter() {
            if (myCommitter == null) {
                myCommitter = new FSCommitter(myFSFS, myTxnRoot, myTxn, null, null);
            }
            return myCommitter;
        }
    }
    
    private class NodeBaton {
        String myPath;
        SVNNodeKind myKind;
        int myAction;
        String myBaseChecksum;
        String myResultChecksum;
        String myCopySourceChecksum;
        long myCopyFromRevision;
        String myCopyFromPath;
    }
}
