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

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

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.SVNMergeRange;
import org.tmatesoft.svn.core.SVNMergeRangeList;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.util.SVNURLUtil;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.internal.wc.SVNConflictVersion;
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.SVNWCContext.MergeInfo;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext.MergePropertiesInfo;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext.SVNWCNodeReposInfo;
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.ng.SvnNgMergeDriver.ObstructionState;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.ISVNConflictHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.SVNConflictAction;
import org.tmatesoft.svn.core.wc.SVNConflictChoice;
import org.tmatesoft.svn.core.wc.SVNConflictDescription;
import org.tmatesoft.svn.core.wc.SVNConflictReason;
import org.tmatesoft.svn.core.wc.SVNConflictResult;
import org.tmatesoft.svn.core.wc.SVNDiffOptions;
import org.tmatesoft.svn.core.wc.SVNOperation;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNTreeConflictDescription;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNLogType;

public class SvnNgMergeCallback implements ISvnDiffCallback {
    
    private Collection<File> conflictedPaths;
    private SvnNgMergeDriver driver;
    
    public SvnNgMergeCallback(SvnNgMergeDriver driver) {
        this.driver = driver;
    }

    public Collection<File> getConflictedPaths() {
        return conflictedPaths;
    }

    public void fileOpened(SvnDiffCallbackResult result, File path, long revision) throws SVNException {
        // do nothing
    }

    public void fileChanged(SvnDiffCallbackResult result, final File path,
            File tmpFile1, File tmpFile2, long rev1, long rev2,
            String mimetype1, String mimeType2, SVNProperties propChanges,
            SVNProperties originalProperties) throws SVNException {
        ObstructionState os = driver.performObstructionCheck(path, SVNNodeKind.UNKNOWN);
        if (os.obstructionState != SVNStatusType.INAPPLICABLE) {
            result.contentState = os.obstructionState;
            if (os.obstructionState == SVNStatusType.MISSING) {
                result.propState = SVNStatusType.MISSING;
            }
            return;
        }
        SVNNodeKind wcKind = os.kind;
        boolean isDeleted = os.deleted;
        
        if (wcKind != SVNNodeKind.FILE  || isDeleted) {
            if (wcKind == SVNNodeKind.NONE) {
                SVNDepth parentDepth = getContext().getNodeDepth(SVNFileUtil.getParentFile(path));
                if (parentDepth != SVNDepth.UNKNOWN && parentDepth.compareTo(SVNDepth.FILES) < 0) {
                    result.contentState = SVNStatusType.MISSING;
                    result.propState = SVNStatusType.MISSING;
                    return;
                }
            }

            treeConflict(path, SVNNodeKind.FILE, SVNConflictAction.EDIT, SVNConflictReason.MISSING);
            
            result.treeConflicted = true;
            result.contentState = SVNStatusType.MISSING;
            result.propState = SVNStatusType.MISSING;
            return;
        }
        
        if (!propChanges.isEmpty()) {
            MergePropertiesInfo mergeOutcome = mergePropChanges(path, propChanges, originalProperties);
            result.propState = mergeOutcome != null ? mergeOutcome.mergeOutcome : null;
            if (mergeOutcome != null && mergeOutcome.treeConflicted) {
                result.treeConflicted = true;
                return;
            }
        } else {
            result.propState = SVNStatusType.UNCHANGED;
        }
        
        if (isRecordOnly()) {
            result.contentState = SVNStatusType.UNCHANGED;
            return;
        }
        
        if (tmpFile1 != null) {
            boolean hasLocalMods = getContext().isTextModified(path, false);
            String targetLabel = ".working";
            String leftLabel = ".merge-left.r" + rev1;
            String rightLabel = ".merge-right.r" + rev2;
            SVNConflictVersion[] cvs = makeConflictVersions(path, SVNNodeKind.FILE);
            
            ISVNOptions opts = getContext().getOptions();
            final ISVNConflictHandler[] conflictHandler =  new ISVNConflictHandler[1];
            if (opts instanceof DefaultSVNOptions) {
                conflictHandler[0] = ((DefaultSVNOptions) opts).getConflictResolver();
                ((DefaultSVNOptions) opts).setConflictHandler(new ISVNConflictHandler() {
                    public SVNConflictResult handleConflict(SVNConflictDescription conflictDescription) throws SVNException {
                        SVNConflictResult result = conflictHandler[0] == null ? 
                                new SVNConflictResult(SVNConflictChoice.POSTPONE, null) : 
                                conflictHandler[0].handleConflict(conflictDescription);

                        if (result != null && result.getConflictChoice() == SVNConflictChoice.POSTPONE) {
                            if (conflictedPaths == null) {
                                conflictedPaths = new HashSet<File>();
                            }
                            conflictedPaths.add(path);
                        }
                        return result;
                    }
                });
            }
            try { 
                MergeInfo mergeOutcome = getContext().mergeText(tmpFile1, tmpFile2, path, leftLabel, rightLabel, targetLabel, cvs[0], cvs[1], isDryRun(), getDiffOptions(), propChanges);
                if (mergeOutcome.mergeOutcome == SVNStatusType.CONFLICTED) {
                    result.contentState = SVNStatusType.CONFLICTED;
                } else if (hasLocalMods && mergeOutcome.mergeOutcome != SVNStatusType.UNCHANGED) {
                    result.contentState = SVNStatusType.MERGED;
                } else if (mergeOutcome.mergeOutcome == SVNStatusType.MERGED) {
                    result.contentState = SVNStatusType.CHANGED;
                } else if (mergeOutcome.mergeOutcome == SVNStatusType.NO_MERGE) {
                    result.contentState = SVNStatusType.MISSING;
                } else {
                    result.contentState = SVNStatusType.UNCHANGED;
                }
            } finally {
                if (opts instanceof DefaultSVNOptions) {
                    ((DefaultSVNOptions) opts).setConflictHandler(conflictHandler[0]);
                }
            }
        }
    }

    public void fileAdded(SvnDiffCallbackResult result, File path,
            File leftFile, File rightFile, long rev1, long rev2,
            String mimeType1, String mimeType2, File copyFromPath,
            long copyFromRevision, SVNProperties propChanges,
            SVNProperties originalProperties) throws SVNException {
        if (isRecordOnly()) {
            result.contentState = SVNStatusType.UNCHANGED;
            result.propState = SVNStatusType.UNCHANGED;
            return;
        }
        result.propState = SVNStatusType.UNKNOWN;
        SVNProperties fileProps = new SVNProperties(originalProperties);
        for (String propName : propChanges.nameSet()) {
            if (SVNProperty.isWorkingCopyProperty(propName)) {
                continue;
            }
            if (!isSameRepos() && !SVNProperty.isRegularProperty(propName)) {
                continue;
            }
            if (!isSameRepos() && SVNProperty.MERGE_INFO.equals(propName)) {
                continue;
            }
            if (propChanges.getSVNPropertyValue(propName) != null) {
                fileProps.put(propName, propChanges.getSVNPropertyValue(propName));
            } else {
                fileProps.remove(propName);
            }
        }
        
        ObstructionState os = driver.performObstructionCheck(path, SVNNodeKind.UNKNOWN);
        if (os.obstructionState != SVNStatusType.INAPPLICABLE) {
            if (isDryRun() && getAddedPath() != null && SVNWCUtils.isChild(getAddedPath(), path)) {
                result.contentState = SVNStatusType.CHANGED;
                if (!fileProps.isEmpty()) {
                    result.propState = SVNStatusType.CHANGED;
                }
            } else {
                result.contentState = os.obstructionState;
            }
            return;
        }
        
        SVNNodeKind kind = SVNFileType.getNodeKind(SVNFileType.getType(path));
        
        if (kind == SVNNodeKind.NONE) {
            if (!isDryRun()) {
                SVNURL copyFromUrl = null;
                long copyFromRev = -1;
                InputStream newBaseContents = null;
                InputStream newContents = null;
                SVNProperties newBaseProps, newProps;
                try {
                    if (isSameRepos()) {
                        String child = SVNWCUtils.getPathAsChild(getTargetPath(), path);
                        if (child != null) {
                            copyFromUrl = getSource2URL().appendPath(child, false);
                        } else {
                            copyFromUrl = getSource2URL();
                        }
                        copyFromRev = rev2;
                        checkReposMatch(path, copyFromUrl);
                        newBaseContents = SVNFileUtil.openFileForReading(rightFile);
                        newContents = null;
                        newBaseProps = fileProps;
                        newProps = null;
                    } else {
                        newBaseProps = new SVNProperties();
                        newProps = fileProps;
                        newBaseContents = SVNFileUtil.DUMMY_IN;
                        newContents = SVNFileUtil.openFileForReading(rightFile);
                    }
                    SVNTreeConflictDescription tc = getContext().getTreeConflict(path);
                    if (tc != null) {
                        treeConflictOnAdd(path, SVNNodeKind.FILE, SVNConflictAction.ADD, SVNConflictReason.ADDED);
                        result.treeConflicted = true;
                    } else {
                        SvnNgReposToWcCopy.addFileToWc(getContext(), path, newBaseContents, newContents, newBaseProps, newProps, copyFromUrl, copyFromRev);
                    }
                } finally {
                    SVNFileUtil.closeFile(newBaseContents);
                    SVNFileUtil.closeFile(newContents);
                }
            }
            result.contentState = SVNStatusType.CHANGED;
            if (!fileProps.isEmpty()) {
                result.propState = SVNStatusType.CHANGED;
            }
        } else if (kind == SVNNodeKind.DIR) {
            treeConflictOnAdd(path, SVNNodeKind.FILE, SVNConflictAction.ADD, SVNConflictReason.OBSTRUCTED);
            result.treeConflicted = true;
            SVNNodeKind wcKind = getContext().readKind(path, false);
            if (wcKind != SVNNodeKind.NONE && driver.isDryRunDeletion(path)) {
                result.contentState = SVNStatusType.CHANGED;
            } else {
                result.contentState = SVNStatusType.OBSTRUCTED;
            }
        } else if (kind == SVNNodeKind.FILE) {
            if (driver.isDryRunDeletion(path)) {
                result.contentState = SVNStatusType.CHANGED;
            } else {
                treeConflictOnAdd(path, SVNNodeKind.FILE, SVNConflictAction.ADD, SVNConflictReason.ADDED);
                result.treeConflicted = true;
            }            
        } else {
            result.contentState = SVNStatusType.UNKNOWN;
        }
    }

    public void fileDeleted(SvnDiffCallbackResult result, File path,
            File leftFile, File rightFile, String mimeType1, String mimeType2,
            SVNProperties originalProperties) throws SVNException {
        
        if (isDryRun()) {
            if (getDryRunDeletions() == null) {
                setDryRunDeletions(new HashSet<File>());
            }
            getDryRunDeletions().add(path);
        }
        if (isRecordOnly()) {
            result.contentState = SVNStatusType.UNCHANGED;
            return;
        }
        ObstructionState os = driver.performObstructionCheck(path, SVNNodeKind.UNKNOWN);
        if (os.obstructionState != SVNStatusType.INAPPLICABLE) {
            result.contentState = os.obstructionState;
            return;
        }
        
        SVNNodeKind kind = SVNFileType.getNodeKind(SVNFileType.getType(path));
        if (kind == SVNNodeKind.FILE) {
            boolean same = compareFiles(leftFile, originalProperties, path);
            if (same || isForce() || isRecordOnly()) {
                if (!isDryRun()) {
                    SvnNgRemove.delete(getContext(), path, null, false, true, null);
                }
                result.contentState = SVNStatusType.CHANGED;
            } else {
                treeConflict(path, SVNNodeKind.FILE, SVNConflictAction.DELETE, SVNConflictReason.EDITED);
                result.treeConflicted = true;
                result.contentState = SVNStatusType.OBSTRUCTED;
            }
        } else if (kind == SVNNodeKind.DIR) {
            treeConflict(path, SVNNodeKind.FILE, SVNConflictAction.DELETE, SVNConflictReason.OBSTRUCTED);
            result.treeConflicted = true;
            result.contentState = SVNStatusType.OBSTRUCTED;
        } else if (kind == SVNNodeKind.NONE) {
            treeConflict(path, SVNNodeKind.FILE, SVNConflictAction.DELETE, SVNConflictReason.DELETED);
            result.treeConflicted = true;
            result.contentState = SVNStatusType.MISSING;
        } else {
            result.contentState = SVNStatusType.UNKNOWN;
        }
    }

    private void setDryRunDeletions(Collection<File> set) {
        driver.dryRunDeletions = set;
    }

    private void setDryRunAddtions(Collection<File> set) {
        driver.dryRunAdded = set;
    }

    public void dirDeleted(SvnDiffCallbackResult result, File path) throws SVNException {
        if (isRecordOnly()) {
            result.contentState = SVNStatusType.UNCHANGED;
            return;
        }
        ObstructionState os = driver.performObstructionCheck(path, SVNNodeKind.UNKNOWN);
        boolean isVersioned = os.kind == SVNNodeKind.DIR || os.kind == SVNNodeKind.FILE;
        if (os.obstructionState != SVNStatusType.INAPPLICABLE) {
            result.contentState = os.obstructionState;
            return;
        }
        if (os.deleted) {
            os.kind = SVNNodeKind.NONE;
        }
        if (isDryRun()) {
            if (getDryRunDeletions() == null) {
                setDryRunDeletions(new HashSet<File>());
            }
            getDryRunDeletions().add(path);
        }
        if (os.kind == SVNNodeKind.DIR) {
            if (isVersioned && !os.deleted) {
                try {
                    if (!isForce()) {
                        SvnNgRemove.checkCanDelete(driver.operation.getOperationFactory(), getContext(), path);
                    }
                    if (!isDryRun()) {
                        SvnNgRemove.delete(getContext(), path, null, false, false, null);
                    }
                    result.contentState = SVNStatusType.CHANGED;
                } catch (SVNException e) {
                    treeConflict(path, SVNNodeKind.DIR, SVNConflictAction.DELETE, SVNConflictReason.EDITED);
                    result.treeConflicted = true;
                    result.contentState = SVNStatusType.CONFLICTED;
                }
            } else {
                treeConflict(path, SVNNodeKind.DIR, SVNConflictAction.DELETE, SVNConflictReason.DELETED);
                result.treeConflicted = true;                
            }
        } else if (os.kind == SVNNodeKind.FILE) {
            result.contentState = SVNStatusType.OBSTRUCTED;
        } else if (os.kind == SVNNodeKind.NONE) {
            treeConflict(path, SVNNodeKind.DIR, SVNConflictAction.DELETE, SVNConflictReason.DELETED);
            result.treeConflicted = true;
            result.contentState = SVNStatusType.MISSING;
        } else {
            result.contentState = SVNStatusType.UNKNOWN;
        }
    }

    public void dirOpened(SvnDiffCallbackResult result, File path, long revision) throws SVNException {
        ObstructionState os = driver.performObstructionCheck(path, SVNNodeKind.UNKNOWN);
        if (os.obstructionState != SVNStatusType.INAPPLICABLE) {
            result.skipChildren = true;
            return;
        }
        if (os.kind != SVNNodeKind.DIR || os.deleted) {
            if (os.kind == SVNNodeKind.NONE) {
                SVNDepth parentDepth = getContext().getNodeDepth(SVNFileUtil.getParentFile(path));
                if (parentDepth != SVNDepth.UNKNOWN && parentDepth.compareTo(SVNDepth.IMMEDIATES) < 0) {
                    result.skipChildren = true;
                    return;
                }
            }
            if (os.kind == SVNNodeKind.FILE) {
                treeConflict(path, SVNNodeKind.DIR, SVNConflictAction.EDIT, SVNConflictReason.REPLACED);
                result.treeConflicted = true;
            } else if (os.deleted || os.kind == SVNNodeKind.NONE) {
                treeConflict(path, SVNNodeKind.DIR, SVNConflictAction.EDIT, SVNConflictReason.DELETED);
                result.treeConflicted = true;
            }
        }
    }

    public void dirAdded(SvnDiffCallbackResult result, File path, long revision, String copyFromPath, long copyFromRevision) throws SVNException {
        if (isRecordOnly()) {
            result.contentState = SVNStatusType.UNCHANGED;
            return;
        }
        File parentPath = SVNFileUtil.getParentFile(path);
        String child = SVNWCUtils.getPathAsChild(getTargetPath(), path);
        SVNURL copyFromUrl = null;
        long copyFromRev = -1;
        if (isSameRepos()) {
            copyFromUrl = getSource2URL().appendPath(child, false);
            copyFromRev = revision;
            checkReposMatch(parentPath, copyFromUrl);
        }
        ObstructionState os = driver.performObstructionCheck(path, SVNNodeKind.UNKNOWN);
        boolean isVersioned = os.kind == SVNNodeKind.DIR || os.kind == SVNNodeKind.FILE;
        if (os.obstructionState == SVNStatusType.OBSTRUCTED && (os.deleted || os.kind == SVNNodeKind.NONE)) {
            SVNFileType diskKind = SVNFileType.getType(path);
            if (diskKind == SVNFileType.DIRECTORY) {
                os.obstructionState = SVNStatusType.INAPPLICABLE;
                os.kind = SVNNodeKind.DIR;
            }
        }
        if (os.obstructionState != SVNStatusType.INAPPLICABLE) {
            if (isDryRun() && getAddedPath() != null && SVNWCUtils.isChild(getAddedPath(), path)) {
                result.contentState = SVNStatusType.CHANGED;
            } else {
                result.contentState = os.obstructionState;
            }
            return;
        }
        if (os.deleted) {
            os.kind = SVNNodeKind.NONE;
        }
        
        if (os.kind == SVNNodeKind.NONE) {
            if (isDryRun()) {
                if (getDryRunAdditions() == null) {
                    setDryRunAddtions(new HashSet<File>());
                }
                getDryRunAdditions().add(path);
                setAddedPath(path);
            } else {
                path.mkdir();
                if (copyFromUrl != null) {
                    SVNWCNodeReposInfo reposInfo = getContext().getNodeReposInfo(parentPath);
                    File reposRelPath = new File(SVNURLUtil.getRelativeURL(reposInfo.reposRootUrl, copyFromUrl, false));
                    getContext().getDb().opCopyDir(path, new SVNProperties(), 
                            copyFromRev, new SVNDate(0, 0), null, 
                            reposRelPath, 
                            reposInfo.reposRootUrl, 
                            reposInfo.reposUuid, 
                            copyFromRev, 
                            null, false,
                            SVNDepth.INFINITY, 
                            null, 
                            null);
                } else {
                    getContext().getDb().opAddDirectory(path, null, null);
                }
            }
            result.contentState = SVNStatusType.CHANGED;
        } else if (os.kind == SVNNodeKind.DIR) {
            if (!isVersioned || os.deleted) {
                if (!isDryRun()) {
                    if (copyFromUrl != null) {
                        SVNWCNodeReposInfo reposInfo = getContext().getNodeReposInfo(parentPath);
                        File reposRelPath = new File(SVNURLUtil.getRelativeURL(reposInfo.reposRootUrl, copyFromUrl, false));
                        getContext().getDb().opCopyDir(path, new SVNProperties(), 
                                copyFromRev, new SVNDate(0, 0), null, 
                                reposRelPath, 
                                reposInfo.reposRootUrl, 
                                reposInfo.reposUuid, 
                                copyFromRev, 
                                null, false,
                                SVNDepth.INFINITY, 
                                null, 
                                null);
                    } else {
                        getContext().getDb().opAddDirectory(path, null, null);
                    }
                } else {
                    setAddedPath(path);
                }
                result.contentState = SVNStatusType.CHANGED;
            } else {
                if (driver.isDryRunDeletion(path)) {
                    result.contentState = SVNStatusType.CHANGED;
                } else {
                    treeConflictOnAdd(path, SVNNodeKind.DIR, SVNConflictAction.ADD, SVNConflictReason.ADDED);
                    result.treeConflicted = true;
                    result.contentState = SVNStatusType.OBSTRUCTED;
                }
            }
        } else if (os.kind == SVNNodeKind.FILE) {
            if (isDryRun()) {
                setAddedPath(null);
            }
            if (isVersioned && driver.isDryRunDeletion(path)) {
                result.contentState = SVNStatusType.CHANGED;
            } else {
                treeConflictOnAdd(path, SVNNodeKind.DIR, SVNConflictAction.ADD, SVNConflictReason.OBSTRUCTED);
                result.treeConflicted = true;
                result.contentState = SVNStatusType.OBSTRUCTED;
            }
        } else {
            if (isDryRun()) {
                setAddedPath(null);
            }
            result.contentState = SVNStatusType.UNKNOWN;
        }
    }

    public void dirPropsChanged(SvnDiffCallbackResult result, File path, boolean isAdded, SVNProperties propChanges, SVNProperties originalProperties) throws SVNException {
        ObstructionState os = driver.performObstructionCheck(path, SVNNodeKind.DIR);
        if (os.obstructionState != SVNStatusType.INAPPLICABLE) {
            result.propState = os.obstructionState;
            return;
        }
        if (isAdded && isDryRun() && driver.isDryRunAddition(path)) {
            return;
        }
        MergePropertiesInfo info = mergePropChanges(path, propChanges, originalProperties);
        result.treeConflicted = info != null ? info.treeConflicted : false;
        result.propState = info != null ? info.mergeOutcome : null;
    }

    public void dirClosed(SvnDiffCallbackResult result, File path, boolean isAdded) throws SVNException {
        if (isDryRun() && getDryRunDeletions() != null) {
            getDryRunDeletions().clear();
        }
    }
    
    private void checkReposMatch(File path, SVNURL url) throws SVNException {
        if (!SVNURLUtil.isAncestor(getReposRootURL(), url)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Url ''{0}'' of ''{1}'' is not in repository ''{2}''", url, path, getReposRootURL());
            SVNErrorManager.error(err, SVNLogType.WC);
        }
    }
    
    private MergePropertiesInfo mergePropChanges(File localAbsPath, SVNProperties propChanges, SVNProperties originalProperties) throws SVNException {
        SVNProperties props = new SVNProperties();
        SvnNgPropertiesManager.categorizeProperties(propChanges, props, null, null);
        
        if (isRecordOnly() && !props.isEmpty()) {
            SVNProperties mergeinfoProps = new SVNProperties();
            if (props.containsName(SVNProperty.MERGE_INFO)) {
                mergeinfoProps.put(SVNProperty.MERGE_INFO, props.getStringValue(SVNProperty.MERGE_INFO));
            }
            props = mergeinfoProps;
        }
        
        MergePropertiesInfo mergeOutcome = null;
        if (!props.isEmpty()) {
            if (getSource1Rev() < getSource2Rev() || !areSourcesAncestral()) {
                props = filterSelfReferentialMergeInfo(props, localAbsPath, isHonorMergeInfo(), isSameRepos(), isReintegrateMerge(), getRepos2());
            }
            SVNException err = null;
            try {
                mergeOutcome = getContext().mergeProperties(localAbsPath, null, null, originalProperties, props, isDryRun(), getContext().getOptions().getConflictResolver());
            } catch (SVNException e) {
                err = e;
            }
            
            if (!isDryRun()) {
                for (String propName : props.nameSet()) {
                    if (!SVNProperty.MERGE_INFO.equals(propName)) {
                        continue;
                    }
                    SVNProperties pristineProps = getContext().getPristineProps(localAbsPath);
                    boolean hasPristineMergeInfo = false;
                    if (pristineProps != null && pristineProps.containsName(SVNProperty.MERGE_INFO)) {
                        hasPristineMergeInfo = true;
                    }
                    if (!hasPristineMergeInfo && props.getSVNPropertyValue(propName) != null) {
                        addPathWithAddedMergeInfo(localAbsPath);
                    } else if (hasPristineMergeInfo && props.getSVNPropertyValue(propName) == null) {
                        addPathWithDeletedMergeInfo(localAbsPath);
                    }
                }
            }
            
            if (err != null && (err.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND ||
                    err.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_UNEXPECTED_STATUS)) {
                if (mergeOutcome != null) {
                    mergeOutcome.mergeOutcome = SVNStatusType.MISSING;
                    mergeOutcome.treeConflicted = true;
                }
            } else if (err != null) {
                throw err;
            }
        }
        return mergeOutcome;
    }

    private void addPathWithAddedMergeInfo(File localAbsPath) {
        if (driver.pathsWithNewMergeInfo == null) {
            driver.pathsWithNewMergeInfo = new HashSet<File>();
        }
        driver.pathsWithNewMergeInfo.add(localAbsPath);
    }

    private void addPathWithDeletedMergeInfo(File localAbsPath) {
        if (driver.pathsWithDeletedMergeInfo == null) {
            driver.pathsWithDeletedMergeInfo = new HashSet<File>();
        }
        driver.pathsWithDeletedMergeInfo.add(localAbsPath);
    }

    private SVNProperties filterSelfReferentialMergeInfo(SVNProperties props, File localAbsPath, boolean honorMergeInfo, boolean sameRepos,
            boolean reintegrateMerge, SVNRepository repos) throws SVNException {
        if (!sameRepos) {
            return omitMergeInfoChanges(props);
        }
        if (!honorMergeInfo && !reintegrateMerge) {
            return props;
        }
        
        boolean isAdded = getContext().isNodeAdded(localAbsPath);
        if (isAdded) {
            return props;
        }
        long baseRevision = getContext().getNodeBaseRev(localAbsPath);        
        SVNProperties adjustedProps = new SVNProperties();
        
        for (String propName : props.nameSet()) {
            if (!SVNProperty.MERGE_INFO.equals(propName) || props.getSVNPropertyValue(propName) == null || "".equals(props.getSVNPropertyValue(propName))) {
                adjustedProps.put(propName, props.getSVNPropertyValue(propName));
                continue;
            }
            SVNURL targetUrl = getContext().getUrlFromPath(localAbsPath);
            SVNURL oldUrl = repos.getLocation();
            repos.setLocation(targetUrl, false);
            String mi = props.getStringValue(propName);
            
            Map<String, SVNMergeRangeList> mergeinfo = null;
            Map<String, SVNMergeRangeList> filteredYoungerMergeinfo = null;
            Map<String, SVNMergeRangeList> filteredMergeinfo = null;
            
            try {
                mergeinfo = SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(mi), null);
            } catch (SVNException e) {
                adjustedProps.put(propName, props.getSVNPropertyValue(propName));
                if (e.getErrorMessage().getErrorCode() == SVNErrorCode.MERGE_INFO_PARSE_ERROR) {
                    repos.setLocation(oldUrl, false);
                    continue;
                }
                throw e;
            }
            Map<String, SVNMergeRangeList>[] splitted = splitMergeInfoOnRevision(mergeinfo, baseRevision);
            Map<String, SVNMergeRangeList> youngerMergeInfo = splitted[0];
            mergeinfo = splitted[1];
            
            if (youngerMergeInfo != null) {
                SVNURL mergeSourceRootUrl = repos.getRepositoryRoot(true);
                
                for (Iterator<String> youngerMergeInfoIter = youngerMergeInfo.keySet().iterator(); youngerMergeInfoIter.hasNext();) {
                    String sourcePath = youngerMergeInfoIter.next();
                    SVNMergeRangeList rangeList = (SVNMergeRangeList) youngerMergeInfo.get(sourcePath);
                    SVNMergeRange ranges[] = rangeList.getRanges();
                    List<SVNMergeRange> adjustedRanges = new ArrayList<SVNMergeRange>();
                    
                    SVNURL mergeSourceURL = mergeSourceRootUrl.appendPath(sourcePath, false);
                    for (int i = 0; i < ranges.length; i++) {
                        SVNMergeRange range = ranges[i];
                        Structure<LocationsInfo> locations = null;
                        try {
                            locations = new SvnNgRepositoryAccess(null, getContext()).getLocations(
                                    repos, 
                                    SvnTarget.fromURL(targetUrl), 
                                    SVNRevision.create(baseRevision), 
                                    SVNRevision.create(range.getStartRevision() + 1), 
                                    SVNRevision.UNDEFINED);
                            SVNURL startURL = locations.get(LocationsInfo.startUrl);
                            if (!mergeSourceURL.equals(startURL)) {
                                adjustedRanges.add(range);
                            }
                            locations.release();
                        } catch (SVNException svne) {
                            SVNErrorCode code = svne.getErrorMessage().getErrorCode();
                            if (code == SVNErrorCode.CLIENT_UNRELATED_RESOURCES || 
                                    code == SVNErrorCode.RA_DAV_PATH_NOT_FOUND ||
                                    code == SVNErrorCode.FS_NOT_FOUND ||
                                    code == SVNErrorCode.FS_NO_SUCH_REVISION) {
                                adjustedRanges.add(range);
                            } else {
                                throw svne;
                            }
                        }
                    }

                    if (!adjustedRanges.isEmpty()) {
                        if (filteredYoungerMergeinfo == null) {
                            filteredYoungerMergeinfo = new TreeMap<String, SVNMergeRangeList>();
                        }
                        SVNMergeRangeList adjustedRangeList = SVNMergeRangeList.fromCollection(adjustedRanges); 
                        filteredYoungerMergeinfo.put(sourcePath, adjustedRangeList);
                    }
                }
            }
            if (mergeinfo != null && !mergeinfo.isEmpty()) {
                
                Map<String, SVNMergeRangeList> implicitMergeInfo = 
                        getRepositoryAccess().getHistoryAsMergeInfo(getRepos2(), SvnTarget.fromFile(localAbsPath),  
                                baseRevision, -1);                         
                filteredMergeinfo = SVNMergeInfoUtil.removeMergeInfo(implicitMergeInfo, mergeinfo, true);
            }
            
            if (oldUrl != null) {
                repos.setLocation(oldUrl, false);
            }
            
            if (filteredMergeinfo != null && filteredYoungerMergeinfo != null) {
                filteredMergeinfo = SVNMergeInfoUtil.mergeMergeInfos(filteredMergeinfo, filteredYoungerMergeinfo);
            } else if (filteredYoungerMergeinfo != null) {
                filteredMergeinfo = filteredYoungerMergeinfo;
            }

            if (filteredMergeinfo != null && !filteredMergeinfo.isEmpty()) {
                String filteredMergeInfoStr = SVNMergeInfoUtil.formatMergeInfoToString(filteredMergeinfo, null);
                adjustedProps.put(SVNProperty.MERGE_INFO, filteredMergeInfoStr);
            }
        }
        
        return adjustedProps;
    }

    private Map<String, SVNMergeRangeList>[] splitMergeInfoOnRevision(Map<String, SVNMergeRangeList> mergeinfo, long revision) {
        Map<String, SVNMergeRangeList> youngerMergeinfo = null;
        for (String path : new HashSet<String>(mergeinfo.keySet())) {
            SVNMergeRangeList rl = mergeinfo.get(path);
            for (int i = 0; i < rl.getSize(); i++) {
                SVNMergeRange r = rl.getRanges()[i];
                if (r.getEndRevision() <= revision) {
                    continue;
                } else {
                    SVNMergeRangeList youngerRl = new SVNMergeRangeList(new SVNMergeRange[0]);
                    for (int j = 0; j < rl.getSize(); j++) {
                        SVNMergeRange r2 = rl.getRanges()[j];
                        SVNMergeRange youngerRange = r2.dup();
                        if (i == j && r.getStartRevision() + 1 <= revision) {
                            youngerRange.setStartRevision(revision);
                            r.setEndRevision(revision);
                        }
                        youngerRl.pushRange(youngerRange.getStartRevision(), youngerRange.getEndRevision(), youngerRange.isInheritable());
                    }
                    
                    if (youngerMergeinfo == null) {
                        youngerMergeinfo = new TreeMap<String, SVNMergeRangeList>();
                    }
                    youngerMergeinfo.put(path, youngerRl);
                    mergeinfo = SVNMergeInfoUtil.removeMergeInfo(youngerMergeinfo, mergeinfo, true);
                    break;
                }
            }
        }
        @SuppressWarnings("unchecked")
        Map<String, SVNMergeRangeList>[] result = new Map[2]; 
        result[0] = youngerMergeinfo;
        result[1] = mergeinfo;
        return result;
    }

    protected static SVNProperties omitMergeInfoChanges(SVNProperties props) {
        SVNProperties result = new SVNProperties();
        for (String name : props.nameSet()) {
            if (SVNProperty.MERGE_INFO.equals(name)) {
                continue;
            }
            SVNPropertyValue pv = props.getSVNPropertyValue(name);
            result.put(name, pv);
        }
        return result;
    }

    private boolean isHonorMergeInfo() {
        return driver.isHonorMergeInfo();
    }

    private SVNConflictVersion[] makeConflictVersions(File target, SVNNodeKind kind) throws SVNException {
        SVNURL srcReposUrl = getRepos1().getRepositoryRoot(true);
        String child = SVNWCUtils.getPathAsChild(getTargetPath(), target);
        SVNURL leftUrl;
        SVNURL rightUrl;
        if (child != null) {
            leftUrl = getSource1URL().appendPath(child, false);
            rightUrl = getSource2URL().appendPath(child, false);
        } else {
            leftUrl = getSource1URL();
            rightUrl = getSource2URL();
        }
        String leftPath = SVNWCUtils.isChild(srcReposUrl, leftUrl);
        String rightPath = SVNWCUtils.isChild(srcReposUrl, rightUrl);
        SVNConflictVersion lv = new SVNConflictVersion(srcReposUrl, leftPath, getSource1Rev(), kind);
        SVNConflictVersion rv = new SVNConflictVersion(srcReposUrl, rightPath, getSource2Rev(), kind);
        
        return new SVNConflictVersion[] {lv, rv};
    }
    
    private void treeConflictOnAdd(File path, SVNNodeKind kind, SVNConflictAction action, SVNConflictReason reason) throws SVNException {
        if (isRecordOnly() || isDryRun()) {
            return;
        }
        SVNTreeConflictDescription tc = makeTreeConflict(path, kind, action, reason);
        SVNTreeConflictDescription existingTc = getContext().getTreeConflict(path);
        
        if (existingTc == null) {
            getContext().getDb().opSetTreeConflict(path, tc);
            if (conflictedPaths == null) {
                conflictedPaths = new HashSet<File>();
            }
            conflictedPaths.add(path);
        } else if (existingTc.getConflictAction() == SVNConflictAction.DELETE && tc.getConflictAction() == SVNConflictAction.ADD) {
            existingTc.setConflictAction(SVNConflictAction.REPLACE);
            getContext().getDb().opSetTreeConflict(path, existingTc);
        }
    }
    
    private SVNTreeConflictDescription makeTreeConflict(File path, SVNNodeKind kind, SVNConflictAction action, SVNConflictReason reason) throws SVNException {
        final SVNConflictVersion[] cvs = makeConflictVersions(path, kind);
        final SVNTreeConflictDescription tc = new SVNTreeConflictDescription(path, kind, action, reason, SVNOperation.MERGE, cvs[0], cvs[1]);
        return tc;
    }
    
    private void treeConflict(File path, SVNNodeKind kind, SVNConflictAction action, SVNConflictReason reason) throws SVNException {
        if (isRecordOnly() || isDryRun()) {
            return;
        }
        SVNTreeConflictDescription tc = getContext().getTreeConflict(path);
        if (tc == null) {
            tc = makeTreeConflict(path, kind, action, reason);
            getContext().getDb().opSetTreeConflict(path, tc);
            
            if (conflictedPaths == null) {
                conflictedPaths = new HashSet<File>();
            }
            conflictedPaths.add(path);
        }
    }

    private boolean compareProps(SVNProperties p1, SVNProperties p2) throws SVNException {
        if (p1 == null || p2 == null) {
            return p1 == p2;
        }
        SVNProperties diff = p1.compareTo(p2);
        for (String propName : diff.nameSet()) {
            if (SVNProperty.isRegularProperty(propName) && !SVNProperty.MERGE_INFO.equals(propName)) {
                return false;
            }
        }
        return true;
    }
    
    private boolean compareFiles(File oldPath, SVNProperties oldProps, File minePath) throws SVNException {
        SVNProperties workingProperties = getContext().getActualProps(minePath);
        boolean same = compareProps(oldProps, workingProperties);
        if (same) {
            InputStream is = null;
            InputStream old = null;
            try {
                if (workingProperties != null && workingProperties.getStringValue(SVNProperty.SPECIAL) != null) {
                    is = SVNFileUtil.readSymlink(minePath);
                } else {
                    is = getContext().getTranslatedStream(minePath, minePath, true, false);
                }
                old = SVNFileUtil.openFileForReading(oldPath);
                same = SVNFileUtil.compare(is, old);
            } finally {
                SVNFileUtil.closeFile(is);
                SVNFileUtil.closeFile(old);
            }
        }
        return same;
    }
    
    private SVNWCContext getContext() {
        return driver.context;
    }
    
    private boolean isReintegrateMerge() {
        return driver.reintegrateMerge;
    }
    
    private boolean isRecordOnly() {
        return driver.recordOnly;
    }
    
    private boolean isDryRun() {
        return driver.dryRun;
    }
    
    private boolean isForce() {
        return driver.forceDelete;
    }

    private boolean isSameRepos() {
        return driver.sameRepos;
    }
    
    private SVNDiffOptions getDiffOptions() {
        return driver.diffOptions;
    }
    
    private File getAddedPath() {
        return driver.addedPath;
    }
    
    private void setAddedPath(File path) {
        driver.addedPath = path;
    }
    
    private boolean areSourcesAncestral() {
        return driver.sourcesAncestral;
    }
    
    private File getTargetPath() {
        return driver.targetAbsPath;
    }
    
    private SVNRepository getRepos1() {
        return driver.repos1;
    }

    private SVNRepository getRepos2() {
        return driver.repos2;
    }
    
    private SVNURL getReposRootURL() {
        return driver.reposRootUrl;
    }
    
    private SvnRepositoryAccess getRepositoryAccess() {
        return driver.repositoryAccess;
    }
    
    private SVNURL getSource1URL() {
        return driver.mergeSource.url1;
    }
    private SVNURL getSource2URL() {
        return driver.mergeSource.url2;
    }
    private long getSource1Rev() {
        return driver.mergeSource.rev1;
    }
    private long getSource2Rev() {
        return driver.mergeSource.rev2;
    }
    
    private Collection<File> getDryRunDeletions() {
        return driver.dryRunDeletions;
    }
    private Collection<File> getDryRunAdditions() {
        return driver.dryRunAdded;
    }

}
