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

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.*;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslatorInputStream;
import org.tmatesoft.svn.core.internal.wc17.SVNAmbientDepthFilterEditor17;
import org.tmatesoft.svn.core.internal.wc17.SVNReporter17;
import org.tmatesoft.svn.core.internal.wc17.SVNStatusEditor17;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb;
import org.tmatesoft.svn.core.internal.wc17.db.Structure;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields;
import org.tmatesoft.svn.core.internal.wc2.SvnRepositoryAccess;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNCapability;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.ISVNDiffStatusHandler;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc2.ISvnObjectReceiver;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.core.wc2.SvnStatus;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class SvnNgDiffUtil {

    public static void doDiffSummarizeReposWC(SvnTarget target1, SVNRevision revision1, SVNRevision pegRevision, SvnTarget target2, SVNRevision revision2, boolean reverse, SvnNgRepositoryAccess repositoryAccess, SVNWCContext context, boolean useGitDiffFormat, SVNDepth depth, boolean useAncestry, Collection<String> changelists, boolean showCopiesAsAdds, ISvnDiffGenerator generator, ISVNDiffStatusHandler handler, ISVNCanceller canceller) throws SVNException {
        assert !target2.isURL();

        SVNURL url1 = repositoryAccess.getTargetURL(target1);
        String target = context.getActualTarget(target2.getFile());
        File anchor;
        if (target == null || target.length() == 0) {
            anchor = target2.getFile();
        } else {
            anchor = SVNFileUtil.getParentFile(target2.getFile());
        }
        SVNURL anchorUrl = context.getNodeUrl(anchor);

        if (anchorUrl == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "Directory ''{0}'' has no URL", anchor);
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }

//        final ISvnDiffGenerator generator = getDiffGenerator();

        if (pegRevision != SVNRevision.UNDEFINED) {
            url1 = repositoryAccess.getLocations(null, target1, pegRevision, revision1, SVNRevision.UNDEFINED).get(SvnRepositoryAccess.LocationsInfo.startUrl);

            if (generator != null) {
                if (!reverse) {
                    generator.setOriginalTargets(SvnTarget.fromURL(url1), SvnTarget.fromURL(anchorUrl.appendPath(target, false)));
                    generator.setAnchors(SvnTarget.fromURL(url1), SvnTarget.fromURL(anchorUrl.appendPath(target, false)));
                } else {
                    generator.setOriginalTargets(SvnTarget.fromURL(anchorUrl.appendPath(target, false)), SvnTarget.fromURL(url1));
                    generator.setAnchors(SvnTarget.fromURL(anchorUrl.appendPath(target, false)), SvnTarget.fromURL(url1));
                }
            }
        } else {
            if (generator != null) {
                if (!reverse) {
                    generator.setOriginalTargets(target1, target2);
                    generator.setAnchors(target1, target2);
                } else {
                    generator.setOriginalTargets(target2, target1);
                    generator.setAnchors(target2, target1);
                }
            }
        }

        ISvnDiffCallback callback = new SvnDiffSummarizeCallback(target1.getFile(), reverse, anchorUrl, anchor, handler);
        SVNRepository repository2 = repositoryAccess.createRepository(anchorUrl, null, true);

        if (useGitDiffFormat && generator != null) {
            File wcRoot = context.getDb().getWCRoot(anchor);
            generator.setRepositoryRoot(SvnTarget.fromFile(wcRoot));
        }

        boolean serverSupportsDepth = repository2.hasCapability(SVNCapability.DEPTH);

        long revisionNumber1 = repositoryAccess.getRevisionNumber(repository2, url1.equals(target1.getURL()) ? null : target1, revision1, null).lng(SvnRepositoryAccess.RevisionsPair.revNumber);

//        SvnDiffCallback callback = createDiffCallback(generator, reverse, revisionNumber1, -1);

        SVNReporter17 reporter = new SVNReporter17(target2.getFile(), context, false, !serverSupportsDepth, depth, false, false, true, false, SVNDebugLog.getDefaultLog());
        boolean revisionIsBase = isRevisionBase(revision2);
        SvnDiffEditor svnDiffEditor = new SvnDiffEditor(anchor, target, callback, depth, context, reverse, revisionIsBase, showCopiesAsAdds, !useAncestry, changelists, useGitDiffFormat, canceller);

        ISVNUpdateEditor updateEditor = svnDiffEditor;
        if (!serverSupportsDepth && depth == SVNDepth.UNKNOWN) {
            updateEditor = new SVNAmbientDepthFilterEditor17(updateEditor, context, anchor, target, revisionIsBase);
        }
        ISVNEditor editor = SVNCancellableEditor.newInstance(updateEditor, canceller, SVNDebugLog.getDefaultLog());
        try{
            repository2.diff(url1, revisionNumber1, revisionNumber1, target, !useAncestry, getDiffDepth(depth), true, reporter, editor);
        } finally {
            svnDiffEditor.cleanup();
        }
    }

    public static void doDiffWCWC(File localAbsPath, SvnNgRepositoryAccess repositoryAccess, SVNWCContext context, SVNDepth depth, boolean useAncestry, Collection<String> changelists, boolean showCopiesAsAdds, boolean useGitDiffFormat, ISvnDiffGenerator generator, ISvnDiffCallback callback, ISVNCanceller canceller) throws SVNException {
        assert SVNFileUtil.isAbsolute(localAbsPath);
        SvnDiffCallbackResult result = new SvnDiffCallbackResult();

        SVNNodeKind kind = context.getDb().readKind(localAbsPath, false, true, false);

        File anchorAbsPath;
        if (kind == SVNNodeKind.DIR) {
            anchorAbsPath = localAbsPath;
        } else {
            anchorAbsPath = SVNFileUtil.getParentFile(localAbsPath);
        }

        if (useGitDiffFormat) {
            showCopiesAsAdds = true;
        }
        if (showCopiesAsAdds) {
            useAncestry = true;
        }
        ISvnDiffCallback2 callback2 = new SvnDiffCallbackWrapper(callback, true, anchorAbsPath);
        if (!showCopiesAsAdds && !useGitDiffFormat) {
            callback2 = new SvnCopyAsChangedDiffCallback(callback2);
        }
        boolean getAll;
        if (showCopiesAsAdds || useGitDiffFormat || useAncestry) {
            getAll = true;
        } else {
            getAll = false;
        }

        DiffStatusCallback diffStatusCallback = new DiffStatusCallback(anchorAbsPath, !useAncestry, showCopiesAsAdds, changelists, callback2,  context);

        SVNStatusEditor17 statusEditor = new SVNStatusEditor17(localAbsPath, context, null, true, getAll, depth, diffStatusCallback);
        statusEditor.walkStatus(localAbsPath, depth, getAll, true, false, null);

        while (diffStatusCallback.currentNode != null) {
            DiffStatusCallback.NodeState ns = diffStatusCallback.currentNode;

            if (!ns.skip) {
                if (ns.propChanges != null) {
                    callback2.dirChanged(result, ns.relPath, ns.leftSource, ns.rightSource, ns.leftProps, ns.rightProps, ns.propChanges, null);
                } else {
                    callback2.dirClosed(result, ns.relPath, ns.leftSource, ns.rightSource, null);
                }
            }
            diffStatusCallback.currentNode = ns.parent;
        }
    }

    private static SVNDepth getDiffDepth(SVNDepth depth) {
        return depth != SVNDepth.INFINITY ? depth : SVNDepth.UNKNOWN;
    }

    private static boolean isRevisionBase(SVNRevision revision2) {
        return revision2 == SVNRevision.BASE;
    }

    private static class DiffStatusCallback implements ISvnObjectReceiver<SvnStatus> {

        private final File anchorAbsPath;
        private final boolean ignoreAncestry;
        private final boolean showCopiesAsAdds;
        private final Collection<String> changelists;
        private final ISvnDiffCallback2 callback;
        private final ISVNWCDb db;
        private final SVNWCContext context;

        private NodeState currentNode;

        private DiffStatusCallback(File anchorAbsPath, boolean ignoreAncestry, boolean showCopiesAsAdds, Collection<String> changelists, ISvnDiffCallback2 callback, SVNWCContext context) {
            this.anchorAbsPath = anchorAbsPath;
            this.ignoreAncestry = ignoreAncestry;
            this.showCopiesAsAdds = showCopiesAsAdds;
            this.changelists = changelists;
            this.callback = callback;
            this.db = context.getDb();
            this.context = context;
        }

        public void receive(SvnTarget target, SvnStatus status) throws SVNException {
            SvnDiffCallbackResult result = new SvnDiffCallbackResult();

            File localAbsPath = target.getFile();

            if (!status.isVersioned()) {
                return;
            }

            if (status.getNodeStatus() == SVNStatusType.STATUS_CONFLICTED &&
                    status.getTextStatus() == SVNStatusType.STATUS_NONE &&
                    status.getPropertiesStatus() == SVNStatusType.STATUS_NONE) {
                return;
            }
            if (status.getNodeStatus() == SVNStatusType.STATUS_NORMAL && !status.isCopied()) {
                return;
            }

            while (currentNode != null && !SVNPathUtil.isAncestor(SVNFileUtil.getFilePath(currentNode.localAbsPath), SVNFileUtil.getFilePath(localAbsPath))) {
                NodeState ns = currentNode;

                if (!ns.skip) {
                    if (ns.propChanges != null) {
                        callback.dirChanged(result, ns.relPath, ns.leftSource, ns.rightSource, ns.leftProps, ns.rightProps, ns.propChanges, null);
                    } else {
                        callback.dirClosed(result, ns.relPath, ns.leftSource, ns.rightSource, null);
                    }
                }

                currentNode = ns.parent;

            }
            ensureState(SVNFileUtil.getParentFile(localAbsPath), false);

            if (currentNode != null && currentNode.skipChildren) {
                return;
            }
            if (changelists != null && (status.getChangelist() == null || !changelists.contains(status.getChangelist()))) {
                return;
            }

            SVNNodeKind baseKind = SVNNodeKind.UNKNOWN;
            SVNNodeKind dbKind = status.getKind();
            SVNDepth depthBelowHere = SVNDepth.UNKNOWN;

            File childAbsPath = localAbsPath;
            File childRelPath = SVNFileUtil.skipAncestor(anchorAbsPath, localAbsPath);

            boolean reposOnly = false;
            boolean localOnly = false;

            Structure<StructureFields.NodeInfo> nodeInfoStructure = db.readInfo(localAbsPath, StructureFields.NodeInfo.status, StructureFields.NodeInfo.haveBase);
            ISVNWCDb.SVNWCDbStatus dbStatus = nodeInfoStructure.get(StructureFields.NodeInfo.status);
            boolean haveBase = nodeInfoStructure.is(StructureFields.NodeInfo.haveBase);

            ISVNWCDb.SVNWCDbStatus baseStatus;

            if (!haveBase) {
                localOnly = true;
            } else if (dbStatus == ISVNWCDb.SVNWCDbStatus.Normal) {
                baseKind = dbKind;
            } else if (dbStatus == ISVNWCDb.SVNWCDbStatus.Deleted) {
                reposOnly = true;
                ISVNWCDb.WCDbBaseInfo baseInfo = db.getBaseInfo(localAbsPath, ISVNWCDb.WCDbBaseInfo.BaseInfoField.status, ISVNWCDb.WCDbBaseInfo.BaseInfoField.kind);
                baseStatus = baseInfo.status;
                baseKind = baseInfo.kind == ISVNWCDb.SVNWCDbKind.Dir ? SVNNodeKind.DIR : SVNNodeKind.FILE;

                if (baseStatus != ISVNWCDb.SVNWCDbStatus.Normal) {
                    return;
                }
            } else {
                ISVNWCDb.WCDbBaseInfo baseInfo = db.getBaseInfo(localAbsPath, ISVNWCDb.WCDbBaseInfo.BaseInfoField.status, ISVNWCDb.WCDbBaseInfo.BaseInfoField.kind);
                baseStatus = baseInfo.status;
                baseKind = baseInfo.kind == ISVNWCDb.SVNWCDbKind.Dir ? SVNNodeKind.DIR : SVNNodeKind.FILE;

                if (baseStatus != ISVNWCDb.SVNWCDbStatus.Normal) {
                    localOnly = true;
                } else if (baseKind != dbKind || !ignoreAncestry) {
                    reposOnly = true;
                    localOnly = true;
                }
            }


            if (reposOnly) {
                if (baseKind == SVNNodeKind.FILE) {
                    diffBaseOnlyFile(childAbsPath, childRelPath, SVNRepository.INVALID_REVISION, this.db, this.callback);
                } else if (baseKind == SVNNodeKind.DIR) {
                    diffBaseOnlyDirectory(childAbsPath, childRelPath, SVNRepository.INVALID_REVISION, depthBelowHere, this.db, this.callback);
                }
            } else if (!localOnly) {
                if (dbKind == SVNNodeKind.FILE) {
                    diffBaseWorkingDiff(childAbsPath, childRelPath, SVNRepository.INVALID_REVISION, changelists, false, context, callback);
                } else if (dbKind == SVNNodeKind.DIR) {
                    ensureState(localAbsPath, false);
                    if (status.getPropertiesStatus() != SVNStatusType.STATUS_NONE && status.getPropertiesStatus() != SVNStatusType.STATUS_NORMAL) {
                        currentNode.leftProps = db.getBaseProps(localAbsPath);
                        currentNode.rightProps = db.readProperties(localAbsPath);
                        currentNode.propChanges = currentNode.leftProps.compareTo(currentNode.rightProps);
                    }
                }
            }

            if (localOnly) {
                if (dbKind == SVNNodeKind.FILE) {
                    diffLocalOnlyFile(childAbsPath, childRelPath, changelists, false, context, callback);
                } else if (dbKind == SVNNodeKind.DIR) {
                    diffLocalOnlyDirectory(childAbsPath, childRelPath, depthBelowHere, changelists, false, context, callback);
                }
            }

            if (dbKind == SVNNodeKind.DIR && (localOnly || reposOnly)) {
                ensureState(localAbsPath, true);
            }
        }

        private void ensureState(File localAbsPath, boolean recursiveSkip) throws SVNException {
            if (currentNode == null) {
                if (!SVNPathUtil.isAncestor(SVNFileUtil.getFilePath(anchorAbsPath), SVNFileUtil.getFilePath(localAbsPath))) {
                    return;
                }
                ensureState(SVNFileUtil.getParentFile(localAbsPath), false);
            } else if (SVNPathUtil.isAncestor(SVNFileUtil.getFilePath(currentNode.localAbsPath), SVNFileUtil.getFilePath(localAbsPath))) {
                ensureState(SVNFileUtil.getParentFile(localAbsPath), false);
            } else {
                return;
            }

            if (currentNode != null && currentNode.skipChildren) {
                return;
            }

            NodeState ns = new NodeState();
            ns.localAbsPath = localAbsPath;
            ns.relPath = SVNFileUtil.skipAncestor(anchorAbsPath, ns.localAbsPath);
            ns.parent = currentNode;
            currentNode = ns;

            if (recursiveSkip) {
                ns.skip = true;
                ns.skipChildren = true;
                return;
            }

            long revision;
            try {
                ISVNWCDb.WCDbBaseInfo baseInfo = db.getBaseInfo(localAbsPath, ISVNWCDb.WCDbBaseInfo.BaseInfoField.revision);
                revision = baseInfo.revision;
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                    throw e;
                }
                revision = 0;
            }

            ns.leftSource = new SvnDiffSource(revision);
            ns.rightSource = new SvnDiffSource(SVNRepository.INVALID_REVISION);

            SvnDiffCallbackResult result = new SvnDiffCallbackResult();
            callback.dirOpened(result, ns.relPath, ns.leftSource, ns.rightSource, null, null);
            ns.skip = result.skip;
            ns.skipChildren = result.skipChildren;
        }

        private static class NodeState {
            private NodeState parent;

            private File localAbsPath;
            private File relPath;

            private SvnDiffSource leftSource;
            private SvnDiffSource rightSource;
            private SvnDiffSource copySource;

            private boolean skip;
            private boolean skipChildren;

            private SVNProperties leftProps;
            private SVNProperties rightProps;
            private SVNProperties propChanges;
        }
    }

    protected static void diffBaseOnlyFile(File localAbsPath, File relPath, long revision, ISVNWCDb db, ISvnDiffCallback2 callback) throws SVNException {
        SvnDiffCallbackResult result = new SvnDiffCallbackResult();

        ISVNWCDb.WCDbBaseInfo baseInfo = db.getBaseInfo(localAbsPath, ISVNWCDb.WCDbBaseInfo.BaseInfoField.status, ISVNWCDb.WCDbBaseInfo.BaseInfoField.kind,
                ISVNWCDb.WCDbBaseInfo.BaseInfoField.revision, ISVNWCDb.WCDbBaseInfo.BaseInfoField.checksum, ISVNWCDb.WCDbBaseInfo.BaseInfoField.props);
        ISVNWCDb.SVNWCDbStatus status = baseInfo.status;
        ISVNWCDb.SVNWCDbKind kind = baseInfo.kind;
        if (!SVNRevision.isValidRevisionNumber(revision)) {
            revision = baseInfo.revision;
        }
        SvnChecksum checksum = baseInfo.checksum;
        SVNProperties props = baseInfo.props;

        assert status == ISVNWCDb.SVNWCDbStatus.Normal && kind == ISVNWCDb.SVNWCDbKind.File && checksum != null;

        SvnDiffSource leftSource = new SvnDiffSource(revision);

        result.reset();
        callback.fileOpened(result, relPath, leftSource, null, null, false, null);
        boolean skip = result.skip;

        if (skip) {
            return;
        }

        File pristineFile = db.getPristinePath(localAbsPath, checksum);

        result.reset();
        callback.fileDeleted(result, relPath, leftSource, pristineFile, props);
    }

    protected static void diffBaseOnlyDirectory(File localAbsPath, File relPath, long revision, SVNDepth depth, ISVNWCDb db, ISvnDiffCallback2 callback) throws SVNException {
        SvnDiffCallbackResult result = new SvnDiffCallbackResult();

        long reportRevision = revision;
        if (!SVNRevision.isValidRevisionNumber(reportRevision)) {
            ISVNWCDb.WCDbBaseInfo baseInfo = db.getBaseInfo(localAbsPath, ISVNWCDb.WCDbBaseInfo.BaseInfoField.revision);
            reportRevision = baseInfo.revision;
        }
        SvnDiffSource leftSource = new SvnDiffSource(reportRevision);

        result.reset();
        callback.dirOpened(result, relPath, leftSource, null, null, null);
        boolean skip = result.skip;
        boolean skipChildren = result.skipChildren;

        if (!skipChildren && (depth == SVNDepth.UNKNOWN || depth.compareTo(SVNDepth.EMPTY) > 0)) {
            Map<String, ISVNWCDb.WCDbBaseInfo> nodes = db.getBaseChildrenMap(localAbsPath, true);
            List<String> children = new ArrayList<String>(nodes.keySet());
            Collections.sort(children);

            for (String name : children) {
                ISVNWCDb.WCDbBaseInfo info = nodes.get(name);

                if (info.status != ISVNWCDb.SVNWCDbStatus.Normal) {
                    continue;
                }

                File childAbsPath = SVNFileUtil.createFilePath(localAbsPath, name);
                File childRelPath = SVNFileUtil.createFilePath(relPath, name);

                switch (info.kind) {
                    case File:
                    case Symlink:
                        SvnNgDiffUtil.diffBaseOnlyFile(childAbsPath, childRelPath, revision, db, callback);
                        break;
                    case Dir:
                        if (depth.compareTo(SVNDepth.FILES) > 0 || depth == SVNDepth.UNKNOWN) {
                            SVNDepth depthBelowHere = depth;
                            if (depthBelowHere == SVNDepth.IMMEDIATES) {
                                depthBelowHere = SVNDepth.EMPTY;
                            }
                            diffBaseOnlyDirectory(childAbsPath, childRelPath, revision, depthBelowHere, db, callback);
                        }
                        break;
                    default:
                        break;
                }
            }
        }

        if (!skip) {
            SVNProperties props = db.getBaseProps(localAbsPath);

            result.reset();
            callback.dirDeleted(result, relPath, leftSource, props, null);
        }
    }

    protected static void diffLocalOnlyFile(File localAbsPath, File relPath, Collection<String> changelists, boolean diffPristine, SVNWCContext context, ISvnDiffCallback2 callback) throws SVNException {
        SvnDiffCallbackResult result = new SvnDiffCallbackResult();
        ISVNWCDb db = context.getDb();

        Structure<StructureFields.NodeInfo> nodeInfoStructure = db.readInfo(localAbsPath, StructureFields.NodeInfo.status, StructureFields.NodeInfo.kind, StructureFields.NodeInfo.revision,
                StructureFields.NodeInfo.checksum,
                StructureFields.NodeInfo.originalReposRelpath, StructureFields.NodeInfo.originalRevision,
                StructureFields.NodeInfo.changelist,
                StructureFields.NodeInfo.hadProps, StructureFields.NodeInfo.propsMod);
        ISVNWCDb.SVNWCDbStatus status = nodeInfoStructure.get(StructureFields.NodeInfo.status);
        ISVNWCDb.SVNWCDbKind kind = nodeInfoStructure.get(StructureFields.NodeInfo.kind);
        long revision = nodeInfoStructure.lng(StructureFields.NodeInfo.revision);
        SvnChecksum checksum = nodeInfoStructure.get(StructureFields.NodeInfo.checksum);
        File originalReposRelPath = nodeInfoStructure.get(StructureFields.NodeInfo.originalReposRelpath);
        long originalRevision = nodeInfoStructure.lng(StructureFields.NodeInfo.originalRevision);
        String changelist = nodeInfoStructure.get(StructureFields.NodeInfo.changelist);
        boolean hadProps = nodeInfoStructure.is(StructureFields.NodeInfo.hadProps);
        boolean propMods = nodeInfoStructure.is(StructureFields.NodeInfo.propsMod);

        assert kind == ISVNWCDb.SVNWCDbKind.File && (status == ISVNWCDb.SVNWCDbStatus.Normal || status == ISVNWCDb.SVNWCDbStatus.Added || (status == ISVNWCDb.SVNWCDbStatus.Deleted && diffPristine));

        if (changelist != null && changelists != null && !changelists.contains(changelist)) {
            return;
        }

        SVNProperties pristineProps;
        if (status == ISVNWCDb.SVNWCDbStatus.Deleted) {
            assert diffPristine;

            Structure<StructureFields.PristineInfo> pristineInfoStructure = db.readPristineInfo(localAbsPath);
            status = pristineInfoStructure.get(StructureFields.PristineInfo.status);
            kind = pristineInfoStructure.get(StructureFields.PristineInfo.kind);
            checksum = pristineInfoStructure.get(StructureFields.PristineInfo.checksum);
            hadProps = pristineInfoStructure.is(StructureFields.PristineInfo.hadProps);
            pristineProps = pristineInfoStructure.get(StructureFields.PristineInfo.props);
            propMods = false;
        } else if (!hadProps) {
            pristineProps = new SVNProperties();
        } else {
            pristineProps = db.readPristineProperties(localAbsPath);
        }

        SvnDiffSource copyFromSource = null;
        if (originalReposRelPath != null) {
            copyFromSource = new SvnDiffSource(originalRevision);
            copyFromSource.setReposRelPath(originalReposRelPath);
        }

        boolean fileMod = true;
        SvnDiffSource rightSource;
        if (propMods || !SVNRevision.isValidRevisionNumber(revision)) {
            rightSource = new SvnDiffSource(SVNRepository.INVALID_REVISION);
        } else {
            if (diffPristine) {
                fileMod = false;
            } else {
                fileMod = context.isTextModified(localAbsPath, false);
            }

            if (!fileMod) {
                rightSource = new SvnDiffSource(revision);
            } else {
                rightSource = new SvnDiffSource(SVNRepository.INVALID_REVISION);
            }
        }

        result.reset();
        callback.fileOpened(result, relPath, null, rightSource, copyFromSource, false, null);
        boolean skip = result.skip;

        if (skip) {
            return;
        }

        SVNProperties rightProps;
        if (propMods && !diffPristine) {
            rightProps = db.readProperties(localAbsPath);
        } else {
            rightProps = new SVNProperties(pristineProps);
        }

        File pristineFile;
        if (checksum != null) {
            pristineFile = db.getPristinePath(localAbsPath, checksum);
        } else {
            pristineFile = null;
        }

        File translatedFile;
        if (diffPristine) {
            translatedFile = pristineFile;
        } else {
            translatedFile = context.getTranslatedFile(localAbsPath, localAbsPath, true, false, true, false, false);
        }

        result.reset();
        callback.fileAdded(result, relPath, copyFromSource, rightSource,
                copyFromSource != null ? pristineFile : null,
                translatedFile,
                copyFromSource != null ? pristineProps : null,
                rightProps);
    }

    protected static void diffLocalOnlyDirectory(File localAbsPath, File relPath, SVNDepth depth, Collection<String> changelists, boolean diffPristine, SVNWCContext context, ISvnDiffCallback2 callback) throws SVNException {
        SvnDiffCallbackResult result = new SvnDiffCallbackResult();
        ISVNWCDb db = context.getDb();

        boolean skip = false;
        boolean skipChildren = false;
        SvnDiffSource rightSource = new SvnDiffSource(SVNRepository.INVALID_REVISION);
        SVNDepth depthBelowHere = depth;

        result.reset();
        callback.dirOpened(result, relPath, null, rightSource, null, null);
        skip = result.skip;
        skipChildren = result.skipChildren;

        Map<String, ISVNWCDb.SVNWCDbInfo> nodes = new HashMap<String, ISVNWCDb.SVNWCDbInfo>();
        db.readChildren(localAbsPath, nodes, new HashSet<String>());

        if (depthBelowHere == SVNDepth.IMMEDIATES) {
            depthBelowHere = SVNDepth.EMPTY;
        }

        List<String> children = new ArrayList<String>(nodes.keySet());
        Collections.sort(children);

        for (String name : children) {
            File childAbsPath = SVNFileUtil.createFilePath(localAbsPath, name);
            ISVNWCDb.SVNWCDbInfo info = nodes.get(name);

            if (info.status.isNotPresent()) {
                continue;
            }

            if (!diffPristine && info.status == ISVNWCDb.SVNWCDbStatus.Deleted) {
                continue;
            }

            File childRelPath = SVNFileUtil.createFilePath(relPath, name);

            switch (info.kind) {
                case File:
                case Symlink:
                    diffLocalOnlyFile(childAbsPath, childRelPath, changelists, diffPristine, context, callback);
                    break;
                case Dir:
                    if (depth.compareTo(SVNDepth.FILES) > 0 || depth == SVNDepth.UNKNOWN) {
                        diffLocalOnlyDirectory(childAbsPath, childRelPath, depthBelowHere, changelists, diffPristine, context, callback);
                    }
                    break;
                default:
                    break;
            }
        }

        if (!skip) {
            SVNProperties rightProps;
            if (diffPristine) {
                rightProps = db.readPristineProperties(localAbsPath);
            } else {
                rightProps = db.readProperties(localAbsPath);
            }

            result.reset();
            callback.dirAdded(result, relPath, null, rightSource, null, rightProps, null);
        }
    }

    protected static void diffBaseWorkingDiff(File localAbsPath, File relPath, long revision, Collection<String> changeists, boolean diffPristine, SVNWCContext context, ISvnDiffCallback2 callback) throws SVNException {
        SvnDiffCallbackResult result = new SvnDiffCallbackResult();
        ISVNWCDb db = context.getDb();

        Structure<StructureFields.NodeInfo> nodeInfoStructure = db.readInfo(localAbsPath, StructureFields.NodeInfo.status, StructureFields.NodeInfo.revision,
                StructureFields.NodeInfo.checksum, StructureFields.NodeInfo.recordedSize, StructureFields.NodeInfo.recordedTime,
                StructureFields.NodeInfo.changelist, StructureFields.NodeInfo.hadProps, StructureFields.NodeInfo.propsMod);
        ISVNWCDb.SVNWCDbStatus status = nodeInfoStructure.get(StructureFields.NodeInfo.status);
        long dbRevision = nodeInfoStructure.lng(StructureFields.NodeInfo.revision);
        SvnChecksum workingChecksum = nodeInfoStructure.get(StructureFields.NodeInfo.checksum);
        long recordedSize = nodeInfoStructure.lng(StructureFields.NodeInfo.recordedSize);
        long recordedTime = nodeInfoStructure.lng(StructureFields.NodeInfo.recordedTime);
        String changelist = nodeInfoStructure.get(StructureFields.NodeInfo.changelist);
        boolean hadProps = nodeInfoStructure.is(StructureFields.NodeInfo.hadProps);
        boolean propsMod = nodeInfoStructure.is(StructureFields.NodeInfo.propsMod);

        SvnChecksum checksum = workingChecksum;

        assert status == ISVNWCDb.SVNWCDbStatus.Normal || status == ISVNWCDb.SVNWCDbStatus.Added || (status == ISVNWCDb.SVNWCDbStatus.Deleted && diffPristine);

        if (changeists != null && !changeists.contains(changelist)) {
            return;
        }

        boolean filesSame = false;

        if (status != ISVNWCDb.SVNWCDbStatus.Normal) {
            ISVNWCDb.WCDbBaseInfo baseInfo = db.getBaseInfo(localAbsPath, ISVNWCDb.WCDbBaseInfo.BaseInfoField.status, ISVNWCDb.WCDbBaseInfo.BaseInfoField.revision, ISVNWCDb.WCDbBaseInfo.BaseInfoField.checksum, ISVNWCDb.WCDbBaseInfo.BaseInfoField.hadProps);
            ISVNWCDb.SVNWCDbStatus baseStatus = baseInfo.status;
            dbRevision = baseInfo.revision;
            checksum = baseInfo.checksum;
            hadProps = baseInfo.hadProps;
            recordedSize = -1;
            recordedTime = 0;
            propsMod = true;
        } else if (diffPristine) {
            filesSame = true;
        } else {
            SVNNodeKind kind = SVNFileType.getNodeKind(SVNFileType.getType(localAbsPath));

            if (kind != SVNNodeKind.FILE || (kind == SVNNodeKind.FILE && SVNFileUtil.getFileLength(localAbsPath) == recordedSize && SVNFileUtil.getFileLastModified(localAbsPath) == recordedTime)) {
                filesSame = true;
            }
        }
        if (filesSame && !propsMod) {
            return;
        }
        assert checksum != null;

        if (!SVNRevision.isValidRevisionNumber(revision)) {
            revision = dbRevision;
        }
        SvnDiffSource leftSource = new SvnDiffSource(revision);
        SvnDiffSource rightSource = new SvnDiffSource(SVNRepository.INVALID_REVISION);

        result.reset();
        callback.fileOpened(result, relPath, leftSource, rightSource, null, false, null);
        boolean skip = result.skip;

        if (skip) {
            return;
        }
        File pristineFile = db.getPristinePath(localAbsPath, checksum);

        File localFile;
        if (diffPristine) {
            localFile = db.getPristinePath(localAbsPath, workingChecksum);
        } else if (! (hadProps || propsMod)) {
            localFile = localAbsPath;
        } else if (filesSame) {
            localFile = pristineFile;
        } else {
            localFile = context.getTranslatedFile(localAbsPath, localAbsPath, true, false, true, false, false);
        }

        if (!filesSame) {
            filesSame = SVNFileUtil.compareFiles(localFile, pristineFile, null);
        }

        SVNProperties baseProps;
        if (hadProps) {
            baseProps = db.getBaseProps(localAbsPath);
        } else {
            baseProps = new SVNProperties();
        }

        SVNProperties localProps;
        if (status == ISVNWCDb.SVNWCDbStatus.Normal && (diffPristine || !propsMod)) {
            localProps = baseProps;
        } else if (diffPristine) {
            localProps = db.readPristineProperties(localAbsPath);
        } else {
            localProps = db.readProperties(localAbsPath);
        }

        SVNProperties propChanges = baseProps.compareTo(localProps);

        if (propChanges.size() > 0 || !filesSame) {
            result.reset();
            callback.fileChanged(result, relPath, leftSource, rightSource, pristineFile, localFile, baseProps, localProps, !filesSame, propChanges);
        } else {
            result.reset();
            callback.fileClosed(result, relPath, leftSource, rightSource);
        }
    }

    public static void doArbitraryNodesDiff(SvnTarget target1, SvnTarget target2, SVNDepth depth, SVNWCContext context, ISvnDiffCallback callback, ISVNCanceller canceller) throws SVNException {
        File path1 = target1.getFile();
        File path2 = target2.getFile();
        SVNNodeKind kind1 = SVNFileType.getNodeKind(SVNFileType.getType(path1));
        SVNNodeKind kind2 = SVNFileType.getNodeKind(SVNFileType.getType(path2));

        if (kind1 != kind2) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.NODE_UNEXPECTED_KIND, "''{0}'' is not the same node kind as ''{1}''", path1, path2);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }
        if (depth == SVNDepth.UNKNOWN) {
            depth = SVNDepth.INFINITY;
        }
        if (kind1 == SVNNodeKind.FILE) {
            doArbitraryFilesDiff(path1, path2, SVNFileUtil.createFilePath(SVNFileUtil.getFileName(path1)), false, false, null, context, callback, canceller);
        } else if (kind1 == SVNNodeKind.DIR) {
            doArbitraryDirsDiff(path1, path2, null, null, depth, context, callback, canceller);
        } else {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.NODE_UNEXPECTED_KIND, "''{0}'' is not a file or directory", kind1 == SVNNodeKind.NONE ? path1 : path2);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }
    }


    private static void doArbitraryFilesDiff(File localAbsPath1, File localAbsPath2, File path, boolean file1IsEmpty, boolean file2IsEmpty, SVNProperties originalPropertiesOverride, SVNWCContext context, ISvnDiffCallback callback, ISVNCanceller canceller) throws SVNException {
        if (canceller != null) {
            canceller.checkCancelled();
        }

        SvnDiffCallbackResult result = new SvnDiffCallbackResult();

        SVNProperties originalProps;
        if (originalPropertiesOverride != null) {
            originalProps = originalPropertiesOverride;
        } else {
            if (localAbsPath1 != null) {
                try {
                    originalProps = context.getActualProps(localAbsPath1);
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND && e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_WORKING_COPY) {
                        throw e;
                    }
                    originalProps = new SVNProperties();
                }
            } else {
                originalProps = new SVNProperties();
            }
        }

        SVNProperties modifiedProps;
        if (localAbsPath2 != null) {
            try {
                modifiedProps = context.getActualProps(localAbsPath2);
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND && e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_WORKING_COPY) {
                    throw e;
                }
                modifiedProps = new SVNProperties();
            }
        } else {
            modifiedProps = new SVNProperties();
        }

        SVNProperties propChanges = originalProps.compareTo(modifiedProps);

        String originalMimeType = originalProps.getStringValue(SVNProperty.MIME_TYPE);
        if (!file1IsEmpty && originalMimeType == null) {
            String mimeType = SVNFileUtil.detectMimeType(localAbsPath1, context.getOptions().getFileExtensionsToMimeTypes());

            if (mimeType != null) {
                originalMimeType = mimeType;
            }
        }

        String modifiedMimeType = modifiedProps.getStringValue(SVNProperty.MIME_TYPE);
        if (!file2IsEmpty && modifiedMimeType == null) {
            String mimeType = SVNFileUtil.detectMimeType(localAbsPath2, context.getOptions().getFileExtensionsToMimeTypes());

            if (mimeType != null) {
                modifiedMimeType = mimeType;
            }
        }
        if (file1IsEmpty && !file2IsEmpty) {
            callback.fileAdded(result, path, localAbsPath1, localAbsPath2, SVNRepository.INVALID_REVISION, SVNRepository.INVALID_REVISION,
                    originalMimeType, modifiedMimeType, null, SVNRepository.INVALID_REVISION, propChanges, originalProps);
        } else if (!file1IsEmpty && file2IsEmpty) {
            callback.fileDeleted(result, path, localAbsPath1, localAbsPath2, originalMimeType, modifiedMimeType, originalProps);
        } else {
            InputStream inputStream1 = SVNFileUtil.openFileForReading(localAbsPath1);
            InputStream inputStream2 = SVNFileUtil.openFileForReading(localAbsPath2);

            try {
                if (originalProps != null) {
                    byte[] eol = SVNTranslator.getEOL(originalProps.getStringValue(SVNProperty.EOL_STYLE), context.getOptions());
                    if (eol != null) {
                        inputStream1 = new SVNTranslatorInputStream(inputStream1, eol, true, null, false);
                    }
                }
                if (modifiedProps != null) {
                    byte[] eol = SVNTranslator.getEOL(modifiedProps.getStringValue(SVNProperty.EOL_STYLE), context.getOptions());
                    if (eol != null) {
                        inputStream2 = new SVNTranslatorInputStream(inputStream2, eol, true, null, false);
                    }
                }
                boolean same = SVNFileUtil.compare(inputStream1, inputStream2);
                if (!same || propChanges.size() > 0) {
                    callback.fileChanged(result, path, same ? null : localAbsPath1, same ? null : localAbsPath2, SVNRepository.INVALID_REVISION, SVNRepository.INVALID_REVISION, originalMimeType, modifiedMimeType, propChanges, originalProps);
                }
            } finally {
                SVNFileUtil.closeFile(inputStream1);
                SVNFileUtil.closeFile(inputStream2);
            }
        }
    }

    private static void doArbitraryDirsDiff(File localAbsPath1, File localAbsPath2, File rootAbsPath1, File rootAbsPath2, SVNDepth depth, SVNWCContext context, ISvnDiffCallback callback, ISVNCanceller canceller) throws SVNException {
        SVNNodeKind kind1 = null;
        try {
            kind1 = SVNFileType.getNodeKind(SVNFileType.getType(localAbsPath1.getCanonicalFile()));
        } catch (IOException e) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }

        ArbitraryDiffWalker diffWalker = new ArbitraryDiffWalker();
        diffWalker.recursingWithinAddedSubtree = (kind1 != SVNNodeKind.DIR);
        diffWalker.root1AbsPath = rootAbsPath1 != null ? rootAbsPath1 : localAbsPath1;
        diffWalker.root2AbsPath = rootAbsPath2 != null ? rootAbsPath2 : localAbsPath2;
        diffWalker.recursingWithinAdmDir = false;

        if (depth.compareTo(SVNDepth.IMMEDIATES) <= 0) {
            arbitraryDiffThisDir(diffWalker, localAbsPath1, depth, context, callback, canceller);
        } else if (depth == SVNDepth.INFINITY) {
            walkDirectory(diffWalker.recursingWithinAddedSubtree ? localAbsPath2 : localAbsPath1, diffWalker, context, callback, canceller);
        }
    }

    private static void arbitraryDiffThisDir(ArbitraryDiffWalker diffWalker, File localAbsPath, SVNDepth depth, SVNWCContext context, ISvnDiffCallback callback, ISVNCanceller canceller) throws SVNException {
        SvnDiffCallbackResult result = new SvnDiffCallbackResult();

        if (diffWalker.recursingWithinAdmDir) {
            if (SVNFileUtil.skipAncestor(diffWalker.admDirAbsPath, localAbsPath) != null) {
                return;
            } else {
                diffWalker.recursingWithinAdmDir = false;
                diffWalker.admDirAbsPath = null;
            }
        } else if (SVNFileUtil.getFileName(localAbsPath).equals(SVNFileUtil.getAdminDirectoryName())) {
            diffWalker.recursingWithinAdmDir = true;
            diffWalker.admDirAbsPath = localAbsPath;
            return;
        }

        File childRelPath;
        if (diffWalker.recursingWithinAddedSubtree) {
            childRelPath = SVNFileUtil.skipAncestor(diffWalker.root2AbsPath, localAbsPath);
        } else {
            childRelPath = SVNFileUtil.skipAncestor(diffWalker.root1AbsPath, localAbsPath);
        }
        if (childRelPath == null) {
            return;
        }
        File localAbsPath1 = SVNFileUtil.createFilePath(diffWalker.root1AbsPath, childRelPath);
        SVNNodeKind kind1 = null;
        try {
            kind1 = SVNFileType.getNodeKind(SVNFileType.getType(localAbsPath1.getCanonicalFile()));
        } catch (IOException e) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }
        File localAbsPath2 = SVNFileUtil.createFilePath(diffWalker.root2AbsPath, childRelPath);
        SVNNodeKind kind2 = null;
        try {
            kind2 = SVNFileType.getNodeKind(SVNFileType.getType(localAbsPath2.getCanonicalFile()));
        } catch (IOException e) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }

        File[] children1 = null;
        if (depth.compareTo(SVNDepth.EMPTY) > 0) {
            if (kind1 == SVNNodeKind.DIR) {
                children1 = SVNFileListUtil.listFiles(localAbsPath1);
            } else {
                children1 = new File[0];
            }
        }
        File[] children2 = null;
        if (kind2 == SVNNodeKind.DIR) {
            SVNProperties originalProps;
            try {
                originalProps = context.getActualProps(localAbsPath1);
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND && e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_WORKING_COPY) {
                    throw e;
                }
                originalProps = new SVNProperties();
            }
            SVNProperties modifiedProps;
            try {
                modifiedProps = context.getActualProps(localAbsPath2);
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND && e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_WORKING_COPY) {
                    throw e;
                }
                modifiedProps = new SVNProperties();
            }
            SVNProperties propChanges = originalProps.compareTo(modifiedProps);
            if (propChanges.size() > 0) {
                callback.dirPropsChanged(result, childRelPath, false, propChanges, originalProps);
            }
            if (depth.compareTo(SVNDepth.EMPTY) > 0) {
                children2 = SVNFileListUtil.listFiles(localAbsPath2);
            }
        } else if (depth.compareTo(SVNDepth.EMPTY) > 0) {
            children2 = new File[0];
        }

        if (depth.compareTo(SVNDepth.EMPTY) <= 0) {
            return;
        }

        Set<String> mergedChildren = new HashSet<String>();
        if (children1 != null) {
            for (File child1 : children1) {
                mergedChildren.add(SVNFileUtil.getFileName(child1));
            }
        }
        if (children2 != null) {
            for (File child2 : children2) {
                mergedChildren.add(SVNFileUtil.getFileName(child2));
            }
        }
        List<String> sortedChildren = new ArrayList<String>(mergedChildren);
        Collections.sort(sortedChildren);
        for (String name : sortedChildren) {
            if (canceller != null) {
                canceller.checkCancelled();
            }

            if (name.equals(SVNFileUtil.getAdminDirectoryName())) {
                continue;
            }
            File childAbsPath1 = SVNFileUtil.createFilePath(localAbsPath1, name);
            File childAbsPath2 = SVNFileUtil.createFilePath(localAbsPath2, name);

            SVNNodeKind childKind1 = null;
            SVNNodeKind childKind2 = null;
            try {
                childKind1 = SVNFileType.getNodeKind(SVNFileType.getType(childAbsPath1.getCanonicalFile()));
                childKind2 = SVNFileType.getNodeKind(SVNFileType.getType(childAbsPath2.getCanonicalFile()));
            } catch (IOException e) {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e);
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }
            if (childKind1 == SVNNodeKind.DIR && childKind2 == SVNNodeKind.DIR) {
                if (depth == SVNDepth.IMMEDIATES) {
                    doArbitraryDirsDiff(childAbsPath1, childAbsPath2, diffWalker.root1AbsPath, diffWalker.root2AbsPath, SVNDepth.EMPTY, context, callback, canceller);
                } else {
                    continue;
                }
            }
            if (childKind1 == SVNNodeKind.FILE && (childKind2 == SVNNodeKind.DIR || childKind2 == SVNNodeKind.NONE)) {
                doArbitraryFilesDiff(childAbsPath1, null, SVNFileUtil.createFilePath(childRelPath, name), false, true, null, context, callback, canceller);
            }
            if (childKind2 == SVNNodeKind.FILE && (childKind1 == SVNNodeKind.DIR || childKind1 == SVNNodeKind.NONE)) {
                SVNProperties originalProps;
                try {
                    originalProps = context.getActualProps(childAbsPath1);
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND && e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_WORKING_COPY) {
                        throw e;
                    }
                    originalProps = new SVNProperties();
                }
                doArbitraryFilesDiff(null, childAbsPath2, SVNFileUtil.createFilePath(childRelPath, name), true, false, originalProps, context, callback, canceller);
            }
            if (childKind1 == SVNNodeKind.FILE && childKind2 == SVNNodeKind.FILE) {
                doArbitraryFilesDiff(childAbsPath1, childAbsPath2, SVNFileUtil.createFilePath(childRelPath, name), false, false, null, context, callback, canceller);
            }

            if (depth.compareTo(SVNDepth.FILES) > 0 && childKind2 == SVNNodeKind.DIR && (childKind1 == SVNNodeKind.FILE || childKind1 == SVNNodeKind.NONE)) {
                doArbitraryDirsDiff(childAbsPath1, childAbsPath2, diffWalker.root1AbsPath, diffWalker.root2AbsPath, depth.compareTo(SVNDepth.IMMEDIATES) <= 0 ? SVNDepth.EMPTY : SVNDepth.INFINITY, context, callback, canceller);
            }
        }
    }

    private static void walkDirectory(File localAbsPath, ArbitraryDiffWalker diffWalker, SVNWCContext context, ISvnDiffCallback callback, ISVNCanceller canceller) throws SVNException {
        visit(localAbsPath, SVNFileType.DIRECTORY, diffWalker, context, callback, canceller);

        File[] children = SVNFileListUtil.listFiles(localAbsPath);
        if (children != null) {
            for (File child : children) {
                SVNFileType type = SVNFileType.getType(child);
                if (type == SVNFileType.DIRECTORY) {
                    walkDirectory(child, diffWalker, context, callback, canceller);
                } else if (type == SVNFileType.FILE || type == SVNFileType.SYMLINK) {
                    visit(child, type, diffWalker, context, callback, canceller);
                }
            }
        }
    }

    private static void visit(File localAbsPath, SVNFileType type, ArbitraryDiffWalker diffWalker, SVNWCContext context, ISvnDiffCallback callback, ISVNCanceller canceller) throws SVNException {
        if (canceller != null) {
            canceller.checkCancelled();
        }
        if (type != SVNFileType.DIRECTORY) {
            return;
        }
        arbitraryDiffThisDir(diffWalker, localAbsPath, SVNDepth.INFINITY, context, callback, canceller);
    }

    private static class ArbitraryDiffWalker {
        private File root1AbsPath;
        private File root2AbsPath;
        private boolean recursingWithinAddedSubtree;
        private boolean recursingWithinAdmDir;
        private File admDirAbsPath;
    }
}
