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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNMergeInfo;
import org.tmatesoft.svn.core.SVNMergeInfoInheritance;
import org.tmatesoft.svn.core.SVNMergeRange;
import org.tmatesoft.svn.core.SVNMergeRangeList;
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.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNHashSet;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNMergeDriver;
import org.tmatesoft.svn.core.internal.wc.SVNMergeInfoManager;
import org.tmatesoft.svn.core.io.ISVNLocationSegmentHandler;
import org.tmatesoft.svn.core.io.SVNLocationEntry;
import org.tmatesoft.svn.core.io.SVNLocationSegment;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNRevision;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class FSLog {
    private static final int MAX_OPEN_HISTORIES = 128;  
    
    private FSFS myFSFS;
    private String[] myPaths;
    private boolean myIsDescending;
    private boolean myIsDiscoverChangedPaths;
    private boolean myIsStrictNode;
    private boolean myIsIncludeMergedRevisions;
    private long myStartRevision;
    private long myEndRevision;
    private long myLimit;
    private ISVNLogEntryHandler myHandler;
    private SVNMergeInfoManager myMergeInfoManager;
    private String[] myRevPropNames;
    
    private static final Comparator RLP_COMPARATOR = new Comparator() {
        public int compare(Object arg1, Object arg2) {
            RangeListPath rangeListPath1 = (RangeListPath) arg1;
            RangeListPath rangeListPath2 = (RangeListPath) arg2;
            SVNMergeRange[] ranges1 = rangeListPath1.myRangeList.getRanges();
            SVNMergeRange[] ranges2 = rangeListPath2.myRangeList.getRanges();
            SVNMergeRange range1 = ranges1[0];
            SVNMergeRange range2 = ranges2[0];
            if (range1.getStartRevision() < range2.getStartRevision()) {
                return -1;
            }
            if (range1.getStartRevision() > range2.getStartRevision()) {
                return 1;
            }
            if (range1.getEndRevision() < range2.getEndRevision()) {
                return -1;
            }
            if (range1.getEndRevision() > range2.getEndRevision()) {
                return 1;
            }
            return 0;
        }
    };
    
    private static final Comparator PLR_COMPARATOR = new Comparator() {
        public int compare(Object o1, Object o2) {
            PathListRange plr1 = (PathListRange) o1;
            PathListRange plr2 = (PathListRange) o2;
            
            if (plr1.myRange.getStartRevision() < plr2.myRange.getStartRevision()) {
                return -1;
            }
            if (plr1.myRange.getStartRevision() > plr2.myRange.getStartRevision()) {
                return 1;
            }
            if (plr1.myRange.getEndRevision() < plr2.myRange.getEndRevision()) {
                return -1;
            }
            if (plr1.myRange.getEndRevision() < plr2.myRange.getEndRevision()) {
                return 1;
            }
            return 0;
        }
    };
    
    public FSLog(FSFS owner, String[] paths, long limit, long start, long end, boolean descending, 
            boolean discoverChangedPaths, boolean strictNode, boolean includeMergedRevisions, 
            String[] revPropNames, ISVNLogEntryHandler handler) {
        myFSFS = owner;
        myPaths = paths;
        myStartRevision = start;
        myEndRevision = end;
        myIsDescending = descending;
        myIsDiscoverChangedPaths = discoverChangedPaths;
        myIsStrictNode = strictNode;
        myIsIncludeMergedRevisions = includeMergedRevisions;
        myRevPropNames = revPropNames;
        myLimit = limit;
        myHandler = handler;
    }
    
    public void reset(FSFS owner, String[] paths, long limit, long start, long end, boolean descending, 
            boolean discoverChangedPaths, boolean strictNode, boolean includeMergedRevisions, 
            String[] revPropNames, ISVNLogEntryHandler handler) {
        myFSFS = owner;
        myPaths = paths;
        myStartRevision = start;
        myEndRevision = end;
        myIsDescending = descending;
        myIsDiscoverChangedPaths = discoverChangedPaths;
        myIsStrictNode = strictNode;
        myIsIncludeMergedRevisions = includeMergedRevisions;
        myRevPropNames = revPropNames;
        myLimit = limit;
        myHandler = handler;
    }
    
    public long runLog() throws SVNException {
        long count = 0;
        if (!myIsIncludeMergedRevisions && myPaths.length == 1 && "/".equals(myPaths[0])) {
            count = myEndRevision - myStartRevision + 1;
            if (myLimit > 0 && count > myLimit) {
                count = myLimit;
            }
        
            for (int i = 0; i < count; i++) {
                long rev = myStartRevision + i;
                if (myIsDescending) {
                    rev = myEndRevision - i;
                }
                sendLog(rev, null, null, false, false, false);
            }
            
            return count;
        }

        Map logTargetHistoryAsMergeInfo = null;
        if (myIsIncludeMergedRevisions) {
            logTargetHistoryAsMergeInfo = getPathsHistoryAsMergeInfo(myPaths, myStartRevision, myEndRevision);
        }
        return doLogs(myPaths, logTargetHistoryAsMergeInfo, null, myStartRevision, myEndRevision, myIsIncludeMergedRevisions, false, false, myIsDescending, myLimit);
    }
    
    private long doLogs(String[] paths, Map logTargetHistoryAsMergeinfo, Set nestedMerges, long startRevision, long endRevision, boolean includeMergedRevisions, 
            boolean subtractiveMerge, boolean handlingMergedRevisions, boolean isDescendingOrder, long limit) throws SVNException {
        long sendCount = 0;
        PathInfo[] histories = getPathHistories(paths, startRevision, endRevision, myIsStrictNode);
        
        LinkedList revisions = null;
        Map revMergeInfo = null;
        boolean anyHistoriesLeft = true;
        for (long currentRev = endRevision; anyHistoriesLeft; currentRev = getNextHistoryRevision(histories)) {
            
            boolean changed = false;
            anyHistoriesLeft = false;
            
            for (int i = 0; i < histories.length; i++) {
                PathInfo info = histories[i];
                changed = info.checkHistory(currentRev, myIsStrictNode, startRevision, changed);
                
                if (!info.myIsDone) {
                    anyHistoriesLeft = true;
                }
            }

            if (changed) {
                boolean hasChildren = false;
                Map addedMergeInfo = null;
                Map deletedMergeInfo = null;
                Map[] mergeInfo = null;
                if (includeMergedRevisions) {
                    LinkedList currentPaths = new LinkedList();
                    for (int i = 0; i < histories.length; i++) {
                        PathInfo info = histories[i];
                        currentPaths.add(info.myPath);
                    }
                    mergeInfo = getCombinedMergeInfoChanges((String[]) currentPaths.toArray(new String[currentPaths.size()]), currentRev);
                    addedMergeInfo = mergeInfo[0];
                    deletedMergeInfo = mergeInfo[1];
                    hasChildren = (addedMergeInfo.size() > 0 || deletedMergeInfo.size() > 0);
                }
                
                if (isDescendingOrder) {
                    sendLog(currentRev, logTargetHistoryAsMergeinfo, nestedMerges, subtractiveMerge, handlingMergedRevisions, hasChildren);
                    sendCount++;
                    if (hasChildren) {
                        if (nestedMerges == null) {
                            nestedMerges = new SVNHashSet();
                        }
                        handleMergedRevisions(addedMergeInfo, deletedMergeInfo, logTargetHistoryAsMergeinfo, nestedMerges);
                    }
                    if (limit > 0 && sendCount >= limit) {
                        break;
                    }
                } else {
                    if (revisions == null) {
                        revisions = new LinkedList();
                    }
                    revisions.addLast(currentRev);
                    
                    if (mergeInfo != null) {
                        if (revMergeInfo == null) {
                            revMergeInfo = new TreeMap();
                        }
                        revMergeInfo.put(currentRev, mergeInfo);
                    }
                }
            }
        }
        
        nestedMerges = null;

        if (revisions != null) {
            for (int i = 0; i < revisions.size(); i++) {
                boolean hasChildren = false;
                Map[] mergeInfo = null;
                long rev = ((Long) revisions.get(revisions.size() - i - 1)).longValue();
                
                if (revMergeInfo != null) {
                    mergeInfo = (Map[]) revMergeInfo.get(rev);
                    if (mergeInfo != null && mergeInfo.length == 2) {
                        hasChildren = !mergeInfo[0].isEmpty() || !mergeInfo[1].isEmpty();
                    }
                }
                sendLog(rev, logTargetHistoryAsMergeinfo, nestedMerges, subtractiveMerge, handlingMergedRevisions, hasChildren);
                if (hasChildren) {
                    if (nestedMerges == null) {
                        nestedMerges = new SVNHashSet();
                    }
                    handleMergedRevisions(mergeInfo[0], mergeInfo[1], logTargetHistoryAsMergeinfo, nestedMerges);
                }
                sendCount++;
                if (limit > 0 && sendCount >= limit) {
                    break;
                }
            }
        }
        return sendCount;
    }
    
    private long getNextHistoryRevision(PathInfo[] histories) {
        long nextRevision = SVNRepository.INVALID_REVISION;
        for (int i = 0; i < histories.length; i++) {
            PathInfo info = histories[i];
            if (info.myIsDone) {
                continue;
            }
            if (info.myHistoryRevision > nextRevision) {
                nextRevision = info.myHistoryRevision;
            }
        }
        return nextRevision;
    }

    private void sendLog(long revision, Map logTargetHistoryAsMergeInfo, Set nestedMerges, boolean subtractiveMerge, boolean handlingMergedRevision, boolean hasChildren) throws SVNException {
        if (myHandler == null) {
            return;
        }
        SVNLogEntry logEntry = fillLogEntry(revision, myIsDiscoverChangedPaths || handlingMergedRevision);
        logEntry.setHasChildren(hasChildren);
        logEntry.setSubtractiveMerge(subtractiveMerge);
        boolean revisionIsInteresting = true;
        if (handlingMergedRevision && !logEntry.getChangedPaths().isEmpty() && logTargetHistoryAsMergeInfo != null && !logTargetHistoryAsMergeInfo.isEmpty()) {
            boolean pathIsInHistory = false;
            revisionIsInteresting = false;
            for (Iterator changedPaths = logEntry.getChangedPaths().keySet().iterator(); changedPaths.hasNext();) {
                String changedPath = (String) changedPaths.next();
                for(Iterator mergedPaths = logTargetHistoryAsMergeInfo.keySet().iterator(); mergedPaths.hasNext();) {
                    String mergedPath = (String) mergedPaths.next();
                    if (SVNPathUtil.isAncestor(mergedPath, changedPath)) {
                        SVNMergeRangeList rangeList = (SVNMergeRangeList) logTargetHistoryAsMergeInfo.get(mergedPath);
                        SVNMergeRange[] ranges = rangeList.getRanges();
                        for (int i = 0; i < ranges.length; i++) {
                            if (revision > ranges[i].getStartRevision() && revision <= ranges[i].getEndRevision()) {
                                pathIsInHistory = true;
                                break;
                            }
                        }
                        if (pathIsInHistory) {
                            break;
                        }
                    }
                }
                if (!pathIsInHistory) {
                    revisionIsInteresting = true;
                    break;
                }
                
            }
        }
        if (!myIsDiscoverChangedPaths) {
            logEntry.getChangedPaths().clear();
        }
        if (revisionIsInteresting) {
            if (handlingMergedRevision && nestedMerges != null) {
                if (nestedMerges.contains(revision)) {
                    return;
                }
                nestedMerges.add(revision);
            }
            myHandler.handleLogEntry(logEntry);
        }
    }

    private SVNLogEntry fillLogEntry(long revision, boolean discoverChangedPaths) throws SVNException {
        Map changedPaths = null;
        SVNProperties entryRevProps = null;
        boolean getRevProps = true;
        boolean censorRevProps = false;
        if (revision > 0 && discoverChangedPaths) {
            FSRevisionRoot root = myFSFS.createRevisionRoot(revision);
            changedPaths = root.detectChanged();
        }

        if (getRevProps) {
            SVNProperties revisionProps = myFSFS.getRevisionProperties(revision);

            if (revisionProps != null) {
                String author = revisionProps.getStringValue(SVNRevisionProperty.AUTHOR);
                String datestamp = revisionProps.getStringValue(SVNRevisionProperty.DATE);
                Date date = datestamp != null ? SVNDate.parseDateString(datestamp) : null;

                if (myRevPropNames == null || myRevPropNames.length == 0) {
                    if (censorRevProps) {
                        entryRevProps = new SVNProperties();
                        if (author != null) {
                            entryRevProps.put(SVNRevisionProperty.AUTHOR, author);
                        }
                        if (date != null) {
                            entryRevProps.put(SVNRevisionProperty.DATE, SVNDate.formatDate(date));
                        }
                    } else {
                        entryRevProps = revisionProps;
                        if (date != null) {
                            entryRevProps.put(SVNRevisionProperty.DATE, SVNDate.formatDate(date));
                        }
                    }
                } else {
                    for (int i = 0; i < myRevPropNames.length; i++) {
                        String propName = myRevPropNames[i];
                        SVNPropertyValue propVal = revisionProps.getSVNPropertyValue(propName);
                        if (censorRevProps && !SVNRevisionProperty.AUTHOR.equals(propName) &&
                                !SVNRevisionProperty.DATE.equals(propName)) {
                            continue;
                        }
                        if (entryRevProps == null) {
                            entryRevProps = new SVNProperties();
                        }
                        if (SVNRevisionProperty.DATE.equals(propName) && date != null) {
                            entryRevProps.put(propName, SVNDate.formatDate(date));
                        } else if (propVal != null) {
                            entryRevProps.put(propName, propVal);
                        }
                    }
                }
            }
        }
        
        if (changedPaths == null) {
            changedPaths = new SVNHashMap();
        }
        if (entryRevProps == null) {
            entryRevProps = new SVNProperties();
        }
        SVNLogEntry entry = new SVNLogEntry(changedPaths, revision, entryRevProps, false);
        return entry;
    }
    
    private void handleMergedRevisions(Map addedMergeInfo, Map deletedMergeInfo, Map logTargetHistoryAsMergeInfo, Set nestedMerges) throws SVNException {
        if ((addedMergeInfo == null || addedMergeInfo.isEmpty()) &&
                (deletedMergeInfo == null || deletedMergeInfo.isEmpty())) {           
            return;
        }
        
        LinkedList combinedList = new LinkedList();
        if (addedMergeInfo != null && addedMergeInfo.size() > 0) {
            combinedList = combineMergeInfoPathLists(addedMergeInfo, false);
        }
        if (deletedMergeInfo != null && deletedMergeInfo.size() > 0) {
            combinedList.addAll(combineMergeInfoPathLists(deletedMergeInfo, true));
        }
        
        Collections.sort(combinedList, PLR_COMPARATOR);

        for (int i = combinedList.size() - 1; i >= 0; i--) {
            PathListRange pathListRange = (PathListRange) combinedList.get(i);
            try {
                doLogs(pathListRange.myPaths, logTargetHistoryAsMergeInfo, nestedMerges, pathListRange.myRange.getStartRevision(), 
                        pathListRange.myRange.getEndRevision(), true, pathListRange.reverseMerge, true, true, 0);
            } catch (SVNException svne) {
                SVNErrorCode errCode = svne.getErrorMessage().getErrorCode();
                if (errCode == SVNErrorCode.FS_NOT_FOUND || errCode == SVNErrorCode.FS_NO_SUCH_REVISION) {
                    continue;
                }
                throw svne;
            }
        }
        if (myHandler != null) {
            myHandler.handleLogEntry(SVNLogEntry.EMPTY_ENTRY);
        }
    }
    
    private Map getPathsHistoryAsMergeInfo(String[] paths, long startRevision, long endRevision) throws SVNException {
        if (startRevision < endRevision) {
            long temp = startRevision;
            startRevision = endRevision;
            endRevision = temp;
        }
        Map target = new SVNHashMap();
        FSLocationsFinder locationsFinder = new FSLocationsFinder(myFSFS);
        final Collection locationSegments = new ArrayList();        
        ISVNLocationSegmentHandler locationsReceiver = new ISVNLocationSegmentHandler() {
            public void handleLocationSegment(SVNLocationSegment locationSegment) throws SVNException {
                locationSegments.add(locationSegment);                    
            }
        };
        
        for (int i = 0; i < paths.length; i++) {
            locationsFinder.getNodeLocationSegments(paths[i], startRevision, startRevision, endRevision, locationsReceiver);
            Map mergeInfo = SVNMergeDriver.getMergeInfoFromSegments(locationSegments);
            target = SVNMergeInfoUtil.mergeMergeInfos(target, mergeInfo);
        }
        return target;
    }

    private PathInfo[] getPathHistories(String[] paths, long start, long end, boolean strictNodeHistory) throws SVNException {
        PathInfo[] histories = new PathInfo[paths.length];
        FSRevisionRoot root = myFSFS.createRevisionRoot(end);
        for (int i = 0; i < paths.length; i++) {
            String path = paths[i];
            
            PathInfo pathHistory = new PathInfo();
            pathHistory.myPath = path;
            pathHistory.myHistoryRevision = end;
            pathHistory.myIsDone = false;
            pathHistory.myIsFirstTime = true;
            
            if (i < MAX_OPEN_HISTORIES) {
                pathHistory.myHistory = root.getNodeHistory(path);
            }
            
            histories[i] = pathHistory.getHistory(strictNodeHistory, start);
        }
        return histories;
    }
    
    private Map[] getCombinedMergeInfoChanges(String[] paths, long revision) throws SVNException {
        if (revision == 0) {
            return new Map[] {new TreeMap(), new TreeMap()};
        }
        if (paths == null || paths.length == 0) {
            return new Map[] {new TreeMap(), new TreeMap()};
        }
        Map resultAdded = new SVNHashMap();
        Map resultDeleted = new SVNHashMap();
        Map addedMergeInfoCatalog = new SVNHashMap();
        Map deletedMergeInfoCatalog = new SVNHashMap();
        FSRevisionRoot root = myFSFS.createRevisionRoot(revision);
        
        collectChangedMergeInfo(addedMergeInfoCatalog, deletedMergeInfoCatalog, revision);
        
        for (int i = 0; i < paths.length; i++) {
            String path = paths[i];
            if (deletedMergeInfoCatalog.containsKey(path)) {
                continue;
            }
            long[] appearedRevision = new long[] {-1};
            SVNLocationEntry prevLocation = null;
            try {
                prevLocation = myFSFS.getPreviousLocation(path, revision, appearedRevision);                
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() == SVNErrorCode.FS_NOT_FOUND) {
                    continue;
                }
                throw e;
            }
            String prevPath = null;
            long prevRevision = -1;
            if (!(prevLocation != null && prevLocation.getPath() != null && prevLocation.getRevision() >=0 && appearedRevision[0] == revision)) {
                prevPath = path;
                prevRevision = revision - 1;
            } else if (prevLocation != null) {
                prevPath = prevLocation.getPath();
                prevRevision = prevLocation.getRevision();
            }
            
            FSRevisionRoot prevRoot = myFSFS.createRevisionRoot(prevRevision);
            String[] queryPaths = new String[] {prevPath};
            Map catalog = null;
            try {
                catalog = getMergeInfoManager().getMergeInfo(queryPaths, prevRoot, SVNMergeInfoInheritance.INHERITED, false);
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() == SVNErrorCode.FS_NOT_FOUND) {
                    continue;
                }
                throw e;
            }
            SVNMergeInfo prevMergeinfo = (SVNMergeInfo) catalog.get(prevPath);
            queryPaths = new String[] {path};
            catalog = getMergeInfoManager().getMergeInfo(queryPaths, root, SVNMergeInfoInheritance.INHERITED, false);
            SVNMergeInfo mergeInfo = (SVNMergeInfo) catalog.get(path);
            
            Map deleted = new SVNHashMap();
            Map added = new SVNHashMap();
            
            SVNMergeInfoUtil.diffMergeInfo(deleted, added, 
                    prevMergeinfo != null ? prevMergeinfo.getMergeSourcesToMergeLists() : null, mergeInfo != null ? mergeInfo.getMergeSourcesToMergeLists() : null, false);
            
            resultAdded = SVNMergeInfoUtil.mergeMergeInfos(resultAdded, added);            
            resultDeleted = SVNMergeInfoUtil.mergeMergeInfos(resultDeleted, deleted);            
        }
        for(Iterator ps = addedMergeInfoCatalog.keySet().iterator(); ps.hasNext();) {
            String changedPath = (String) ps.next();
            Map addedMergeInfo = (Map) addedMergeInfoCatalog.get(changedPath);
            for (int i = 0; i < paths.length; i++) {
                String path = paths[i];
                if (!SVNPathUtil.isAncestor(path, changedPath)) {
                    continue;
                }
                Map deletedMergeInfo = (Map) deletedMergeInfoCatalog.get(changedPath);
                resultDeleted = SVNMergeInfoUtil.mergeMergeInfos(resultDeleted, deletedMergeInfo);
                resultAdded = SVNMergeInfoUtil.mergeMergeInfos(resultAdded, addedMergeInfo);
                break;
            }
        }
        return new Map[] {resultAdded, resultDeleted};
    }
    
    private void collectChangedMergeInfo(Map addedMergeInfo, Map deletedMergeInfo, long revision) throws SVNException {
        if (revision == 0) {
            return;
        }
        FSRevisionRoot root = myFSFS.createRevisionRoot(revision);
        Map changedPaths = root.getChangedPaths();
        if (changedPaths == null || changedPaths.isEmpty()) {
            return;
        }
        for (Iterator paths = changedPaths.keySet().iterator(); paths.hasNext();) {
            String changedPath = (String) paths.next();
            FSPathChange change = (FSPathChange) changedPaths.get(changedPath);
            if (!change.arePropertiesModified()) {
                continue;
            }
            FSPathChangeKind changeKind = change.getChangeKind();
            
            String basePath = null;
            long baseRevision = -1;
            String mergeInfoValue = null;
            String previousMergeInfoValue = null;
            
            if (changeKind == FSPathChangeKind.FS_PATH_CHANGE_ADD ||
                    changeKind == FSPathChangeKind.FS_PATH_CHANGE_REPLACE) {
                String copyFromPath = change.getCopyPath();
                long copyFromRev = change.getCopyRevision();
                if (copyFromPath != null && copyFromRev >= 0) {
                    basePath = copyFromPath;
                    baseRevision = copyFromRev;
                }
            } else if (changeKind == FSPathChangeKind.FS_PATH_CHANGE_MODIFY) {
                long[] appearedRevision = new long[] {-1};
                SVNLocationEntry prevLocation = myFSFS.getPreviousLocation(changedPath, revision, appearedRevision);
                if (!(prevLocation != null && 
                        prevLocation.getPath() != null && prevLocation.getRevision() >= 0 && appearedRevision[0] == prevLocation.getRevision())) {
                    basePath = changedPath;
                    baseRevision = revision - 1;
                } else {
                    basePath = prevLocation.getPath();
                    baseRevision = prevLocation.getRevision();
                }
            } else {
                continue;
            }
            
            FSRevisionRoot baseRoot = null;
            if (basePath != null && baseRevision >= 0) {
                baseRoot = myFSFS.createRevisionRoot(baseRevision);
                SVNProperties props = myFSFS.getProperties(baseRoot.getRevisionNode(basePath));
                previousMergeInfoValue = props.getStringValue(SVNProperty.MERGE_INFO);
            }

            SVNProperties props = myFSFS.getProperties(root.getRevisionNode(changedPath));
            if (props != null) {
                mergeInfoValue = props.getStringValue(SVNProperty.MERGE_INFO);
            }
  
            if (mergeInfoValue == null && previousMergeInfoValue == null) {
                continue;
            }
            
            if (previousMergeInfoValue != null && mergeInfoValue == null) {
              String[] queryPaths = new String[] { changedPath };
              Map tmpCatalog = getMergeInfoManager().getMergeInfo(queryPaths, root, SVNMergeInfoInheritance.INHERITED, false);
              SVNMergeInfo tmpMergeInfo = (SVNMergeInfo) tmpCatalog.get(changedPath);
              if (tmpMergeInfo != null) {
                  mergeInfoValue = SVNMergeInfoUtil.formatMergeInfoToString(tmpMergeInfo.getMergeSourcesToMergeLists(), null);
              }
            } else if (mergeInfoValue != null && previousMergeInfoValue == null && basePath != null && 
                    SVNRevision.isValidRevisionNumber(baseRevision)) {
                String[] queryPaths = new String[] { basePath };
                Map tmpCatalog = getMergeInfoManager().getMergeInfo(queryPaths, baseRoot, SVNMergeInfoInheritance.INHERITED, false);
                SVNMergeInfo tmpMergeInfo = (SVNMergeInfo) tmpCatalog.get(basePath);
                if (tmpMergeInfo != null) {
                    previousMergeInfoValue = SVNMergeInfoUtil.formatMergeInfoToString(tmpMergeInfo.getMergeSourcesToMergeLists(), null);
                }
            }
        
            if ((previousMergeInfoValue != null && mergeInfoValue == null) ||
                    (previousMergeInfoValue == null && mergeInfoValue != null) ||
                    (previousMergeInfoValue != null && mergeInfoValue != null &&
                            !previousMergeInfoValue.equals(mergeInfoValue))) {
                Map mergeInfo = null;
                Map previousMergeInfo = null;
                if (mergeInfoValue != null) {
                    mergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(mergeInfoValue), null);
                }
                if (previousMergeInfoValue != null) {
                    previousMergeInfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(previousMergeInfoValue), null);
                }
                Map added = new SVNHashMap();
                Map deleted = new SVNHashMap();
                SVNMergeInfoUtil.diffMergeInfo(deleted, added, previousMergeInfo, mergeInfo, false);
                
                addedMergeInfo.put(changedPath, added);
                deletedMergeInfo.put(changedPath, deleted);
            }
        }
        
    }

    private LinkedList combineMergeInfoPathLists(Map mergeInfo, boolean reverseMerge) {
        List rangeListPaths = new LinkedList();
        for (Iterator pathsIter = mergeInfo.keySet().iterator(); pathsIter.hasNext();) {
            String path = (String) pathsIter.next();
            SVNMergeRangeList changes = (SVNMergeRangeList) mergeInfo.get(path);
            RangeListPath rangeListPath = new RangeListPath();
            rangeListPath.myPath = path;
            rangeListPath.myRangeList = changes.dup();
            SVNMergeRange[] rangesArray = rangeListPath.myRangeList.getRanges(); 
            for (int i = 0; i < rangesArray.length; i++) {
                SVNMergeRange range = rangesArray[i];
                range.setStartRevision(range.getStartRevision() + 1);
            }
            rangeListPaths.add(rangeListPath);
        }
        
        LinkedList combinedList = new LinkedList();
        
        while (rangeListPaths.size() > 1) {
            Collections.sort(rangeListPaths, RLP_COMPARATOR);
            RangeListPath rangeListPath = (RangeListPath) rangeListPaths.get(0);
            RangeListPath firstRLP = rangeListPath;
            long youngest = rangeListPath.myRangeList.getRanges()[0].getStartRevision();
            long nextYoungest = youngest;
            int numRevs = 1;
            for (; nextYoungest == youngest; numRevs++) {
                if (numRevs == rangeListPaths.size()) {
                    numRevs++;
                    break;
                }
                rangeListPath = (RangeListPath) rangeListPaths.get(numRevs);
                nextYoungest = rangeListPath.myRangeList.getRanges()[0].getStartRevision();
            }
            numRevs--;
            long youngestEnd = firstRLP.myRangeList.getRanges()[0].getEndRevision();
            long tail = nextYoungest - 1;
            if (nextYoungest == youngest || youngestEnd < nextYoungest) {
                tail = youngestEnd;
            }

            PathListRange pathListRange = new PathListRange();
            pathListRange.reverseMerge = reverseMerge;
            pathListRange.myRange = new SVNMergeRange(youngest, tail, false);
            List paths = new LinkedList();
            for (int i = 0; i < numRevs; i++) {
                RangeListPath rp = (RangeListPath) rangeListPaths.get(i); 
                paths.add(rp.myPath);
            }
            pathListRange.myPaths = (String[]) paths.toArray(new String[paths.size()]);
        
            combinedList.add(pathListRange);
            
            for (int i = 0; i < numRevs; i++) {
                RangeListPath rp = (RangeListPath) rangeListPaths.get(i);
                SVNMergeRange range = rp.myRangeList.getRanges()[0];
                range.setStartRevision(tail + 1);
                if (range.getStartRevision() > range.getEndRevision()) {
                    if (rp.myRangeList.getSize() == 1) {
                        rangeListPaths.remove(0);
                        i--;
                        numRevs--;
                    } else {
                        SVNMergeRange[] ranges = new SVNMergeRange[rp.myRangeList.getSize() - 1];
                        System.arraycopy(rp.myRangeList.getRanges(), 1, ranges, 0, ranges.length);
                        rp.myRangeList = new SVNMergeRangeList(ranges);
                    }
                }
            }
        }
        
        if (!rangeListPaths.isEmpty()) {
            RangeListPath firstRangeListPath = (RangeListPath) rangeListPaths.get(0);
            while (!firstRangeListPath.myRangeList.isEmpty()) {
                PathListRange pathListRange = new PathListRange();
                pathListRange.reverseMerge = reverseMerge;
                pathListRange.myPaths = new String[] { firstRangeListPath.myPath };
                pathListRange.myRange = firstRangeListPath.myRangeList.getRanges()[0];
                SVNMergeRange[] ranges = new SVNMergeRange[firstRangeListPath.myRangeList.getSize() - 1];
                System.arraycopy(firstRangeListPath.myRangeList.getRanges(), 1, ranges, 0, ranges.length);
                firstRangeListPath.myRangeList = new SVNMergeRangeList(ranges);
                combinedList.add(pathListRange);
            }
        }
        
        return combinedList;
    }

    private SVNMergeInfoManager getMergeInfoManager() {
        if (myMergeInfoManager == null) {
            myMergeInfoManager = new SVNMergeInfoManager();
        }
        return myMergeInfoManager;
    }
    
    private class RangeListPath {
        String myPath;
        SVNMergeRangeList myRangeList;
    }
    
    private class PathListRange {
        public boolean reverseMerge;
        String myPaths[];
        SVNMergeRange myRange;
    }
    
    private class PathInfo {
        FSNodeHistory myHistory;
        boolean myIsDone;
        boolean myIsFirstTime;
        long myHistoryRevision;
        String myPath;
        
        public PathInfo getHistory(boolean strictNodeHistory, long start) throws SVNException {
            FSNodeHistory history = null;
            if (myHistory != null) {
                history = myHistory.getPreviousHistory(strictNodeHistory ? false : true);
                myHistory = history;
            } else {
                FSRevisionRoot historyRoot = myFSFS.createRevisionRoot(myHistoryRevision);
                history = historyRoot.getNodeHistory(myPath);
                history = history.getPreviousHistory(strictNodeHistory ? false : true);
                if (myIsFirstTime) {
                    myIsFirstTime = false;
                } else if (history != null) {
                    history = history.getPreviousHistory(strictNodeHistory ? false : true);
                }
            }

            if (history == null) {
                myIsDone = true;
                return this;
            }

            myPath = history.getHistoryEntry().getPath();
            myHistoryRevision = history.getHistoryEntry().getRevision();
            
            if (myHistoryRevision < start) {
                myIsDone = true;
            }
            return this;
        }

        public boolean checkHistory(long currentRevision, boolean strictNodeHistory, 
                                    long start, boolean changed) throws SVNException {
            if (myIsDone) {
                return changed;
            }
            
            if (myHistoryRevision < currentRevision) {
                return changed;
            }
            
            changed = true;
            getHistory(strictNodeHistory, start);
            return changed;
        }
    }
    
}
