package org.tmatesoft.svn.core.internal.wc2.ng;

import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNMergeInfoInheritance;
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.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNURLUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.SVNWCUtils;
import org.tmatesoft.svn.core.internal.wc17.db.Structure;
import org.tmatesoft.svn.core.internal.wc2.SvnRepositoryAccess;
import org.tmatesoft.svn.core.internal.wc2.SvnRepositoryAccess.LocationsInfo;
import org.tmatesoft.svn.core.internal.wc2.SvnRepositoryAccess.RepositoryInfo;
import org.tmatesoft.svn.core.internal.wc2.SvnWcGeneration;
import org.tmatesoft.svn.core.io.SVNLocationSegment;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.ISvnObjectReceiver;
import org.tmatesoft.svn.core.wc2.SvnGetProperties;
import org.tmatesoft.svn.core.wc2.SvnMerge;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNLogType;

public class SvnNgMergeReintegrate extends SvnNgOperationRunner<Void, SvnMerge>{

    @Override
    public boolean isApplicable(SvnMerge operation, SvnWcGeneration wcGeneration) throws SVNException {
        if (super.isApplicable(operation, wcGeneration)) {
            return operation.isReintegrate();
        }
        return false;
    }

    @Override
    protected Void run(SVNWCContext context) throws SVNException {
        File lockPath = getLockPath(getFirstTarget());
        if (getOperation().isDryRun()) {
            merge(context, getOperation().getSource(), getFirstTarget(), getOperation().isDryRun());
            
        } else {
            
            try {
                lockPath = getWcContext().acquireWriteLock(lockPath, false, true);
                merge(context, getOperation().getSource(), getFirstTarget(), getOperation().isDryRun());
            } finally {
                getWcContext().releaseWriteLock(lockPath);
                sleepForTimestamp();
            }
        }
        return null;
    }
    
    private File getLockPath(File firstTarget) throws SVNException {
        SVNNodeKind kind = getWcContext().readKind(firstTarget, false);
        if (kind == SVNNodeKind.DIR) {
            return firstTarget;
        } else {
            return SVNFileUtil.getParentFile(firstTarget);
        }
    }

    private void merge(SVNWCContext context, SvnTarget mergeSource, File mergeTarget, boolean dryRun) throws SVNException {
        SVNFileType targetKind = SVNFileType.getType(mergeTarget);
        if (targetKind == SVNFileType.NONE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_PATH_NOT_FOUND, "Path ''{0}'' does not exist", mergeTarget);
            SVNErrorManager.error(err, SVNLogType.WC);
        }     
        
        SVNURL url2 = getRepositoryAccess().getTargetURL(mergeSource);
        if (url2 == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "''{0}'' has no URL", mergeTarget);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        
        SVNURL wcReposRoot = context.getNodeReposInfo(mergeTarget).reposRootUrl;
        Structure<RepositoryInfo> sourceReposInfo = getRepositoryAccess().createRepositoryFor(mergeSource, mergeSource.getPegRevision(), mergeSource.getPegRevision(), null);
        SVNURL sourceReposRoot = ((SVNRepository) sourceReposInfo.get(RepositoryInfo.repository)).getRepositoryRoot(true);
        
        if (!wcReposRoot.equals(sourceReposRoot)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, 
                    "''{0}'' must be from the same repositor as ''{1}''", mergeSource.getURL(), mergeTarget);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        SvnNgMergeDriver mergeDriver = new SvnNgMergeDriver(getWcContext(), getOperation(), getRepositoryAccess(), getOperation().getMergeOptions());

        mergeDriver.ensureWcIsSuitableForMerge(mergeTarget, false, false, false);
        
        long targetBaseRev = context.getNodeBaseRev(mergeTarget);
        long rev1 = targetBaseRev;
        File sourceReposRelPath = new File(SVNURLUtil.getRelativeURL(wcReposRoot, url2, false));
        File targetReposRelPath = context.getNodeReposRelPath(mergeTarget);
        
        if ("".equals(sourceReposRelPath.getPath()) || "".equals(targetReposRelPath.getPath())) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_NOT_READY_TO_MERGE, 
                    "Neither reintegrate source nor target can be the root of repository");
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        final Map<File, String> explicitMergeInfo = new HashMap<File, String>();
        SvnGetProperties pg = getOperation().getOperationFactory().createGetProperties();
        pg.setDepth(SVNDepth.INFINITY);
        pg.setSingleTarget(SvnTarget.fromFile(mergeTarget));
        pg.setReceiver(new ISvnObjectReceiver<SVNProperties>() {
            public void receive(SvnTarget target, SVNProperties props) throws SVNException {
                final String value = props.getStringValue(SVNProperty.MERGE_INFO);
                if (value != null) {
                    explicitMergeInfo.put(target.getFile(), value);
                }
            }
        });
        pg.run();
        
        if (!explicitMergeInfo.isEmpty()) {
            final Map<File, File> externals = context.getDb().getExternalsDefinedBelow(mergeTarget);
            for (Iterator<File> wcPaths = explicitMergeInfo.keySet().iterator(); wcPaths.hasNext();) {
                final File wcPath = wcPaths.next();
                if (externals.containsKey(wcPath)) {
                    wcPaths.remove();
                }
            }
        }
        
        sourceReposInfo = getRepositoryAccess().createRepositoryFor(SvnTarget.fromURL(url2), SVNRevision.UNDEFINED, mergeSource.getPegRevision(), null);
        SVNRepository sourceRepository = sourceReposInfo.get(RepositoryInfo.repository);
        long rev2 = sourceReposInfo.lng(RepositoryInfo.revision);
        url2 = sourceReposInfo.get(RepositoryInfo.url);
        sourceReposInfo.release();
        
        SVNURL targetUrl = context.getNodeUrl(mergeTarget);
        SVNRepository targetRepository = getRepositoryAccess().createRepository(targetUrl, null, false);
        //
        try {
            Map<File, Map<String, SVNMergeRangeList>> mergedToSourceCatalog = new HashMap<File, Map<String,SVNMergeRangeList>>();
            Map<File, Map<String, SVNMergeRangeList>> unmergedToSourceCatalog = new HashMap<File, Map<String,SVNMergeRangeList>>();
            SvnTarget url1 = calculateLeftHandSide(context,
                    mergedToSourceCatalog,
                    unmergedToSourceCatalog, 
                    mergeTarget,
                    targetReposRelPath,
                    explicitMergeInfo,
                    targetBaseRev,
                    sourceReposRelPath,
                    sourceReposRoot,
                    wcReposRoot,
                    rev2,
                    sourceRepository,
                    targetRepository);
            
            if (url1 == null) {
                return;
            }
            
            if (!url1.getURL().equals(targetUrl)) {
                targetRepository.setLocation(url1.getURL(), false);
            }
            rev1 = url1.getPegRevision().getNumber();
            SVNLocationSegment yc = getRepositoryAccess().getYoungestCommonAncestor(url2, rev2, url1.getURL(), rev1);
            
            if (yc == null || !(yc.getPath() != null && yc.getStartRevision() >= 0)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_NOT_READY_TO_MERGE, 
                        "''{0}''@''{1}'' must be ancestrally related to ''{2}''@''{3}''", url1, rev1, url2, rev2);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            
            if (rev1 > yc.getStartRevision()) {
                Map<File, Map<String, SVNMergeRangeList>> finalUnmergedCatalog = new HashMap<File, Map<String,SVNMergeRangeList>>();
                String ycPath = yc.getPath();
                if (ycPath.startsWith("/")) {
                    ycPath = ycPath.substring(1);
                }
                findUnsyncedRanges(sourceReposRelPath, new File(ycPath), 
                        unmergedToSourceCatalog, mergedToSourceCatalog, finalUnmergedCatalog, targetRepository);
                
                if (!finalUnmergedCatalog.isEmpty()) {
                    String catalog = SVNMergeInfoUtil.formatMergeInfoCatalogToString2(finalUnmergedCatalog, "  ", "    Missing ranges: ");
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_NOT_READY_TO_MERGE, 
                            "Reintegrate can only be used if revisions {0} through {1} were " +
                            "previously merged from {2} to the reintegrate source, but this is " +
                            "not the case:\n{3}", new Object[] {yc.getStartRevision() + 1, rev2, targetUrl, catalog});
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
            
            mergeDriver.mergeCousinsAndSupplementMergeInfo(mergeTarget, 
                    targetRepository, sourceRepository, 
                    url1.getURL(), rev1, 
                    url2, rev2, 
                    yc.getStartRevision(), 
                    sourceReposRoot, 
                    wcReposRoot, 
                    SVNDepth.INFINITY,
                    false,
                    false, 
                    false, 
                    false, 
                    dryRun);
        } finally {
            targetRepository.closeSession();
        }
    }
    
    private void findUnsyncedRanges(
            final File sourceReposRelPath,
            final File targetReposRelPath,
            Map<File, Map<String, SVNMergeRangeList>> unmergedToSourceCatalog,
            final Map<File, Map<String, SVNMergeRangeList>> mergedToSourceCatalog,
            final Map<File, Map<String, SVNMergeRangeList>> finalUnmergedCatalog, SVNRepository repos) throws SVNException {
        
        SVNMergeRangeList potentiallyUnmergedRanges = null;
        if (unmergedToSourceCatalog != null) {
            potentiallyUnmergedRanges = new SVNMergeRangeList(new SVNMergeRange[0]);
            for (File cpath : unmergedToSourceCatalog.keySet()) {
                Map<String, SVNMergeRangeList> mi = unmergedToSourceCatalog.get(cpath);
                for (SVNMergeRangeList mrl : mi.values()) {
                    potentiallyUnmergedRanges = potentiallyUnmergedRanges.merge(mrl);
                }
            }
        }
        if (potentiallyUnmergedRanges != null && !potentiallyUnmergedRanges.isEmpty()) {
            long youngest = potentiallyUnmergedRanges.getRanges()[0].getStartRevision() + 1;
            long oldest = potentiallyUnmergedRanges.getRanges()[potentiallyUnmergedRanges.getSize() - 1].getEndRevision();
            repos.log(new String[] {""}, youngest, oldest, true, false, -1, false, null, new ISVNLogEntryHandler() {
                public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
                    for (String changedPath : logEntry.getChangedPaths().keySet()) {
                        if (changedPath.startsWith("/")) {
                            changedPath = changedPath.substring(1);
                        }
                        String relPath = SVNWCUtils.isChild(SVNFileUtil.getFilePath(targetReposRelPath), changedPath);
                        if (relPath == null) {
                            continue;
                        }
                        File sourceRelpath = SVNFileUtil.createFilePath(sourceReposRelPath, relPath);
                        String mergeInfoForPath = "/" + changedPath + ":" + logEntry.getRevision();
                        Map<String, SVNMergeRangeList> mi = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(mergeInfoForPath), null);
                        File[] subtreeMissing = new File[1];
                        boolean inCatalog = isMergeinfoInCatalog(sourceRelpath, subtreeMissing, mi, logEntry.getRevision(), mergedToSourceCatalog);
                        if (!inCatalog) {
                            File missingPath = null;
                            File subtreeMissingInThisRev = subtreeMissing[0];
                            if (subtreeMissingInThisRev == null) {
                                subtreeMissingInThisRev = sourceReposRelPath;
                            }
                            if (subtreeMissingInThisRev != null && !subtreeMissingInThisRev.equals(sourceRelpath)) {
                                missingPath = SVNWCUtils.skipAncestor(subtreeMissingInThisRev, sourceRelpath);
                                missingPath = new File(changedPath.substring(0, changedPath.length() - missingPath.getPath().length()));
                            } else {
                                missingPath = new File(changedPath);
                            }
                            Map<String, SVNMergeRangeList> mi2 = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer("/" + SVNFileUtil.getFilePath(missingPath) + ":" + logEntry.getRevision()), null);
                            Map<String, SVNMergeRangeList> existing = finalUnmergedCatalog.get(missingPath);
                            if (existing != null) {
                                existing = SVNMergeInfoUtil.mergeMergeInfos(existing, mi2);
                            } else {
                                existing = mi2;
                            }
                            finalUnmergedCatalog.put(subtreeMissingInThisRev, existing);
                        }
                    }
                }
            });
        }
        
    }
    
    private boolean isMergeinfoInCatalog(File sourceRelpath, File[] catPath, Map<String, SVNMergeRangeList> mergeinfo, 
            long revision, Map<File, Map<String, SVNMergeRangeList>> catalog) throws SVNException {
        if (mergeinfo != null && catalog != null && !catalog.isEmpty()) {
            File path = sourceRelpath;
            String walkPath = null;
            Map<String, SVNMergeRangeList> miInCatalog = null;
            while(true) {
                miInCatalog = catalog.get(path);
                if (miInCatalog != null) {
                    if (catPath != null) {
                        catPath[0] = path;
                    }
                    break;
                } else {
                    walkPath = walkPath != null ? SVNPathUtil.append(SVNFileUtil.getFileName(path), walkPath) : SVNFileUtil.getFileName(path);
                    path = path.getParentFile();
                    if (path == null) {
                        break;
                    }
                }
            }
            if (miInCatalog != null) {
                if (walkPath != null) { 
                    miInCatalog = SVNMergeInfoUtil.adjustMergeInfoSourcePaths(null, walkPath, miInCatalog);
                }
                miInCatalog = SVNMergeInfoUtil.intersectMergeInfo(miInCatalog, mergeinfo, true);
                return SVNMergeInfoUtil.mergeInfoEquals(mergeinfo, miInCatalog, true);
                
            }
        }
        return false;
    }

    private SvnTarget calculateLeftHandSide(SVNWCContext context,
            Map<File, Map<String, SVNMergeRangeList>>  mergedToSourceCatalog,
            Map<File, Map<String, SVNMergeRangeList>>  unmergedToSourceCatalog,
            File targetAbsPath,
            File targetReposRelPath,
            Map<File, String> subtreesWithMergeInfo,
            long targetRev,
            File sourceReposRelPath,
            SVNURL sourceReposRoot,
            SVNURL targetReposRoot,
            long sourceRev,
            SVNRepository sourceRepository,
            SVNRepository targetRepository)  throws SVNException {
        
        if (!subtreesWithMergeInfo.containsKey(targetAbsPath)) {
            subtreesWithMergeInfo.put(targetAbsPath, "");
        }
        
        final Map<File, List<SVNLocationSegment>> segmentsMap = new HashMap<File, List<SVNLocationSegment>>();
        
        for (File path : subtreesWithMergeInfo.keySet()) {
            String miValue = subtreesWithMergeInfo.get(path);
            try {
                SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(miValue), null);
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() == SVNErrorCode.MERGE_INFO_PARSE_ERROR) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_MERGEINFO_NO_MERGETRACKING,
                            "Invalid mergeinfo detected on ''{0}'', reintegrate merge not possible", path);
                    SVNErrorManager.error(err, SVNLogType.WC);                    
                }
                throw e;
            }
            File pathReposRelPath = context.getNodeReposRelPath(path);
            File pathSessionRelPath = SVNWCUtils.skipAncestor(targetReposRelPath, pathReposRelPath);
            if (pathSessionRelPath == null && pathReposRelPath.equals(targetReposRelPath)) {
                pathSessionRelPath = new File("");
            }
            
            List<SVNLocationSegment> segments = targetRepository.getLocationSegments(SVNFileUtil.getFilePath(pathSessionRelPath), targetRev, targetRev, -1);
            segmentsMap.put(pathReposRelPath, segments);
        }
        
        SVNURL sourceUrl = SVNWCUtils.join(sourceReposRoot, sourceReposRelPath);
        SVNURL targetUrl = SVNWCUtils.join(targetReposRoot, targetReposRelPath);
        SVNLocationSegment yc = getRepositoryAccess().getYoungestCommonAncestor(sourceUrl, sourceRev, targetUrl, targetRev);
        if (!(yc != null && yc.getPath() != null && yc.getStartRevision() >= 0)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_NOT_READY_TO_MERGE, 
                    "''{0}''@''{1}'' must be ancestrally related to ''{2}''@''{3}''", sourceUrl, sourceRev, targetUrl, targetRev);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        
        if (sourceRev == yc.getStartRevision()) {
            return null;
        }
        
        Map<File, Map<String, SVNMergeRangeList>> mergeInfoCatalog = 
                SvnNgMergeinfoUtil.convertToCatalog2(sourceRepository.getMergeInfo(new String[] {""}, sourceRev, SVNMergeInfoInheritance.INHERITED, true));
        if (mergedToSourceCatalog != null) {
            mergedToSourceCatalog.putAll(mergeInfoCatalog);
        }
        UnmergedMergeInfo unmergedMergeInfo = findUnmergedMergeInfo(yc.getStartRevision(), mergeInfoCatalog, segmentsMap, sourceReposRelPath, targetReposRelPath, targetRev, sourceRev, sourceRepository, targetRepository);
        unmergedMergeInfo.catalog = SVNMergeInfoUtil.elideMergeInfoCatalog(unmergedMergeInfo.catalog);
        if (unmergedToSourceCatalog != null && unmergedMergeInfo.catalog != null) {
            for (String path : unmergedMergeInfo.catalog.keySet()) {
                Map<String, SVNMergeRangeList> mi = unmergedMergeInfo.catalog.get(path);
                if (path.startsWith("/")) {
                    path = path.substring(1);
                }
                path = path.replace(File.separatorChar, '/');
                unmergedToSourceCatalog.put(new File(path), mi);
            }
        }
        if (unmergedMergeInfo.neverSynced) {
            return SvnTarget.fromURL(sourceReposRoot.appendPath(yc.getPath(), false), SVNRevision.create(yc.getStartRevision()));
        } else {
            Structure<LocationsInfo> locations = getRepositoryAccess().getLocations(targetRepository, SvnTarget.fromURL(targetUrl), 
                    SVNRevision.create(targetRev), 
                    SVNRevision.create(unmergedMergeInfo.youngestMergedRevision), 
                    SVNRevision.UNDEFINED);
            SVNURL youngestUrl = locations.get(LocationsInfo.startUrl);
            locations.release();
            return SvnTarget.fromURL(youngestUrl, SVNRevision.create(unmergedMergeInfo.youngestMergedRevision));
        }
    }

    private UnmergedMergeInfo findUnmergedMergeInfo(long ycAncestorRev, Map<File, Map<String, SVNMergeRangeList>> sourceCatalog, Map<File, List<SVNLocationSegment>> targetSegments,
            File sourceReposRelPath, File targetReposRelPath, long targetRev, long sourceRev, SVNRepository sourceRepos, SVNRepository targetRepos) throws SVNException {
        UnmergedMergeInfo result = new UnmergedMergeInfo();
        result.neverSynced = true;
        Map<String, Map<String, SVNMergeRangeList>> newCatalog = new TreeMap<String, Map<String,SVNMergeRangeList>>();
        for (File path : targetSegments.keySet()) {
            List<SVNLocationSegment> segments = targetSegments.get(path);
            File sourcePathRelToSession = SVNWCUtils.skipAncestor(targetReposRelPath, path);
            if (sourcePathRelToSession == null && targetReposRelPath.equals(path)) {
                sourcePathRelToSession = new File("");
            }
            File sourcePath = SVNFileUtil.createFilePath(sourceReposRelPath, sourcePathRelToSession);
            Map<String, SVNMergeRangeList> targetHistoryAsMergeInfo = SvnRepositoryAccess.getMergeInfoFromSegments(segments);
            targetHistoryAsMergeInfo = SVNMergeInfoUtil.filterMergeInfoByRanges(targetHistoryAsMergeInfo, sourceRev, ycAncestorRev);
            Map<String, SVNMergeRangeList> sourceMergeInfo = sourceCatalog.get(sourcePath);
            
            if (sourceMergeInfo != null) {
                sourceCatalog.remove(SVNFileUtil.getFileDir(sourcePath));
                Map<String, SVNMergeRangeList> explicitIntersection = SVNMergeInfoUtil.intersectMergeInfo(sourceMergeInfo, targetHistoryAsMergeInfo, true);
                if (explicitIntersection != null && !explicitIntersection.isEmpty()) {
                    result.neverSynced = false;
                    long[] endPoints = SVNMergeInfoUtil.getRangeEndPoints(explicitIntersection);
                    if (result.youngestMergedRevision < 0 || endPoints[0] > result.youngestMergedRevision) {
                        result.youngestMergedRevision = endPoints[0];
                    }
                }
            } else {
                SVNNodeKind kind = sourceRepos.checkPath(SVNFileUtil.getFilePath(sourcePathRelToSession), sourceRev);
                if (kind == SVNNodeKind.NONE) {
                    continue;
                }
                Map<File, Map<String, SVNMergeRangeList>> subtreeCatalog = 
                        SvnNgMergeinfoUtil.convertToCatalog2(sourceRepos.getMergeInfo(new String[] {SVNFileUtil.getFilePath(sourcePathRelToSession)}, sourceRev, SVNMergeInfoInheritance.INHERITED, false));
                sourceMergeInfo = subtreeCatalog.get(sourcePathRelToSession);
                if (sourceMergeInfo == null) {
                    sourceMergeInfo = new HashMap<String, SVNMergeRangeList>();
                }
            }
            
            segments = sourceRepos.getLocationSegments(SVNFileUtil.getFilePath(sourcePathRelToSession), sourceRev, sourceRev, -1);
            Map<String, SVNMergeRangeList> sourceHistroryAsMergeInfo = SvnRepositoryAccess.getMergeInfoFromSegments(segments);
            sourceMergeInfo = SVNMergeInfoUtil.mergeMergeInfos(sourceMergeInfo, sourceHistroryAsMergeInfo);
            Map<String, SVNMergeRangeList> commonMergeInfo = SVNMergeInfoUtil.intersectMergeInfo(sourceMergeInfo, targetHistoryAsMergeInfo, true);
            Map<String, SVNMergeRangeList> filteredMergeInfo = SVNMergeInfoUtil.removeMergeInfo(commonMergeInfo, targetHistoryAsMergeInfo, true);
            
            newCatalog.put(SVNFileUtil.getFilePath(sourcePath), filteredMergeInfo);
        }
        
        if (!sourceCatalog.isEmpty()) {
            for(File path : sourceCatalog.keySet()) {
                File sourcePathRelToSession =  sourceReposRelPath.equals(path) ? new File("") : SVNWCUtils.skipAncestor(sourceReposRelPath, path);
                
                File targetPath = sourceReposRelPath.equals(path) ? new File("") : SVNWCUtils.skipAncestor(sourceReposRelPath, path);
                List<SVNLocationSegment> segments = null;
                Map<String, SVNMergeRangeList> sourceMergeInfo = sourceCatalog.get(path);
                try {
                    segments = targetRepos.getLocationSegments(SVNFileUtil.getFilePath(targetPath), targetRev, targetRev, -1);
                } catch (SVNException e) {
                    SVNErrorCode ec = e.getErrorMessage().getErrorCode();
                    if (ec == SVNErrorCode.FS_NOT_FOUND || ec == SVNErrorCode.RA_DAV_REQUEST_FAILED) {
                        continue;
                    }
                    throw e;
                }
                
                Map<String, SVNMergeRangeList> targetHistoryAsMergeInfo = SvnRepositoryAccess.getMergeInfoFromSegments(segments);
                Map<String, SVNMergeRangeList> explicitIntersection = SVNMergeInfoUtil.intersectMergeInfo(sourceMergeInfo, targetHistoryAsMergeInfo, true);
                if (explicitIntersection != null && !explicitIntersection.isEmpty()) {
                    result.neverSynced = false;
                    long[] endPoints = SVNMergeInfoUtil.getRangeEndPoints(explicitIntersection);
                    if (result.youngestMergedRevision < 0 || endPoints[0] > result.youngestMergedRevision) {
                        result.youngestMergedRevision = endPoints[0];
                    }
                }
                segments = sourceRepos.getLocationSegments(SVNFileUtil.getFilePath(sourcePathRelToSession), targetRev, targetRev, -1);
                Map<String, SVNMergeRangeList> sourceHistoryAsMergeInfo = SvnRepositoryAccess.getMergeInfoFromSegments(segments);
                sourceMergeInfo = SVNMergeInfoUtil.mergeMergeInfos(sourceMergeInfo, sourceHistoryAsMergeInfo);
                Map<String, SVNMergeRangeList> commonMergeInfo = SVNMergeInfoUtil.intersectMergeInfo(sourceMergeInfo, targetHistoryAsMergeInfo, true);
                Map<String, SVNMergeRangeList> filteredMergeInfo = SVNMergeInfoUtil.removeMergeInfo(commonMergeInfo, targetHistoryAsMergeInfo, true);
                if (!filteredMergeInfo.isEmpty()) {
                    newCatalog.put(SVNFileUtil.getFilePath(path), filteredMergeInfo);
                }
            }
        }
        if (result.youngestMergedRevision >= 0) {
            newCatalog = SVNMergeInfoUtil.filterCatalogByRanges(newCatalog, result.youngestMergedRevision, 0);
        }
        result.catalog = newCatalog;
        return result;
    }
    
    private static class UnmergedMergeInfo {
        private Map<String, Map<String, SVNMergeRangeList>> catalog;
        private boolean neverSynced;
        private long youngestMergedRevision; 
    }
}
