/*
 * ====================================================================
 * Copyright (c) 2004-2010 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.wc17;

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbKind;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbRepositoryInfo;
import org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
import org.tmatesoft.svn.core.wc.ISVNOptions;
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.SvnStatus;
import org.tmatesoft.svn.core.wc2.SvnTarget;

import java.io.File;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

/**
 * @version 1.3
 * @author TMate Software Ltd.
 */
public class SVNRemoteStatusEditor17 extends SVNStatusEditor17 implements ISVNEditor, ISvnObjectReceiver<SvnStatus> {

    private boolean myIsRootOpen;
    private SvnStatus myAnchorStatus;

    private DirectoryInfo myDirectoryInfo;
    private FileInfo myFileInfo;

    private File myAnchorAbsPath;
    private String myTargetBaseName;
    private File myTargetAbsPath;
    
    private boolean myIsMarkingDeleted;

    public SVNRemoteStatusEditor17(File anchorAbsPath, String targetBaseName, SVNWCContext wcContext, ISVNOptions options, boolean includeIgnored, boolean reportAll, SVNDepth depth,
            ISvnObjectReceiver<SvnStatus> realHandler) throws SVNException {
        super(SVNFileUtil.createFilePath(anchorAbsPath, targetBaseName), wcContext, options, includeIgnored, reportAll, depth, realHandler);
        myAnchorStatus = internalStatus(wcContext, anchorAbsPath);
        myAnchorAbsPath = anchorAbsPath;
        myTargetBaseName = targetBaseName;
        myTargetAbsPath = SVNFileUtil.createFilePath(anchorAbsPath, targetBaseName);
        collectExternals(myTargetAbsPath);
    }

    public void openRoot(long revision) throws SVNException {
        myIsRootOpen = true;
        myDirectoryInfo = new DirectoryInfo(null, null);
    }

    public void deleteEntry(String path, long revision) throws SVNException {
        final File local_abspath = SVNFileUtil.createFilePath(myAnchorAbsPath, path);

        tweakStatusHash(myDirectoryInfo, new DirectoryInfo(path, myDirectoryInfo), local_abspath, SVNStatusType.STATUS_DELETED, SVNStatusType.STATUS_NONE, SVNStatusType.STATUS_NONE,
                SVNRevision.create(revision), null);
        if (myDirectoryInfo.parent != null && myTargetBaseName == null)
            tweakStatusHash(myDirectoryInfo.parent, myDirectoryInfo, myDirectoryInfo.localAbsPath, SVNStatusType.STATUS_MODIFIED, SVNStatusType.STATUS_MODIFIED,
                    SVNStatusType.STATUS_NONE, null, null);
    }

    private void tweakStatusHash(DirectoryInfo dirInfo, DirectoryInfo childDir, File localAbsPath, SVNStatusType reposNodeStatus, SVNStatusType reposTextStatus,
            SVNStatusType reposPropStatus, SVNRevision deletedRev, SVNLock reposLock) throws SVNException {

        Map<File, SvnStatus> statushash = dirInfo.statii;

        /* Is PATH already a hash-key? */
        SvnStatus statstruct = statushash.get(localAbsPath);

        /* If not, make it so. */
        if (statstruct == null) {
            /*
             * If this item isn't being added, then we're most likely dealing
             * with a non-recursive (or at least partially non-recursive)
             * working copy. Due to bugs in how the client reports the state of
             * non-recursive working copies, the repository can send back
             * responses about paths that don't even exist locally. Our best
             * course here is just to ignore those responses. After all, if the
             * client had reported correctly in the first, that path would
             * either be mentioned as an 'add' or not mentioned at all,
             * depending on how we eventually fix the bugs in non-recursivity.
             * See issue #2122 for details.
             */
            if (reposNodeStatus != SVNStatusType.STATUS_ADDED)
                return;

            /* Use the public API to get a statstruct, and put it into the hash. */
            statstruct = internalStatus(myWCContext, localAbsPath);
            if (statstruct.getNodeStatus() == SVNStatusType.STATUS_UNVERSIONED || statstruct.getNodeStatus() == SVNStatusType.STATUS_NONE) {
                statstruct.setWorkingCopyFormat(myAnchorStatus.getWorkingCopyFormat());
            }
            statstruct.setRepositoryLock(reposLock);
            statushash.put(localAbsPath, statstruct);
        }

        /* Merge a repos "delete" + "add" into a single "replace". */
        if ((reposNodeStatus == SVNStatusType.STATUS_ADDED) && (statstruct.getRepositoryNodeStatus() == SVNStatusType.STATUS_DELETED))
            reposNodeStatus = SVNStatusType.STATUS_REPLACED;

        /* Tweak the structure's repos fields. */
        if (reposNodeStatus != null)
            statstruct.setRepositoryNodeStatus(reposNodeStatus);
        if (reposTextStatus != null)
            statstruct.setRepositoryTextStatus(reposTextStatus);
        if (reposPropStatus != null)
            statstruct.setRepositoryPropertiesStatus(reposPropStatus);

        /* Copy out-of-date info. */
        statstruct.setRepositoryRootUrl(myRepositoryRoot);
        statstruct.setRepositoryRelativePath(childDir.computeRepositoryRelativePath());
        /*
         * The last committed date, and author for deleted items isn't
         * available.
         */
        if (statstruct.getRepositoryNodeStatus() == SVNStatusType.STATUS_DELETED) {
            statstruct.setRepositoryKind(statstruct.getKind());

            /*
             * Pre 1.5 servers don't provide the revision a path was deleted. So
             * we punt and use the last committed revision of the path's parent,
             * which has some chance of being correct. At worse it is a higher
             * revision than the path was deleted, but this is better than
             * nothing...
             */
                if (deletedRev == null || !deletedRev.isValid())
                statstruct.setRepositoryChangedRevision(dirInfo.ood_changed_rev);
            else
                statstruct.setRepositoryChangedRevision(deletedRev.getNumber());
        } else {
            statstruct.setRepositoryKind(childDir.ood_kind);
            statstruct.setRepositoryChangedRevision(childDir.ood_changed_rev);
            statstruct.setRepositoryChangedDate(childDir.ood_changed_date);
            if (childDir.ood_changed_author != null)
                statstruct.setRepositoryChangedAuthor(childDir.ood_changed_author);
        }

        return;
    }

    public void addDir(String path, String copyFromPath, long copyFromRevision) throws SVNException {
        myDirectoryInfo = new DirectoryInfo(path, myDirectoryInfo);
        myDirectoryInfo.added = true;
        myDirectoryInfo.parent.text_changed = true;
    }

    public void openDir(String path, long revision) throws SVNException {
        myDirectoryInfo = new DirectoryInfo(path, myDirectoryInfo);
    }

    public void changeDirProperty(String name, SVNPropertyValue value) throws SVNException {
        if (!name.startsWith(SVNProperty.SVN_ENTRY_PREFIX) && !name.startsWith(SVNProperty.SVN_WC_PREFIX)) {
            myDirectoryInfo.prop_changed = true;
        }

        /* Note any changes to the repository. */
        if (value != null) {
            if (SVNProperty.COMMITTED_REVISION.equals(name)) {
                try {
                    myDirectoryInfo.ood_changed_rev = Long.parseLong(value.getString());
                } catch (NumberFormatException nfe) {
                    myDirectoryInfo.ood_changed_rev = SVNWCDb.INVALID_REVNUM;
                }
            } else if (SVNProperty.LAST_AUTHOR.equals(name)) {
                myDirectoryInfo.ood_changed_author = value.getString();
            } else if (SVNProperty.COMMITTED_DATE.equals(name)) {
                myDirectoryInfo.ood_changed_date = SVNDate.parseDate(value.getString());
            }
        }
    }

    public void closeDir() throws SVNException {
        DirectoryInfo db = myDirectoryInfo;
        DirectoryInfo pb = db.parent;

        /*
         * If nothing has changed and directory has no out of date descendants,
         * return.
         */
        if (db.added || db.prop_changed || db.text_changed || db.ood_changed_rev != SVNWCDb.INVALID_REVNUM) {
            SVNStatusType repos_node_status;
            SVNStatusType repos_text_status;
            SVNStatusType repos_prop_status;

            /* If this is a new directory, add it to the statushash. */
            if (db.added) {
                repos_node_status = SVNStatusType.STATUS_ADDED;
                repos_text_status = SVNStatusType.STATUS_ADDED;
                repos_prop_status = db.prop_changed ? SVNStatusType.STATUS_ADDED : SVNStatusType.STATUS_NONE;
            } else {
                repos_node_status = (db.text_changed || db.prop_changed) ? SVNStatusType.STATUS_MODIFIED : SVNStatusType.STATUS_NONE;
                repos_text_status = db.text_changed ? SVNStatusType.STATUS_MODIFIED : SVNStatusType.STATUS_NONE;
                repos_prop_status = db.prop_changed ? SVNStatusType.STATUS_MODIFIED : SVNStatusType.STATUS_NONE;
            }

            /*
             * Maybe add this directory to its parent's status hash. Note that
             * tweak_statushash won't do anything if repos_text_status is not
             * svn_wc_status_added.
             */
            if (pb != null) {
                /*
                 * ### When we add directory locking, we need to find a ###
                 * directory lock here.
                 */
                tweakStatusHash(pb, db, db.localAbsPath, repos_node_status, repos_text_status, repos_prop_status, null, null);
            } else {
                /*
                 * We're editing the root dir of the WC. As its repos status
                 * info isn't otherwise set, set it directly to trigger
                 * invocation of the status callback below.
                 */
                myAnchorStatus.setRepositoryNodeStatus(repos_node_status);
                myAnchorStatus.setRepositoryPropertiesStatus(repos_prop_status);
                myAnchorStatus.setRepositoryTextStatus(repos_text_status);

                /* If the root dir is out of date set the ood info directly too. */
                if (db.ood_changed_rev != myAnchorStatus.getRevision()) {
                    myAnchorStatus.setRepositoryChangedRevision(db.ood_changed_rev);
                    myAnchorStatus.setRepositoryChangedDate(db.ood_changed_date);
                    myAnchorStatus.setRepositoryKind(db.ood_kind);
                    myAnchorStatus.setRepositoryChangedAuthor(db.ood_changed_author);
                }
            }
        }

        /*
         * Handle this directory's statuses, and then note in the parent that
         * this has been done.
         */
        if (pb != null && !db.excluded) {
            boolean was_deleted = false;
            SvnStatus dir_status = pb.statii.get(db.localAbsPath);

            /* See if the directory was deleted or replaced. */
            if (dir_status != null && ((dir_status.getRepositoryNodeStatus() == SVNStatusType.STATUS_DELETED) || (dir_status.getRepositoryNodeStatus() == SVNStatusType.STATUS_REPLACED)))
                was_deleted = true;

            /* Now do the status reporting. */
            WCDbRepositoryInfo dirReposInfo = new WCDbRepositoryInfo();
            if (dirReposInfo != null && dir_status != null) {
                dirReposInfo.rootUrl = dir_status.getRepositoryRootUrl();
                dirReposInfo.relPath = SVNFileUtil.createFilePath(dir_status.getRepositoryRelativePath());
                dirReposInfo.uuid = dir_status.getRepositoryUuid();
            }
            handleStatii(dirReposInfo, db.statii, was_deleted, db.depth);
            if (dir_status != null && isSendableStatus(dir_status))
                getDefaultHandler().receive(SvnTarget.fromFile(dir_status.getPath()), dir_status);
            pb.statii.remove(db.localAbsPath);
        } else if (pb == null) {
            /*
             * If this is the top-most directory, and the operation had a
             * target, we should only report the target.
             */
            if (myTargetBaseName != null && !"".equals(myTargetBaseName)) {
                SvnStatus tgt_status = db.statii.get(myTargetAbsPath);
                if (tgt_status != null) {
                    if (tgt_status.isVersioned() && tgt_status.getKind() == SVNNodeKind.DIR) {
                        getDirStatus(myTargetAbsPath, null, true, null, null, null, null, getDepth(), isReportAll(), isNoIgnore(), getDefaultHandler());
                    }
                    if (isSendableStatus(tgt_status)) {
                        getDefaultHandler().receive(SvnTarget.fromFile(tgt_status.getPath()), tgt_status);
                    }
                }
            } else {
                /*
                 * Otherwise, we report on all our children and ourself. Note
                 * that our directory couldn't have been deleted, because it is
                 * the root of the edit drive.
                 */
                WCDbRepositoryInfo dirReposInfo = new WCDbRepositoryInfo();
                if (dirReposInfo != null) {
                    dirReposInfo.rootUrl = myAnchorStatus.getRepositoryRootUrl();
                    dirReposInfo.relPath = SVNFileUtil.createFilePath(myAnchorStatus.getRepositoryRelativePath());
                    dirReposInfo.uuid = myAnchorStatus.getRepositoryUuid();
                }
                handleStatii(dirReposInfo, db.statii, false, getDepth());
                if (isSendableStatus(myAnchorStatus)) {
                    getDefaultHandler().receive(SvnTarget.fromFile(myAnchorStatus.getPath()), myAnchorStatus);
                }
                myAnchorStatus = null;
            }
        }
        myDirectoryInfo = myDirectoryInfo.parent;
    }

    private boolean isSendableStatus(SvnStatus status) {

        /* If the repository status was touched at all, it's interesting. */
        if (status.getRepositoryNodeStatus() != SVNStatusType.STATUS_NONE)
            return true;

        /* If there is a lock in the repository, send it. */
        if (status.getRepositoryLock() != null)
            return true;

        /* If the item is ignored, and we don't want ignores, skip it. */
        if ((status.getNodeStatus() == SVNStatusType.STATUS_IGNORED) && (!isNoIgnore()))
            return false;

        /*
         * If we want everything, we obviously want this single-item subset of
         * everything.
         */
        if (isReportAll())
            return true;

        /* If the item is unversioned, display it. */
        if (status.getNodeStatus() == SVNStatusType.STATUS_UNVERSIONED)
            return true;

        /* If the text, property or tree state is interesting, send it. */
        if ((status.getNodeStatus() != SVNStatusType.STATUS_NONE && (status.getNodeStatus() != SVNStatusType.STATUS_NORMAL)))
            return true;
        if (status.isConflicted())
            return true;

        /* If it's switched, send it. */
        if (status.isSwitched())
            return true;

        /* If there is a lock token, send it. */
        if (status.isVersioned() && status.getLock() != null)
            return true;

        /* If the entry is associated with a changelist, send it. */
        if (status.getChangelist() != null)
            return true;

        /* Otherwise, don't send it. */
        return false;
    }

    private void handleStatii(WCDbRepositoryInfo reposInfo, Map<File, SvnStatus> statii, boolean dirWasDeleted, SVNDepth depth) throws SVNException {
        ISvnObjectReceiver<SvnStatus> handler = dirWasDeleted ? this : getDefaultHandler();
        for (Iterator<File> paths = statii.keySet().iterator(); paths.hasNext();) {
            File localAbsPath =  paths.next();
            SvnStatus status = statii.get(localAbsPath);
            if (status.getKind() == SVNNodeKind.DIR && (depth == SVNDepth.UNKNOWN || depth == SVNDepth.INFINITY)) {
                getDirStatus(localAbsPath, null, true, reposInfo, null, null, myGlobalIgnores, depth, isReportAll(), isNoIgnore(), handler);
            }
            if (dirWasDeleted) {
                status.setRepositoryNodeStatus(SVNStatusType.STATUS_DELETED);
            }
            if (isSendableStatus(status)) {
                getDefaultHandler().receive(SvnTarget.fromFile(localAbsPath), status);
            }
        }
    }

    public void addFile(String path, String copyFromPath, long copyFromRevision) throws SVNException {
        myFileInfo = new FileInfo(myDirectoryInfo, path, true);
        myFileInfo.added = true;
        myDirectoryInfo.text_changed = true;
    }

    public void openFile(String path, long revision) throws SVNException {
        myFileInfo = new FileInfo(myDirectoryInfo, path, false);
    }

    public void applyTextDelta(String path, String baseChecksum) throws SVNException {
        myFileInfo.text_changed = true;
        myFileInfo.baseChecksum = baseChecksum;
    }

    public void changeFileProperty(String path, String propertyName, SVNPropertyValue propertyValue) throws SVNException {
        if (!propertyName.startsWith(SVNProperty.SVN_ENTRY_PREFIX) && !propertyName.startsWith(SVNProperty.SVN_WC_PREFIX)) {
            myFileInfo.prop_changed = true;
        }
        if (propertyValue != null) {
            if (SVNProperty.COMMITTED_REVISION.equals(propertyName)) {
                try {
                    myFileInfo.ood_changed_rev = Long.parseLong(propertyValue.getString());
                } catch (NumberFormatException nfe) {
                    myFileInfo.ood_changed_rev = SVNWCDb.INVALID_REVNUM;
                }
            } else if (SVNProperty.COMMITTED_DATE.equals(propertyName)) {
                myFileInfo.ood_changed_date = SVNDate.parseDate(propertyValue.getString());
            } else if (SVNProperty.LAST_AUTHOR.equals(propertyName)) {
                myFileInfo.ood_changed_author = propertyValue.getString();
            }
        }
    }

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

        SVNStatusType repos_node_status;
        SVNStatusType repos_text_status;
        SVNStatusType repos_prop_status;
        SVNLock repos_lock = null;

        if (myFileInfo.baseChecksum != null && textChecksum != null) {
            myFileInfo.text_changed = !myFileInfo.baseChecksum.equals(textChecksum);
        }

        /* If nothing has changed, return. */
        if (!(myFileInfo.added || myFileInfo.prop_changed || myFileInfo.text_changed))
            return;

        /* If this is a new file, add it to the statushash. */
        if (myFileInfo.added) {
            repos_node_status = SVNStatusType.STATUS_ADDED;
            repos_text_status = SVNStatusType.STATUS_ADDED;
            repos_prop_status = myFileInfo.prop_changed ? SVNStatusType.STATUS_ADDED : SVNStatusType.STATUS_NONE;
            if (myRepositoryLocks != null) {
                File dir_repos_relpath = findDirReposRelpath(myFileInfo.parent);
                if (dir_repos_relpath != null) {
                    /*
                     * repos_lock still uses the deprecated filesystem absolute
                     * path format
                     */
                    File repos_relpath = SVNFileUtil.createFilePath(dir_repos_relpath, myFileInfo.name);
                    String reposRelPathWithDirectSlashes = repos_relpath.toString().replace(File.separatorChar, '/');
                    repos_lock = (SVNLock) myRepositoryLocks.get(SVNEncodingUtil.uriDecode("/" + reposRelPathWithDirectSlashes));
                }
            }
        } else {
            repos_node_status = (myFileInfo.text_changed || myFileInfo.prop_changed) ? SVNStatusType.STATUS_MODIFIED : SVNStatusType.STATUS_NONE;
            repos_text_status = myFileInfo.text_changed ? SVNStatusType.STATUS_MODIFIED : SVNStatusType.STATUS_NONE;
            repos_prop_status = myFileInfo.prop_changed ? SVNStatusType.STATUS_MODIFIED : SVNStatusType.STATUS_NONE;
        }

        tweakStatusHash(myFileInfo, myFileInfo.localAbsPath, repos_node_status, repos_text_status, repos_prop_status, SVNWCDb.INVALID_REVNUM, repos_lock);
    }

    private void tweakStatusHash(FileInfo fileInfo, File localAbsPath, SVNStatusType reposNodeStatus, SVNStatusType reposTextStatus, SVNStatusType reposPropStatus, long revnum, SVNLock reposLock)
            throws SVNException {

        Map<File, SvnStatus> statushash = fileInfo.parent.statii;
        SvnStatus statstruct = statushash.get(localAbsPath);
        if (statstruct == null) {
            if (reposNodeStatus != SVNStatusType.STATUS_ADDED)
                return;
            statstruct = internalStatus(myWCContext, localAbsPath);
            statstruct.setRepositoryLock(reposLock);
            if (statstruct.getNodeStatus() == SVNStatusType.STATUS_UNVERSIONED || statstruct.getNodeStatus() == SVNStatusType.STATUS_NONE) {
                statstruct.setWorkingCopyFormat(myAnchorStatus.getWorkingCopyFormat());
            }
            statushash.put(localAbsPath, statstruct);
        }

        if ((reposNodeStatus == SVNStatusType.STATUS_ADDED) && (statstruct.getRepositoryNodeStatus() == SVNStatusType.STATUS_DELETED))
            reposNodeStatus = SVNStatusType.STATUS_REPLACED;

        /* Tweak the structure's repos fields. */
        if (reposNodeStatus != null)
            statstruct.setRepositoryNodeStatus(reposNodeStatus);
        if (reposTextStatus != null)
            statstruct.setRepositoryTextStatus(reposTextStatus);
        if (reposPropStatus != null)
            statstruct.setRepositoryPropertiesStatus(reposPropStatus);

        statstruct.setRepositoryChangedRevision(fileInfo.ood_changed_rev);
        statstruct.setRepositoryChangedDate(fileInfo.ood_changed_date);
        statstruct.setRepositoryKind(fileInfo.ood_kind);
        statstruct.setRepositoryRootUrl(myRepositoryRoot);
        statstruct.setRepositoryRelativePath(fileInfo.computeRepositoryRelativePath());
        
        if (fileInfo.ood_changed_author != null)
            statstruct.setRepositoryChangedAuthor(fileInfo.ood_changed_author);

    }

    private File findDirReposRelpath(DirectoryInfo dirinfo) {
        /* If we have no name, we're the root, return the anchor URL. */
        if (dirinfo.name == null)
            return SVNFileUtil.createFilePath(myAnchorStatus.getRepositoryRelativePath());
        File repos_relpath;
        DirectoryInfo parent = dirinfo.parent;
        SvnStatus status = parent.statii.get(dirinfo.localAbsPath);
        if (status != null)
            return SVNFileUtil.createFilePath(status.getRepositoryRelativePath());
        repos_relpath = findDirReposRelpath(parent);
        if (repos_relpath != null)
            return SVNFileUtil.createFilePath(repos_relpath, dirinfo.name);
        return null;
    }

    public void abortEdit() throws SVNException {
    }

    public void absentDir(String path) throws SVNException {
    }

    public void absentFile(String path) throws SVNException {
    }

    public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException {
        if (diffWindow != SVNDiffWindow.EMPTY) {
            myFileInfo.text_changed = true;
        }
        return null;
    }

    public void textDeltaEnd(String path) throws SVNException {
    }

    public void receive(SvnTarget target, SvnStatus status) throws SVNException {
        if (myIsMarkingDeleted) {
            status.setRepositoryNodeStatus(SVNStatusType.STATUS_DELETED);
        }
        getDefaultHandler().receive(target, status);
    }

    public SVNCommitInfo closeEdit() throws SVNException {
        if (!myIsRootOpen) {
            super.closeEdit();
        }
        return new SVNCommitInfo(getTargetRevision(), null, null);
    }

    private class DirectoryInfo implements ISvnObjectReceiver<SvnStatus> {

        private File localAbsPath;
        private String name;
        private DirectoryInfo parent;
        private TreeMap<File, SvnStatus> statii;
        private long ood_changed_rev;
        private SVNDate ood_changed_date;
        private SVNNodeKind ood_kind;
        private String ood_changed_author;
        private boolean excluded;
        private SVNDepth depth;
        private boolean added;
        private boolean prop_changed;
        private boolean text_changed;
        private String repositoryRelativePath;

        public DirectoryInfo(String path, DirectoryInfo parent) throws SVNException {
            File local_abspath;
            SvnStatus status_in_parent;

            assert (path != null || parent == null);

            /* Construct the absolute path of this directory. */
            if (parent != null)
                local_abspath = SVNFileUtil.createFilePath(myAnchorAbsPath, path);
            else
                local_abspath = myAnchorAbsPath;

            /* Finish populating the baton members. */
            this.localAbsPath = local_abspath;
            this.name = path != null ? SVNPathUtil.tail(path) : null;
            this.parent = parent;
            this.statii = new TreeMap<File, SvnStatus>();
            this.ood_changed_rev = SVNWCContext.INVALID_REVNUM;
            this.ood_changed_date = null;
            this.ood_kind = SVNNodeKind.DIR;
            this.ood_changed_author = null;

            if (parent != null) {
                if (parent.excluded)
                    this.excluded = true;
                else if (parent.depth == SVNDepth.IMMEDIATES)
                    this.depth = SVNDepth.EMPTY;
                else if (parent.depth == SVNDepth.FILES || parent.depth == SVNDepth.EMPTY)
                    this.excluded = true;
                else if (parent.depth == SVNDepth.UNKNOWN)
                    /*
                     * This is only tentative, it can be overridden from d's
                     * entry later.
                     */
                    this.depth = SVNDepth.UNKNOWN;
                else
                    this.depth = SVNDepth.INFINITY;
            } else {
                this.depth = getDepth();
            }

            /*
             * Get the status for this path's children. Of course, we only want
             * to do this if the path is versioned as a directory.
             */
            if (parent != null)
                status_in_parent = parent.statii.get(this.localAbsPath);
            else
                status_in_parent = myAnchorStatus;

            /*
             * Order is important here. We can't depend on
             * status_in_parent->entry being non-NULL until after we've checked
             * all the conditions that might indicate that the parent is
             * unversioned ("unversioned" for our purposes includes being an
             * external or ignored item).
             */
            if (status_in_parent != null && (status_in_parent.getNodeStatus() != SVNStatusType.STATUS_UNVERSIONED) && (status_in_parent.getNodeStatus() != SVNStatusType.STATUS_EXTERNAL)
                    && (status_in_parent.getNodeStatus() != SVNStatusType.STATUS_IGNORED) && (status_in_parent.getKind() == SVNNodeKind.DIR) && (!this.excluded)
                    && (this.depth == SVNDepth.UNKNOWN || this.depth == SVNDepth.INFINITY || this.depth == SVNDepth.FILES || this.depth == SVNDepth.IMMEDIATES)) {
                SvnStatus this_dir_status;
                Collection<String> ignores = myGlobalIgnores;

                WCDbRepositoryInfo parentReposInfo = new WCDbRepositoryInfo();
                parentReposInfo.rootUrl = status_in_parent.getRepositoryRootUrl();
                parentReposInfo.uuid = status_in_parent.getRepositoryUuid();
                
                getDirStatus(local_abspath, null, true, parentReposInfo, null, null, ignores, 
                        this.depth == SVNDepth.FILES ? SVNDepth.FILES : SVNDepth.IMMEDIATES, true, true, this);

                /* If we found a depth here, it should govern. */
                this_dir_status = this.statii.get(this.localAbsPath);
                if (this_dir_status != null && this_dir_status.isVersioned() && (this.depth == SVNDepth.UNKNOWN || this.depth.compareTo(status_in_parent.getDepth()) > 0)) {
                    this.depth = this_dir_status.getDepth();
                }
            }

        }

        public void receive(SvnTarget target, SvnStatus status) throws SVNException {
            if (status != null) {
                statii.put(status.getPath(), status);
            }
        }
        
        private String computeRepositoryRelativePath() throws SVNException {
            if (repositoryRelativePath != null) {
                return repositoryRelativePath;
            }
            if (name == null) {
                return myAnchorStatus.getRepositoryRelativePath();
            }
            SvnStatus status = (SvnStatus) parent.statii.get(localAbsPath);
            if (status != null && status.getRepositoryRelativePath() != null) {
                repositoryRelativePath = status.getRepositoryRelativePath();
                return status.getRepositoryRelativePath();
            }
            String url = parent.computeRepositoryRelativePath();
            if (url != null) {
                 url = SVNPathUtil.append(url, name);
                 repositoryRelativePath = url;
            }
            return repositoryRelativePath;
        }

    }

    public class FileInfo {

        private File localAbsPath;
        private String name;
        private DirectoryInfo parent;
        private long ood_changed_rev;
        private SVNDate ood_changed_date;
        private SVNNodeKind ood_kind;
        private String ood_changed_author;
        private boolean added;
        private boolean text_changed;
        private boolean prop_changed;
        private String baseChecksum;

        public FileInfo(DirectoryInfo parent, String path, boolean added) {
            this.localAbsPath = SVNFileUtil.createFilePath(myAnchorAbsPath, path);
            this.name = SVNFileUtil.getFileName(this.localAbsPath);
            this.parent = parent;
            this.ood_changed_rev = SVNWCDb.INVALID_REVNUM;
            this.ood_changed_date = null;
            this.ood_kind = SVNNodeKind.FILE;
            this.ood_changed_author = null;
        }

        private String computeRepositoryRelativePath() throws SVNException {
            return SVNPathUtil.append(parent.computeRepositoryRelativePath(), name);
        }

    }

}
