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

import java.io.File;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
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.SVNMergeRange;
import org.tmatesoft.svn.core.SVNMergeRangeList;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.internal.wc.ISVNCommitPathHandler;
import org.tmatesoft.svn.core.internal.wc.SVNCommitUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNPropertiesManager;
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.SVNDiffWindow;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNMergeInfoUtil {
    
    public static void mergeCatalog(Map catalog, Map changes) throws SVNException {
        int i = 0;
        int j = 0;
        
        Object[] catalogKeys = catalog.keySet().toArray();
        Object[] changesKeys = changes.keySet().toArray();
        while(i < catalog.size() && j < changes.size()) {
            Comparable catalogKey = (Comparable) catalogKeys[i];
            Comparable changeKey = (Comparable) changesKeys[j];
            
            int compare = catalogKey.compareTo(changeKey);
            
            if (compare == 0) {
                Map catalogMergeinfo = (Map) catalog.get(catalogKey);
                Map changesMergeinfo= (Map) changes.get(changeKey);
                
                Map mergedMergeinfo = mergeMergeInfos(catalogMergeinfo, changesMergeinfo);
                catalog.put(catalogKey, mergedMergeinfo);
                i++;
                j++;
            } else if (compare < 0) {
                i++;
            } else {
                Map changesMergeinfo= (Map) changes.get(changeKey);
                changesMergeinfo = dupMergeInfo(changesMergeinfo, null);
                catalog.put(changeKey, changesMergeinfo);
                j++;
            }
        }
        for(; j < changes.size(); j++) {
            Object changeKey = changesKeys[j];
            Map changesMergeinfo= (Map) changes.get(changeKey);
            changesMergeinfo = dupMergeInfo(changesMergeinfo, null);
            catalog.put(changeKey, changesMergeinfo);
        }
    }

    public static Map<String, Map<String,SVNMergeRangeList>> filterCatalogByRanges(Map<String, Map<String,SVNMergeRangeList>> catalog, long youngestRev, long oldestRev) {
        Map<String, Map<String,SVNMergeRangeList>> filteredCatalog = new TreeMap<String, Map<String,SVNMergeRangeList>>();
        for (Iterator<String> catalogIter = catalog.keySet().iterator(); catalogIter.hasNext();) {
            String path = catalogIter.next();
            Map<String, SVNMergeRangeList> mergeInfo = catalog.get(path);
            Map<String, SVNMergeRangeList> filteredMergeInfo = filterMergeInfoByRanges(mergeInfo, youngestRev, oldestRev);
            if (!filteredMergeInfo.isEmpty()) {
                filteredCatalog.put(path, filteredMergeInfo);
            }
        }
        return filteredCatalog;
    }
    
    public static Map<String, SVNMergeRangeList> filterMergeInfoByRanges(Map<String, SVNMergeRangeList> mergeInfo, long youngestRev, long oldestRev) {
        Map<String, SVNMergeRangeList> filteredMergeInfo = new TreeMap<String, SVNMergeRangeList>();
        if (mergeInfo != null) {
            SVNMergeRange range = new SVNMergeRange(oldestRev, youngestRev, true);
            SVNMergeRangeList filterRangeList = new SVNMergeRangeList(range);
            for (Iterator<String> mergeInfoIter = mergeInfo.keySet().iterator(); mergeInfoIter.hasNext();) {
                String path = mergeInfoIter.next();
                SVNMergeRangeList rangeList = mergeInfo.get(path);
                if (!rangeList.isEmpty()) {
                    SVNMergeRangeList newRangeList = filterRangeList.intersect(rangeList, false);
                    if (!newRangeList.isEmpty()) {
                        filteredMergeInfo.put(path, newRangeList);
                    }
                }
            }
        }
        return filteredMergeInfo;
    }
    
    public static long[] getRangeEndPoints(Map<?, SVNMergeRangeList> mergeInfo) {
        //long[] { youngestRange, oldestRange }
        long[] rangePoints = { SVNRepository.INVALID_REVISION, SVNRepository.INVALID_REVISION };
        
        if (mergeInfo != null) {
            for (Iterator<?> mergeInfoIter = mergeInfo.keySet().iterator(); mergeInfoIter.hasNext();) {
                Object path = mergeInfoIter.next();
                SVNMergeRangeList rangeList = (SVNMergeRangeList) mergeInfo.get(path);
                if (!rangeList.isEmpty()) {
                    SVNMergeRange[] ranges = rangeList.getRanges();
                    SVNMergeRange range = ranges[ranges.length - 1];
                    if (!SVNRevision.isValidRevisionNumber(rangePoints[0]) || range.getEndRevision() > rangePoints[0]) {
                        rangePoints[0] = range.getEndRevision(); 
                    }
                    
                    range = ranges[0];
                    if (!SVNRevision.isValidRevisionNumber(rangePoints[1]) || rangePoints[1] > range.getStartRevision()) {
                        rangePoints[1] = range.getStartRevision();
                    }
                }
            }
        }
        
        return rangePoints;
    }
    
	public static Map<String, Map<String, SVNMergeRangeList>> elideMergeInfoCatalog(Map<String, Map<String, SVNMergeRangeList>> mergeInfoCatalog) throws SVNException {
	    Map<String, Map<String, SVNMergeRangeList>> adjustedMergeInfoCatalog = new TreeMap<String, Map<String, SVNMergeRangeList>>();
	    for (Iterator<String> pathsIter = mergeInfoCatalog.keySet().iterator(); pathsIter.hasNext();) {
	        String path = pathsIter.next();
	        String adjustedPath = path;
	        if (path.startsWith("/")) {
	            adjustedPath = path.substring(1);
	        }
	        adjustedMergeInfoCatalog.put(adjustedPath, mergeInfoCatalog.get(path));
	    }
	    mergeInfoCatalog = adjustedMergeInfoCatalog;
	    ElideMergeInfoCatalogHandler handler = new ElideMergeInfoCatalogHandler(mergeInfoCatalog);
	    ElideMergeInfoEditor editor = new ElideMergeInfoEditor(mergeInfoCatalog);
	    SVNCommitUtil.driveCommitEditor(handler, mergeInfoCatalog.keySet(), editor, -1);
	    List elidablePaths = handler.getElidablePaths();
	    for (Iterator elidablePathsIter = elidablePaths.iterator(); elidablePathsIter.hasNext();) {
            String elidablePath = (String) elidablePathsIter.next();
            mergeInfoCatalog.remove(elidablePath);
        }
        
	    adjustedMergeInfoCatalog = new TreeMap<String, Map<String, SVNMergeRangeList>>();
        for (Iterator<String> pathsIter = mergeInfoCatalog.keySet().iterator(); pathsIter.hasNext();) {
            String path = (String) pathsIter.next();
            String adjustedPath = path;
            if (!path.startsWith("/")) {
                adjustedPath = "/" + adjustedPath;
            }
            adjustedMergeInfoCatalog.put(adjustedPath, mergeInfoCatalog.get(path));
        }
	    return adjustedMergeInfoCatalog;
	}
	
    public static Map<String, SVNMergeRangeList> adjustMergeInfoSourcePaths(Map<String, SVNMergeRangeList> mergeInfo, String walkPath, Map<String, SVNMergeRangeList> wcMergeInfo) {
        mergeInfo = mergeInfo == null ? new TreeMap<String, SVNMergeRangeList>() : mergeInfo;
		for (Iterator<String> paths = wcMergeInfo.keySet().iterator(); paths.hasNext();) {
            String srcMergePath = paths.next();
            SVNMergeRangeList rangeList = (SVNMergeRangeList) wcMergeInfo.get(srcMergePath); 
            mergeInfo.put(SVNPathUtil.getAbsolutePath(SVNPathUtil.append(srcMergePath, walkPath)), rangeList);
        }
		return mergeInfo;
	}
	
	public static boolean removeEmptyRangeLists(Map mergeInfo) {
		boolean removedSomeRanges = false;
		if (mergeInfo != null) {
			for (Iterator mergeInfoIter = mergeInfo.entrySet().iterator(); mergeInfoIter.hasNext();) {
				Map.Entry mergeInfoEntry = (Map.Entry) mergeInfoIter.next();
				SVNMergeRangeList rangeList = (SVNMergeRangeList) mergeInfoEntry.getValue();
				if (rangeList.isEmpty()) {
					mergeInfoIter.remove();
					removedSomeRanges = true;
				}
			}
		}
		return removedSomeRanges;
	}
	
    public static Map<String, SVNMergeRangeList> mergeMergeInfos(Map<String, SVNMergeRangeList> originalSrcsToRangeLists, Map<String, SVNMergeRangeList> changedSrcsToRangeLists) throws SVNException {
        originalSrcsToRangeLists = originalSrcsToRangeLists == null ? new TreeMap() : originalSrcsToRangeLists;
        changedSrcsToRangeLists = changedSrcsToRangeLists == null ? Collections.EMPTY_MAP : changedSrcsToRangeLists;
        String[] paths1 = (String[]) originalSrcsToRangeLists.keySet().toArray(new String[originalSrcsToRangeLists.size()]);
        String[] paths2 = (String[]) changedSrcsToRangeLists.keySet().toArray(new String[changedSrcsToRangeLists.size()]);
        int i = 0;
        int j = 0;
        while (i < paths1.length && j < paths2.length) {
            String path1 = paths1[i];
            String path2 = paths2[j];
            int res = path1.compareTo(path2);
            if (res == 0) {
                SVNMergeRangeList rangeList1 = (SVNMergeRangeList) originalSrcsToRangeLists.get(path1);
                SVNMergeRangeList rangeList2 = (SVNMergeRangeList) changedSrcsToRangeLists.get(path2);
                rangeList1 = rangeList1.merge(rangeList2);
                originalSrcsToRangeLists.put(path1, rangeList1);
                i++;
                j++;
            } else if (res < 0) {
                i++;
            } else {
                originalSrcsToRangeLists.put(path2, changedSrcsToRangeLists.get(path2));
                j++;
            }
        }
        
        for (; j < paths2.length; j++) {
            String path = paths2[j];
            originalSrcsToRangeLists.put(path, changedSrcsToRangeLists.get(path));
        }
        return originalSrcsToRangeLists;
    }
    
    public static String combineMergeInfoProperties(String propValue1, String propValue2) throws SVNException {
        Map srcsToRanges1 = parseMergeInfo(new StringBuffer(propValue1), null);
        Map srcsToRanges2 = parseMergeInfo(new StringBuffer(propValue2), null);
        srcsToRanges1 = mergeMergeInfos(srcsToRanges1, srcsToRanges2);
        return formatMergeInfoToString(srcsToRanges1, null);
    }
    
    public static String combineForkedMergeInfoProperties(String fromPropValue, String workingPropValue, 
            String toPropValue) throws SVNException {
        Map leftDeleted = new TreeMap();
        Map leftAdded = new TreeMap();
        Map fromMergeInfo = parseMergeInfo(new StringBuffer(fromPropValue), null);
        diffMergeInfoProperties(leftDeleted, leftAdded, null, fromMergeInfo, workingPropValue, null);
        
        Map rightDeleted = new TreeMap();
        Map rightAdded = new TreeMap();
        diffMergeInfoProperties(rightDeleted, rightAdded, fromPropValue, null, toPropValue, null);
        leftDeleted = mergeMergeInfos(leftDeleted, rightDeleted);
        leftAdded = mergeMergeInfos(leftAdded, rightAdded);
        fromMergeInfo = mergeMergeInfos(fromMergeInfo, leftAdded);
        Map result = removeMergeInfo(leftDeleted, fromMergeInfo);
        return formatMergeInfoToString(result, null);
    }
    
    public static void diffMergeInfoProperties(Map deleted, Map added, String fromPropValue, Map fromMergeInfo, String toPropValue, Map toMergeInfo) throws SVNException {
        if (fromPropValue != null && fromPropValue.equals(toPropValue)) {
            return;
        } 
        fromMergeInfo = fromMergeInfo == null ? parseMergeInfo(new StringBuffer(fromPropValue), null) : fromMergeInfo;
        toMergeInfo = toMergeInfo == null ? parseMergeInfo(new StringBuffer(toPropValue), null) : toMergeInfo;
        diffMergeInfo(deleted, added, fromMergeInfo, toMergeInfo, true);
    }
    
    public static void diffMergeInfo(Map deleted, Map added, Map from, Map to, 
            boolean considerInheritance) {
        from = from == null ? Collections.EMPTY_MAP : from;
        to = to == null ? Collections.EMPTY_MAP : to;
        if (!from.isEmpty() && to.isEmpty()) {
            dupMergeInfo(from, deleted);
        } else if (from.isEmpty() && !to.isEmpty()) {
            dupMergeInfo(to, added);
        } else if (!from.isEmpty() && !to.isEmpty()) {
            walkMergeInfoHashForDiff(deleted, added, from, to, considerInheritance);
        }
    }
    
    public static Map dupCatalog(Map catalog) {
        Map newMergeInfoCatalog = new TreeMap();
        for (Iterator catalogIter = catalog.keySet().iterator(); catalogIter.hasNext();) {
            String path = (String) catalogIter.next();
            Map mergeInfo = (Map) catalog.get(path);
            Map mergeInfoCopy = dupMergeInfo(mergeInfo, null);
            newMergeInfoCatalog.put(path, mergeInfoCopy);
        }
        return newMergeInfoCatalog;
    }
    
    public static Map dupMergeInfo(Map srcsToRangeLists, Map target) {
        if (srcsToRangeLists == null) {
            return null;
        }
        target = target == null ? new TreeMap() : target;
        for (Iterator paths = srcsToRangeLists.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            SVNMergeRangeList rangeList = (SVNMergeRangeList) srcsToRangeLists.get(path);
            target.put(path, rangeList.dup());
        }
        return target;
    }
    
    public static boolean isNonInheritable(Map<String, SVNMergeRangeList> mergeInfo) {
        if (mergeInfo != null) {
            for (String path : mergeInfo.keySet()) {
                SVNMergeRangeList rangeList = mergeInfo.get(path);
                SVNMergeRange[] ranges = rangeList.getRanges();
                for (int i = 0; ranges != null && i < ranges.length; i++) {
                    if (ranges[i] != null && !ranges[i].isInheritable()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    public static Map<String, SVNMergeRangeList> parseMergeInfo(StringBuffer mergeInfo, Map<String, SVNMergeRangeList> srcPathsToRangeLists) throws SVNException {
        srcPathsToRangeLists = srcPathsToRangeLists == null ? new TreeMap<String, SVNMergeRangeList>() : srcPathsToRangeLists;
        if (mergeInfo.length() == 0) {
            return srcPathsToRangeLists;
        }

        try {
            while (mergeInfo.length() > 0) {
                int eolInd = mergeInfo.indexOf("\n");
                eolInd = eolInd < 0 ? mergeInfo.length() - 1 : eolInd;
                int ind = mergeInfo.lastIndexOf(":", eolInd);
                if (ind == -1) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                            "Pathname not terminated by ':'");
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);
                }
                if (ind == 0) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                    "No pathname preceding ':'");
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);                    
                }
                String path = null;
                if (mergeInfo.charAt(0) =='/') {
                    path = mergeInfo.substring(0, ind);
                } else {
                    String relativePath = mergeInfo.substring(0, ind);
                    path = "/" + relativePath;
                }
                mergeInfo = mergeInfo.delete(0, ind + 1);
                SVNMergeRange[] ranges = parseRevisionList(mergeInfo, path);
                if (mergeInfo.length() != 0 && mergeInfo.charAt(0) != '\n') {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                            "Could not find end of line in range list line in ''{0}''", mergeInfo);
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);
                }
                if (mergeInfo.length() > 0) {
                    mergeInfo = mergeInfo.deleteCharAt(0);
                }
                if (ranges.length > 1) {
                    Arrays.sort(ranges);
                    SVNMergeRange lastRange = ranges[0];
                    Collection newRanges = new ArrayList();
                    newRanges.add(lastRange);                    
                    for (int i = 1; i < ranges.length; i++) {
                        SVNMergeRange range = ranges[i];
                        if (lastRange.getStartRevision() <= range.getEndRevision() &&
                                range.getStartRevision() <= lastRange.getEndRevision()) {
                            
                            if (range.getStartRevision() < lastRange.getEndRevision() &&
                                    range.isInheritable() != lastRange.isInheritable()) {
                                // error.
                                String r1 = lastRange.toString();
                                String r2 = range.toString();
                                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                                        "Unable to parse overlapping revision ranges ''{0}'' and ''{1}'' with different inheritance types", 
                                        new Object[] {r1, r2});
                                SVNErrorManager.error(err, SVNLogType.WC);
                            } 
                            
                            if (lastRange.isInheritable() == range.isInheritable()) {
                                lastRange.setEndRevision(Math.max(range.getEndRevision(), lastRange.getEndRevision()));
                                continue;
                            }
                        }
                        newRanges.add(ranges[i]);
                        lastRange = ranges[i];
                    }
                    ranges = (SVNMergeRange[]) newRanges.toArray(new SVNMergeRange[newRanges.size()]); 
                }
                SVNMergeRangeList existingRange = (SVNMergeRangeList) srcPathsToRangeLists.get(path);
                if (existingRange != null) {
                    ranges = existingRange.merge(new SVNMergeRangeList(ranges)).getRanges();
                }
                srcPathsToRangeLists.put(path, new SVNMergeRangeList(ranges));
            }
        } catch (SVNException svne) {
            if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.MERGE_INFO_PARSE_ERROR) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                        "Could not parse mergeinfo string ''{0}''", mergeInfo.toString());
                SVNErrorManager.error(err, svne, SVNLogType.DEFAULT);
            }
            throw svne;
        }
        return srcPathsToRangeLists;
    }

    /**
     * Note: Make sure that this method is used only for making up an error message. 
     */
    public static String formatMergeInfoCatalogToString(Map catalog, String keyPrefix, String valuePrefix) {
        StringBuffer buffer = null;
        if (catalog != null && !catalog.isEmpty()) {
            buffer = new StringBuffer();
            for (Iterator catalogIter = catalog.keySet().iterator(); catalogIter.hasNext();) {
                String path1 = (String) catalogIter.next();
                if (path1.startsWith("/")) {
                    path1 = path1.substring(1);
                }
                Map mergeInfo = (Map) catalog.get(path1);
                if (keyPrefix != null) {
                    buffer.append(keyPrefix);
                }
                buffer.append(path1);
                buffer.append('\n');
                buffer.append(formatMergeInfoToString(mergeInfo, valuePrefix != null ? valuePrefix : ""));
                buffer.append('\n');
            }
        }
        return buffer != null ? buffer.toString() : "\n";
    }

    public static String formatMergeInfoCatalogToString2(Map<File, Map<String, SVNMergeRangeList>> catalog, String keyPrefix, String valuePrefix) {
        StringBuffer buffer = null;
        if (catalog != null && !catalog.isEmpty()) {
            buffer = new StringBuffer();
            for (Iterator<File> catalogIter = catalog.keySet().iterator(); catalogIter.hasNext();) {
                File path1 = catalogIter.next();
                String path1Str = SVNFileUtil.getFilePath(path1);
                if (path1Str.startsWith("/")) {
                    path1Str = path1Str.substring(1);
                }
                Map<String, SVNMergeRangeList> mergeInfo = catalog.get(path1);
                if (keyPrefix != null) {
                    buffer.append(keyPrefix);
                }
                buffer.append(path1Str);
                buffer.append('\n');
                buffer.append(formatMergeInfoToString(mergeInfo, valuePrefix != null ? valuePrefix : ""));
                buffer.append('\n');
            }
        }
        return buffer != null ? buffer.toString() : "\n";
    }
    
    /**
     * Each element of the resultant array is formed like this:
     * %s:%ld-%ld,.. where the first %s is a merge src path 
     * and %ld-%ld is startRev-endRev merge range.
     */
    public static String[] formatMergeInfoToArray(Map srcsToRangeLists, String prefix) {
        srcsToRangeLists = srcsToRangeLists == null ? Collections.EMPTY_MAP : srcsToRangeLists;
        String[] pathRanges = new String[srcsToRangeLists.size()];
        int k = 0;
        for (Iterator paths = srcsToRangeLists.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            SVNMergeRangeList rangeList = (SVNMergeRangeList) srcsToRangeLists.get(path);
            String output = (prefix != null ? prefix : "") + (path.startsWith("/") ? "" : "/") + path + ':' + rangeList;  
            pathRanges[k++] = output;
        }
        return pathRanges;
    }

    public static String formatMergeInfoToString(Map srcsToRangeLists, String prefix) {
        String[] infosArray = formatMergeInfoToArray(srcsToRangeLists, prefix);
        String result = "";
        for (int i = 0; i < infosArray.length; i++) {
            result += infosArray[i];
            if (i < infosArray.length - 1) {
                result += '\n';
            }
        }
        return result;
    }

    public static boolean shouldElideMergeInfo(Map<String, SVNMergeRangeList> parentMergeInfo, Map<String, SVNMergeRangeList> childMergeInfo, String pathSuffix) {
        boolean elides = false;
        if (childMergeInfo != null) {
            if (childMergeInfo.isEmpty()) {
                if (parentMergeInfo == null || parentMergeInfo.isEmpty()) {
                    elides = true;
                }
            } else if (!(parentMergeInfo == null || parentMergeInfo.isEmpty())) {
                Map pathTweakedMergeInfo = parentMergeInfo;
                if (pathSuffix != null) {
                    pathTweakedMergeInfo = new TreeMap();
                    for (Iterator paths = parentMergeInfo.keySet().iterator(); paths.hasNext();) {
                        String mergeSrcPath = (String) paths.next();
                        pathTweakedMergeInfo.put(SVNPathUtil.getAbsolutePath(SVNPathUtil.append(mergeSrcPath, 
                                pathSuffix)), parentMergeInfo.get(mergeSrcPath));
                    }
                } 
                elides = mergeInfoEquals(pathTweakedMergeInfo, childMergeInfo, true);
            }
        }
        return elides;
    }
    
    public static void elideMergeInfo(Map parentMergeInfo, Map childMergeInfo, File path, 
            String pathSuffix, SVNWCAccess access) throws SVNException {
        boolean elides = shouldElideMergeInfo(parentMergeInfo, childMergeInfo, pathSuffix);
        if (elides) {
            SVNPropertiesManager.setProperty(access, path, SVNProperty.MERGE_INFO, null, true);
        }
    }
    
    public static boolean mergeInfoEquals(Map mergeInfo1, Map mergeInfo2, 
            boolean considerInheritance) {
        mergeInfo1 = mergeInfo1 == null ? Collections.EMPTY_MAP : mergeInfo1;
        mergeInfo2 = mergeInfo2 == null ? Collections.EMPTY_MAP : mergeInfo2;
        
        if (mergeInfo1.size() == mergeInfo2.size()) {
            Map deleted = new SVNHashMap();
            Map added = new SVNHashMap();
            diffMergeInfo(deleted, added, mergeInfo1, mergeInfo2, considerInheritance);
            return deleted.isEmpty() && added.isEmpty();
        }
        return false;
    }
    
    public static String[] findMergeSources(long revision, Map mergeInfo) {
        LinkedList mergeSources = new LinkedList();
        for (Iterator paths = mergeInfo.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            SVNMergeRangeList rangeList = (SVNMergeRangeList) mergeInfo.get(path);
            if (rangeList.includes(revision)) {
                mergeSources.add(path);
            }
        }
        return (String[]) mergeSources.toArray(new String[mergeSources.size()]);
    }
    
    public static Map<String, SVNMergeRangeList> getInheritableMergeInfo(Map<String, SVNMergeRangeList> mergeInfo, String path, long startRev, long endRev) {
        return getInheritableMergeInfo(mergeInfo, path, startRev, endRev, true);
    }

    public static Map<String, SVNMergeRangeList> getInheritableMergeInfo(Map<String, SVNMergeRangeList> mergeInfo, String path, long startRev, long endRev, boolean inheritable) {
        Map<String, SVNMergeRangeList> inheritableMergeInfo = new TreeMap<String, SVNMergeRangeList>();
        if (mergeInfo != null) {
            for (Iterator<String> paths = mergeInfo.keySet().iterator(); paths.hasNext();) {
                String mergeSrcPath = (String) paths.next();
                SVNMergeRangeList rangeList = (SVNMergeRangeList) mergeInfo.get(mergeSrcPath);
                SVNMergeRangeList inheritableRangeList = null;
                if (path == null || path.equals(mergeSrcPath)) {
                    inheritableRangeList = rangeList.getInheritableRangeList(startRev, endRev, inheritable);
                } else {
                    inheritableRangeList = rangeList.dup();
                }
                if (!inheritableRangeList.isEmpty()) {
                    inheritableMergeInfo.put(mergeSrcPath, inheritableRangeList);
                }
            }
        }
        return inheritableMergeInfo;
    }
    
    public static Map removeMergeInfo(Map eraser, Map whiteBoard) {
        return removeMergeInfo(eraser, whiteBoard, true);
    }

    public static Map<String, SVNMergeRangeList> removeMergeInfo(Map<String, SVNMergeRangeList> eraser, Map<String, SVNMergeRangeList> whiteBoard, boolean considerInheritance) {
        Map<String, SVNMergeRangeList> mergeInfo = new TreeMap<String, SVNMergeRangeList>();
        walkMergeInfoHashForDiff(mergeInfo, null, whiteBoard, eraser, considerInheritance);
        return mergeInfo;
    }

    public static Map intersectMergeInfo(Map mergeInfo1, Map mergeInfo2) {
        return intersectMergeInfo(mergeInfo1, mergeInfo2, true);
    }

    public static Map<String, SVNMergeRangeList> intersectMergeInfo(Map mergeInfo1, Map mergeInfo2, boolean considerInheritance) {
        Map<String, SVNMergeRangeList> mergeInfo = new TreeMap<String, SVNMergeRangeList>();
        for (Iterator pathsIter = mergeInfo1.keySet().iterator(); pathsIter.hasNext();) {
            String path = (String) pathsIter.next();
            SVNMergeRangeList rangeList1 = (SVNMergeRangeList) mergeInfo1.get(path);
            SVNMergeRangeList rangeList2 = (SVNMergeRangeList) mergeInfo2.get(path);
            if (rangeList2 != null) {
                rangeList2 = rangeList2.intersect(rangeList1, considerInheritance);
                if (!rangeList2.isEmpty()) {
                    mergeInfo.put(path, rangeList2.dup());
                }
            }
        }
        return mergeInfo;
    }
    
    public static SVNMergeRange[] parseRevisionList(StringBuffer mergeInfo, String path) throws SVNException {
        Collection ranges = new LinkedList();
        while (mergeInfo.length() > 0 && mergeInfo.charAt(0) != '\n' && 
                Character.isWhitespace(mergeInfo.charAt(0))) {
            mergeInfo = mergeInfo.deleteCharAt(0);
        }
        if (mergeInfo.length() == 0 || mergeInfo.charAt(0) == '\n') {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                    "Mergeinfo for ''{0}'' maps to an empty revision range", path);
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        
        while (mergeInfo.length() > 0 && mergeInfo.charAt(0) != '\n') {
            long startRev = parseRevision(mergeInfo);
            if (mergeInfo.length() > 0 && mergeInfo.charAt(0) != '\n' && 
                mergeInfo.charAt(0) != '-' && mergeInfo.charAt(0) != ',' && 
                mergeInfo.charAt(0) != '*') {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                        "Invalid character ''{0}'' found in revision list", 
                        new Character(mergeInfo.charAt(0)));
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }
            
            SVNMergeRange range = new SVNMergeRange(startRev - 1, startRev, true);
            if (mergeInfo.length() > 0 && mergeInfo.charAt(0) == '-') {
                mergeInfo = mergeInfo.deleteCharAt(0);
                long endRev = parseRevision(mergeInfo);
                if (startRev > endRev) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                            "Unable to parse reversed revision range ''{0}-{1}''",
                            new Object[] { new Long(startRev), new Long(endRev) });
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);
                } else if (startRev == endRev) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                            "Unable to parse revision range ''{0}-{1}'' with same start and end revisions",
                            new Object[] { new Long(startRev), new Long(endRev) });
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);
                }
                range.setEndRevision(endRev);
            }
            
            if (mergeInfo.length() == 0 || mergeInfo.charAt(0) == '\n') {
                ranges.add(range);
                return (SVNMergeRange[]) ranges.toArray(new SVNMergeRange[ranges.size()]);
            } else if (mergeInfo.length() > 0 && mergeInfo.charAt(0) == ',') {
                ranges.add(range);
                mergeInfo = mergeInfo.deleteCharAt(0);
            } else if (mergeInfo.length() > 0 && mergeInfo.charAt(0) == '*') {
                range.setInheritable(false);
                mergeInfo = mergeInfo.deleteCharAt(0);
                if (mergeInfo.length() == 0 || mergeInfo.charAt(0) == ',' || mergeInfo.charAt(0) == '\n') {
                    ranges.add(range);
                    if (mergeInfo.length() > 0 && mergeInfo.charAt(0) == ',') {
                        mergeInfo = mergeInfo.deleteCharAt(0);
                    } else {
                        return (SVNMergeRange[]) ranges.toArray(new SVNMergeRange[ranges.size()]);
                    }
                } else {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                                                                 "Invalid character ''{0}'' found in range list", 
                                                                 mergeInfo.length() > 0 ?  mergeInfo.charAt(0) + "" : "");
                    SVNErrorManager.error(err, SVNLogType.DEFAULT);
                }
            } else {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, 
                                                             "Invalid character ''{0}'' found in range list", 
                                                             mergeInfo.length() > 0 ?  mergeInfo.charAt(0) + "" : "");
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }
        }
        
        if (mergeInfo.length() == 0 || mergeInfo.charAt(0) != '\n' ) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MERGE_INFO_PARSE_ERROR, "Range list parsing ended before hitting newline");
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        
        return (SVNMergeRange[]) ranges.toArray(new SVNMergeRange[ranges.size()]);
    }
    
    public static Map<String, SVNMergeRangeList> appendSuffix(Map<String, SVNMergeRangeList> mergeinfo, String suffix) {
        Map<String, SVNMergeRangeList> result = new TreeMap<String, SVNMergeRangeList>();
        for (String path : mergeinfo.keySet()) {
            String pathWithSuffix = SVNPathUtil.append(path, suffix);
            result.put(pathWithSuffix, mergeinfo.get(path));
        }
        return result;
    }

    /**
     * @return [deletedList, addedList]
     */
    public static SVNMergeRangeList[] diffMergeRangeLists(SVNMergeRangeList fromRangeList, SVNMergeRangeList toRangeList, 
            boolean considerInheritance) {
        SVNMergeRangeList deletedRangeList = fromRangeList.diff(toRangeList, considerInheritance);
        SVNMergeRangeList addedRangeList = toRangeList.diff(fromRangeList, considerInheritance);
        return new SVNMergeRangeList[] { deletedRangeList, addedRangeList };
    }
    
    private static long parseRevision(StringBuffer mergeInfo) throws SVNException {
        int ind = 0;
        while (ind < mergeInfo.length() && Character.isDigit(mergeInfo.charAt(ind))) {
            ind++;
        }
        
        if (ind == 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REVISION_NUMBER_PARSE_ERROR, 
                                                         "Invalid revision number found parsing ''{0}''", 
                                                         mergeInfo.toString());
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        
        String numberStr = mergeInfo.substring(0, ind);
        long rev = -1;
        try {
            rev = Long.parseLong(numberStr);
        } catch (NumberFormatException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REVISION_NUMBER_PARSE_ERROR, 
                                                         "Invalid revision number found parsing ''{0}''", 
                                                         mergeInfo.toString());
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }

        if (rev < 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REVISION_NUMBER_PARSE_ERROR, 
                                                         "Negative revision number found parsing ''{0}''", 
                                                         mergeInfo.toString());
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        
        mergeInfo = mergeInfo.delete(0, ind);
        return rev;
    }
    
    private static void walkMergeInfoHashForDiff(Map deleted, Map added, Map from, Map to, 
            boolean considerInheritance) {
        for (Iterator paths = from.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            SVNMergeRangeList fromRangeList = (SVNMergeRangeList) from.get(path);
            SVNMergeRangeList toRangeList = (SVNMergeRangeList) to.get(path);
            if (toRangeList != null) {
                SVNMergeRangeList[] rangeListDiff = diffMergeRangeLists(fromRangeList, toRangeList, considerInheritance);
                
                SVNMergeRangeList deletedRangeList = rangeListDiff[0];
                SVNMergeRangeList addedRangeList = rangeListDiff[1];
                if (deleted != null && deletedRangeList.getSize() > 0) {
                    deleted.put(path, deletedRangeList);
                }
                if (added != null && addedRangeList.getSize() > 0) {
                    added.put(path, addedRangeList);
                }
            } else if (deleted != null) {
                deleted.put(path, fromRangeList.dup());
            }
        }
        
        if (added == null) {
            return;
        }
        
        for (Iterator paths = to.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            SVNMergeRangeList toRangeList = (SVNMergeRangeList) to.get(path);
            if (!from.containsKey(path)) {
                added.put(path, toRangeList.dup());
            }
        }        
    }

	private static class ElideMergeInfoCatalogHandler implements ISVNCommitPathHandler {
        private Map myMergeInfoCatalog;
        private List myElidablePaths;
        
        public ElideMergeInfoCatalogHandler(Map mergeInfoCatalog) {
            myMergeInfoCatalog = mergeInfoCatalog;
            myElidablePaths = new LinkedList();
        }
        
        public boolean handleCommitPath(String path, ISVNEditor editor) throws SVNException {
            ElideMergeInfoEditor elideEditor = (ElideMergeInfoEditor) editor;
	        String inheritedMergeInfoPath = elideEditor.getInheritedMergeInfoPath();
	        if (inheritedMergeInfoPath == null || "/".equals(path)) {
	            return false;
	        }
	        String pathSuffix = SVNPathUtil.getPathAsChild(inheritedMergeInfoPath, path);
	        if (pathSuffix == null) {
	            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "path suffix is null");
	            SVNErrorManager.error(err, SVNLogType.DEFAULT);
	        }
	        boolean elides = shouldElideMergeInfo((Map) myMergeInfoCatalog.get(inheritedMergeInfoPath), 
	                (Map) myMergeInfoCatalog.get(path), pathSuffix);
	        if (elides) {
	            myElidablePaths.add(path);
	        }
	        return false;
	    }
        
        public List getElidablePaths() {
            return myElidablePaths;
        }
	}
	
	private static class ElideMergeInfoEditor implements ISVNEditor {

	    private Map myMergeInfoCatalog;
	    private ElideMergeInfoCatalogDirBaton myCurrentDirBaton;
	    
	    public ElideMergeInfoEditor(Map mergeInfoCatalog) {
	        myMergeInfoCatalog = mergeInfoCatalog;
	    }
	    
        public void abortEdit() throws SVNException {
        }

        public void absentDir(String path) throws SVNException {
        }

        public void absentFile(String path) throws SVNException {
        }

        public void addDir(String path, String copyFromPath, long copyFromRevision) throws SVNException {
        }

        public void addFile(String path, String copyFromPath, long copyFromRevision) throws SVNException {
        }

        public void changeDirProperty(String name, SVNPropertyValue value) throws SVNException {
        }

        public void changeFileProperty(String path, String propertyName, SVNPropertyValue propertyValue) throws SVNException {
        }

        public void closeDir() throws SVNException {
        }

        public SVNCommitInfo closeEdit() throws SVNException {
            return null;
        }

        public void closeFile(String path, String textChecksum) throws SVNException {
        }

        public void deleteEntry(String path, long revision) throws SVNException {
        }

        public void openDir(String path, long revision) throws SVNException {
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            
            ElideMergeInfoCatalogDirBaton dirBaton = new ElideMergeInfoCatalogDirBaton();
            if (myMergeInfoCatalog.get(path) != null) {
                dirBaton.myInheritedMergeInfoPath = path;
            } else {
                dirBaton.myInheritedMergeInfoPath = myCurrentDirBaton.myInheritedMergeInfoPath;
            }
            myCurrentDirBaton = dirBaton;
        }

        public void openFile(String path, long revision) throws SVNException {
        }

        public void openRoot(long revision) throws SVNException {
            myCurrentDirBaton = new ElideMergeInfoCatalogDirBaton();
        }

        public void targetRevision(long revision) throws SVNException {
        }

        public void applyTextDelta(String path, String baseChecksum) throws SVNException {
        }

        public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException {
            return SVNFileUtil.DUMMY_OUT;
        }

        public void textDeltaEnd(String path) throws SVNException {
        }

        public String getInheritedMergeInfoPath() {
            return myCurrentDirBaton.myInheritedMergeInfoPath;
        }
        
        private class ElideMergeInfoCatalogDirBaton {
            private String myInheritedMergeInfoPath;
        }
	    
	}

}
