package org.tmatesoft.svn.core.internal.wc16;

import java.io.File;
import java.util.Iterator;

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.SVNNodeKind;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNCopyDriver;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNLog;
import org.tmatesoft.svn.core.internal.wc.admin.SVNVersionedProperties;
import org.tmatesoft.svn.core.internal.wc.admin.SVNWCAccess;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNRepositoryPool;
import org.tmatesoft.svn.core.wc.SVNCopySource;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import org.tmatesoft.svn.util.ISVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * The <b>SVNMoveClient</b> provides an extra client-side functionality over
 * standard (i.e. compatible with the SVN command line client) move operations.
 * This class helps to overcome the SVN limitations regarding move operations.
 * Using <b>SVNMoveClient</b> you can easily:
 * <ul>
 * <li>move versioned items to other versioned ones within the same Working
 * Copy, what even allows to replace items scheduled for deletion, or those that
 * are missing but are still under version control and have a node kind
 * different from the node kind of the source (!);
 * <li>move versioned items belonging to one Working Copy to versioned items
 * that belong to absolutely different Working Copy;
 * <li>move versioned items to unversioned ones;
 * <li>move unversioned items to versioned ones;
 * <li>move unversioned items to unversioned ones;
 * <li>revert any of the kinds of moving listed above;
 * <li>complete a copy/move operation for a file, that is if you have manually
 * copied/moved a versioned file to an unversioned file in a Working copy, you
 * can run a 'virtual' copy/move on these files to copy/move all the necessary
 * administrative version control information.
 * </ul>
 * 
 * @version 1.3
 * @author TMate Software Ltd.
 * @since 1.2
 */
public class SVNMoveClient16 extends SVNCopyDriver {

    private SVNWCClient16 myWCClient;
    private SVNCopyClient16 myCopyClient;

    /**
     * Constructs and initializes an <b>SVNMoveClient</b> object with the
     * specified run-time configuration and authentication drivers.
     * <p>
     * If <code>options</code> is <span class="javakeyword">null</span>, then
     * this <b>SVNMoveClient</b> will be using a default run-time configuration
     * driver which takes client-side settings from the default SVN's run-time
     * configuration area but is not able to change those settings (read more on
     * {@link ISVNOptions} and {@link SVNWCUtil}).
     * <p>
     * If <code>authManager</code> is <span class="javakeyword">null</span>,
     * then this <b>SVNMoveClient</b> will be using a default authentication and
     * network layers driver (see
     * {@link SVNWCUtil#createDefaultAuthenticationManager()}) which uses
     * server-side settings and auth storage from the default SVN's run-time
     * configuration area (or system properties if that area is not found).
     * 
     * @param authManager
     *            an authentication and network layers driver
     * @param options
     *            a run-time configuration options driver
     */
    public SVNMoveClient16(ISVNAuthenticationManager authManager, ISVNOptions options) {
        super(authManager, options);
        myWCClient = new SVNWCClient16(authManager, options);
        myCopyClient = new SVNCopyClient16(authManager, options);
    }

    /**
     * Constructs and initializes an <b>SVNMoveClient</b> object with the
     * specified run-time configuration and repository pool object.
     * <p/>
     * If <code>options</code> is <span class="javakeyword">null</span>, then
     * this <b>SVNMoveClient</b> will be using a default run-time configuration
     * driver which takes client-side settings from the default SVN's run-time
     * configuration area but is not able to change those settings (read more on
     * {@link ISVNOptions} and {@link SVNWCUtil}).
     * <p/>
     * If <code>repositoryPool</code> is <span class="javakeyword">null</span>,
     * then {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory} will be used
     * to create {@link SVNRepository repository access objects}.
     * 
     * @param repositoryPool
     *            a repository pool object
     * @param options
     *            a run-time configuration options driver
     */
    public SVNMoveClient16(ISVNRepositoryPool repositoryPool, ISVNOptions options) {
        super(repositoryPool, options);
        myWCClient = new SVNWCClient16(repositoryPool, options);
        myCopyClient = new SVNCopyClient16(repositoryPool, options);
    }

    public void setEventHandler(ISVNEventHandler dispatcher) {
        super.setEventHandler(dispatcher);
        myWCClient.setEventHandler(dispatcher);
        myCopyClient.setEventHandler(dispatcher);
    }

    public void setDebugLog(ISVNDebugLog log) {
        super.setDebugLog(log);
        myWCClient.setDebugLog(log);
        myCopyClient.setDebugLog(log);
    }

    public void setOptions(ISVNOptions options) {
        super.setOptions(options);
        if (myWCClient != null) {
            myWCClient.setOptions(options);
        }
        if (myCopyClient != null) {
            myCopyClient.setOptions(options);
        }
    }

    /**
     * Moves a source item to a destination one.
     * <p>
     * <code>dst</code> should not exist. Furher it's considered to be versioned
     * if its parent directory is under version control, otherwise
     * <code>dst</code> is considered to be unversioned.
     * <p>
     * If both <code>src</code> and <code>dst</code> are unversioned, then
     * simply moves <code>src</code> to <code>dst</code> in the filesystem.
     * <p>
     * If <code>src</code> is versioned but <code>dst</code> is not, then
     * exports <code>src</code> to <code>dst</code> in the filesystem and
     * removes <code>src</code> from version control.
     * <p>
     * If <code>dst</code> is versioned but <code>src</code> is not, then moves
     * <code>src</code> to <code>dst</code> (even if <code>dst</code> is
     * scheduled for deletion).
     * <p>
     * If both <code>src</code> and <code>dst</code> are versioned and located
     * within the same Working Copy, then moves <code>src</code> to
     * <code>dst</code> (even if <code>dst</code> is scheduled for deletion), or
     * tries to replace <code>dst</code> with <code>src</code> if the former is
     * missing and has a node kind different from the node kind of the source.
     * If <code>src</code> is scheduled for addition with history,
     * <code>dst</code> will be set the same ancestor URL and revision from
     * which the source was copied. If <code>src</code> and <code>dst</code> are
     * located in different Working Copies, then this method copies
     * <code>src</code> to <code>dst</code>, tries to put the latter under
     * version control and finally removes <code>src</code>.
     * 
     * @param src
     *            a source path
     * @param dst
     *            a destination path
     * @throws SVNException
     *             if one of the following is true:
     *             <ul>
     *             <li><code>dst</code> already exists <li><code>src</code> does
     *             not exist
     *             </ul>
     */
    public void doMove(File src, File dst) throws SVNException {
        if (dst.exists()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "File ''{0}'' already exists", dst);
            SVNErrorManager.error(err, SVNLogType.WC);
        } else if (!src.exists()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.NODE_UNKNOWN_KIND, "Path ''{0}'' does not exist", src);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        boolean srcIsVersioned = isVersionedFile(src);
        boolean dstParentIsVersioned = isVersionedFile(dst.getParentFile());
        if (!srcIsVersioned && !dstParentIsVersioned) {
            SVNFileUtil.rename(src, dst);
        } else if (!dstParentIsVersioned) {
            SVNFileUtil.copy(src, dst, false, false);
            myWCClient.doDelete(src, true, false);
        } else if (!srcIsVersioned) {
            SVNFileUtil.rename(src, dst);
        } else {
            SVNWCAccess wcAccess = createWCAccess();
            File srcParent = src.getParentFile();
            File dstParent = dst.getParentFile();
            SVNAdminArea srcParentArea = null;
            SVNAdminArea dstParentArea = null;
            try {
                if (srcParent.equals(dstParent)) {
                    wcAccess.closeAdminArea(srcParent);
                    srcParentArea = dstParentArea = wcAccess.open(srcParent, true, 0);
                } else {
                    srcParentArea = wcAccess.open(srcParent, false, 0);
                    dstParentArea = wcAccess.open(dstParent, true, 0);
                }
                SVNEntry srcEntry = srcParentArea.getVersionedEntry(src.getName(), false);
                SVNEntry dstEntry = dstParentArea.getEntry(dst.getName(), false);
                File srcWCRoot = SVNWCUtil.getWorkingCopyRoot(src, true);
                File dstWCRoot = SVNWCUtil.getWorkingCopyRoot(dst, true);
                boolean sameWC = srcWCRoot != null && srcWCRoot.equals(dstWCRoot);
                if (sameWC && dstEntry != null && (dstEntry.isScheduledForDeletion() || dstEntry.getKind() != srcEntry.getKind())) {
                    wcAccess.close();
                    if (srcEntry.getKind() == dstEntry.getKind() && srcEntry.getSchedule() == null && srcEntry.isFile()) {
                        SVNCopySource source = new SVNCopySource(SVNRevision.UNDEFINED, SVNRevision.WORKING, src);
                        myCopyClient.doCopy(new SVNCopySource[] {
                            source
                        }, dst, true, false, true);
                        return;
                    }
                    SVNFileUtil.copy(src, dst, false, false);
                    try {
                        myWCClient.doAdd(dst, false, false, false, SVNDepth.INFINITY, false, false);
                    } catch (SVNException e) {
                    }
                    myWCClient.doDelete(src, true, false);
                    return;
                } else if (!sameWC) {
                    SVNEntry dstTmpEntry = dstEntry != null && dstEntry.isThisDir() ? dstEntry : dstParentArea.getVersionedEntry(dstParentArea.getThisDirName(), false);
                    SVNEntry srcTmpEntry = srcEntry != null && srcEntry.isThisDir() ? srcEntry : srcParentArea.getVersionedEntry(srcParentArea.getThisDirName(), false);
                    
                    if (srcTmpEntry.getRepositoryRoot() != null && dstTmpEntry.getRepositoryRoot() != null && srcTmpEntry.getRepositoryRoot().equals(dstTmpEntry.getRepositoryRoot())) {
                        wcAccess.close();
                        SVNCopySource source = new SVNCopySource(SVNRevision.UNDEFINED, SVNRevision.WORKING, src);
                        myCopyClient.doCopy(new SVNCopySource[] {
                            source
                        }, dst, true, false, true);
                        return;
                    }
                }
                if (dstEntry != null) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "There is already a versioned item ''{0}''", dst);
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                SVNFileUtil.copy(src, dst, false, sameWC);
                if (!sameWC) {
                    wcAccess.close();
                    try {
                        myWCClient.doAdd(dst, false, false, false, SVNDepth.INFINITY, false, false);
                    } catch (SVNException e) {
                    }
                } else if (srcEntry.isFile()) {
                    if (dstEntry == null) {
                        dstEntry = dstParentArea.addEntry(dst.getName());
                    }
                    String srcURL = srcEntry.getURL();
                    String srcCFURL = srcEntry.getCopyFromURL();
                    long srcRevision = srcEntry.getRevision();
                    long srcCFRevision = srcEntry.getCopyFromRevision();
                    SVNVersionedProperties srcProps = srcParentArea.getProperties(src.getName());
                    SVNVersionedProperties dstProps = dstParentArea.getProperties(dst.getName());
                    srcProps.copyTo(dstProps);
                    File srcBaseFile = srcParentArea.getBaseFile(src.getName(), false);
                    File dstBaseFile = dstParentArea.getBaseFile(dst.getName(), false);
                    if (srcBaseFile.isFile()) {
                        SVNFileUtil.copy(srcBaseFile, dstBaseFile, false, false);
                    }
                    if (srcEntry.isScheduledForAddition() && srcEntry.isCopied()) {
                        dstEntry.scheduleForAddition();
                        dstEntry.setCopyFromRevision(srcCFRevision);
                        dstEntry.setCopyFromURL(srcCFURL);
                        dstEntry.setKind(SVNNodeKind.FILE);
                        dstEntry.setRevision(srcRevision);
                        dstEntry.setCopied(true);
                    } else if (!srcEntry.isCopied() && !srcEntry.isScheduledForAddition()) {
                        dstEntry.setCopied(true);
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.FILE);
                        dstEntry.setCopyFromRevision(srcRevision);
                        dstEntry.setCopyFromURL(srcURL);
                    } else {
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.FILE);
                        if (!dstEntry.isScheduledForReplacement()) {
                            dstEntry.setRevision(0);
                        }
                    }
                    SVNLog log = dstParentArea.getLog();
                    dstParentArea.saveEntries(false);
                    dstParentArea.saveVersionedProperties(log, true);
                    log.save();
                    dstParentArea.runLogs();
                } else if (srcEntry.isDirectory()) {
                    SVNAdminArea srcArea = wcAccess.open(src, false, 0);
                    srcEntry = srcArea.getEntry(srcArea.getThisDirName(), false);
                    if (dstEntry == null) {
                        dstEntry = dstParentArea.addEntry(dst.getName());
                    }
                    SVNAdminArea dstArea = wcAccess.open(dst, true, SVNWCAccess.INFINITE_DEPTH);
                    SVNVersionedProperties srcProps = srcArea.getProperties(srcArea.getThisDirName());
                    SVNVersionedProperties dstProps = dstArea.getProperties(dstArea.getThisDirName());
                    SVNEntry dstParentEntry = dstParentArea.getEntry(dstParentArea.getThisDirName(), false);
                    String srcURL = srcEntry.getURL();
                    String srcCFURL = srcEntry.getCopyFromURL();
                    String dstURL = dstParentEntry.getURL();
                    String repositoryRootURL = dstParentEntry.getRepositoryRoot();
                    long srcRevision = srcEntry.getRevision();
                    long srcCFRevision = srcEntry.getCopyFromRevision();
                    dstURL = SVNPathUtil.append(dstURL, SVNEncodingUtil.uriEncode(dst.getName()));
                    if (srcEntry.isScheduledForAddition() && srcEntry.isCopied()) {
                        srcProps.copyTo(dstProps);
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.DIR);
                        dstEntry.setCopied(true);
                        dstEntry.setCopyFromRevision(srcCFRevision);
                        dstEntry.setCopyFromURL(srcCFURL);
                        SVNEntry dstThisEntry = dstArea.getEntry(dstArea.getThisDirName(), false);
                        dstThisEntry.scheduleForAddition();
                        dstThisEntry.setKind(SVNNodeKind.DIR);
                        dstThisEntry.setCopyFromRevision(srcCFRevision);
                        dstThisEntry.setCopyFromURL(srcCFURL);
                        dstThisEntry.setRevision(srcRevision);
                        dstThisEntry.setCopied(true);
                        SVNLog log = dstArea.getLog();
                        dstArea.saveVersionedProperties(log, true);
                        dstParentArea.saveEntries(false);
                        log.save();
                        dstArea.runLogs();
                        dstArea.updateURL(dstURL, true);
                        dstParentArea.saveEntries(true);
                    } else if (!srcEntry.isCopied() && !srcEntry.isScheduledForAddition()) {
                        srcProps.copyTo(dstProps);
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.DIR);
                        dstEntry.setCopied(true);
                        dstEntry.setCopyFromRevision(srcRevision);
                        dstEntry.setCopyFromURL(srcURL);
                        SVNEntry dstThisEntry = dstArea.getEntry(dstArea.getThisDirName(), false);
                        dstThisEntry.scheduleForAddition();
                        dstThisEntry.setKind(SVNNodeKind.DIR);
                        dstThisEntry.setCopied(true);
                        dstThisEntry.scheduleForAddition();
                        dstThisEntry.setKind(SVNNodeKind.DIR);
                        dstThisEntry.setCopyFromRevision(srcRevision);
                        dstThisEntry.setCopyFromURL(srcURL);
                        dstThisEntry.setURL(dstURL);
                        dstThisEntry.setRepositoryRoot(repositoryRootURL);
                        SVNLog log = dstArea.getLog();
                        dstArea.saveVersionedProperties(log, true);
                        dstArea.saveEntries(false);
                        log.save();
                        dstArea.runLogs();
                        updateCopiedDirectory(dstArea, dstArea.getThisDirName(), dstURL, repositoryRootURL, null, -1);
                        dstArea.saveEntries(true);
                        dstParentArea.saveEntries(true);
                    } else {
                        dstParentArea.deleteEntry(dst.getName());
                        dstParentArea.saveEntries(true);
                        SVNFileUtil.deleteAll(dst, this);
                        SVNFileUtil.copy(src, dst, false, false);
                        wcAccess.close();
                        myWCClient.doAdd(dst, false, false, false, SVNDepth.INFINITY, false, false);
                    }
                }
                try {
                    wcAccess.close();
                    myWCClient.doDelete(src, true, false);
                } catch (SVNException e) {
                }
            } finally {
                wcAccess.close();
            }
        }
    }

    /**
     * Reverts a previous move operation back. Provided in pair with
     * {@link #doMove(File,File) doMove()} and used to roll back move
     * operations. In this case <code>src</code> is considered to be the target
     * of the previsous move operation, and <code>dst</code> is regarded to be
     * the source of that same operation which have been moved to
     * <code>src</code> and now is to be restored.
     * <p>
     * <code>dst</code> could exist in that case if it has been a WC directory
     * that was scheduled for deletion during the previous move operation.
     * Furher <code>dst</code> is considered to be versioned if its parent
     * directory is under version control, otherwise <code>dst</code> is
     * considered to be unversioned.
     * <p>
     * If both <code>src</code> and <code>dst</code> are unversioned, then
     * simply moves <code>src</code> back to <code>dst</code> in the filesystem.
     * <p>
     * If <code>src</code> is versioned but <code>dst</code> is not, then
     * unmoves <code>src</code> to <code>dst</code> in the filesystem and
     * removes <code>src</code> from version control.
     * <p>
     * If <code>dst</code> is versioned but <code>src</code> is not, then first
     * tries to make a revert on <code>dst</code> - if it has not been committed
     * yet, it will be simply reverted. However in the case <code>dst</code> has
     * been already removed from the repository, <code>src</code> will be copied
     * back to <code>dst</code> and scheduled for addition. Then
     * <code>src</code> is removed from the filesystem.
     * <p>
     * If both <code>src</code> and <code>dst</code> are versioned then the
     * following situations are possible:
     * <ul>
     * <li>If <code>dst</code> is still scheduled for deletion, then it is
     * reverted back and <code>src</code> is scheduled for deletion.
     * <li>in the case if <code>dst</code> exists but is not scheduled for
     * deletion, <code>src</code> is cleanly exported to <code>dst</code> and
     * removed from version control.
     * <li>if <code>dst</code> and <code>src</code> are from different
     * repositories (appear to be in different Working Copies), then
     * <code>src</code> is copied to <code>dst</code> (with scheduling
     * <code>dst</code> for addition, but not with history since copying is made
     * in the filesystem only) and removed from version control.
     * <li>if both <code>dst</code> and <code>src</code> are in the same
     * repository (appear to be located in the same Working Copy) and:
     * <ul style="list-style-type: lower-alpha">
     * <li>if <code>src</code> is scheduled for addition with history, then
     * copies <code>src</code> to <code>dst</code> specifying the source
     * ancestor's URL and revision (i.e. the ancestor of the source is the
     * ancestor of the destination);
     * <li>if <code>src</code> is already under version control, then copies
     * <code>src</code> to <code>dst</code> specifying the source URL and
     * revision as the ancestor (i.e. <code>src</code> itself is the ancestor of
     * <code>dst</code>);
     * <li>if <code>src</code> is just scheduled for addition (without history),
     * then simply copies <code>src</code> to <code>dst</code> (only in the
     * filesystem, without history) and schedules <code>dst</code> for addition;
     * </ul>
     * then <code>src</code> is removed from version control.
     * </ul>
     * 
     * @param src
     *            a source path
     * @param dst
     *            a destination path
     * @throws SVNException
     *             if <code>src</code> does not exist
     */
    public void undoMove(File src, File dst) throws SVNException {
        if (!src.exists()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.NODE_UNKNOWN_KIND, "Path ''{0}'' does not exist", src);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        boolean srcIsVersioned = isVersionedFile(src);
        boolean dstParentIsVersioned = isVersionedFile(dst.getParentFile());
        if (!srcIsVersioned && !dstParentIsVersioned) {
            SVNFileUtil.rename(src, dst);
        } else if (!dstParentIsVersioned) {
            SVNFileUtil.copy(src, dst, false, false);
            myWCClient.doDelete(src, true, false);
        } else if (!srcIsVersioned) {
            SVNFileUtil.rename(src, dst);
            SVNWCAccess dstAccess = createWCAccess();
            boolean revert = false;
            try {
                dstAccess.probeOpen(dst, false, 0);
                SVNEntry dstEntry = dstAccess.getEntry(dst, false);
                revert = dstEntry != null && dstEntry.isScheduledForDeletion();
            } catch (SVNException e) {
            } finally {
                dstAccess.close();
            }
            if (revert) {
                myWCClient.doRevert(new File[] {
                    dst
                }, SVNDepth.INFINITY, null);
            } else {
            }
        } else {
            SVNWCAccess wcAccess = createWCAccess();
            File srcParent = src.getParentFile();
            File dstParent = dst.getParentFile();
            SVNAdminArea srcParentArea = null;
            SVNAdminArea dstParentArea = null;
            try {
                if (srcParent.equals(dstParent)) {
                    wcAccess.closeAdminArea(srcParent);
                    srcParentArea = dstParentArea = wcAccess.open(srcParent, true, 0);
                } else {
                    srcParentArea = wcAccess.open(srcParent, false, 0);
                    dstParentArea = wcAccess.open(dstParent, true, 0);
                }
                SVNEntry srcEntry = srcParentArea.getEntry(src.getName(), true);
                SVNEntry dstEntry = dstParentArea.getEntry(dst.getName(), true);
                if (dstEntry != null && dstEntry.isScheduledForDeletion()) {
                    wcAccess.close();
                    myWCClient.doRevert(new File[] {
                        dst
                    }, SVNDepth.INFINITY, null);
                    myWCClient.doDelete(src, true, false);
                    return;
                }
                SVNEntry dstParentEntry = wcAccess.getEntry(dstParent, false);
                File srcWCRoot = SVNWCUtil.getWorkingCopyRoot(src, true);
                File dstWCRoot = SVNWCUtil.getWorkingCopyRoot(dst, true);
                boolean sameWC = srcWCRoot != null && srcWCRoot.equals(dstWCRoot);
                SVNFileUtil.copy(src, dst, false, sameWC);
                if (dstEntry != null && dstEntry.getKind() != srcEntry.getKind()) {
                    wcAccess.close();
                    myWCClient.doDelete(src, true, false);
                    return;
                }
                if (!sameWC) {
                    wcAccess.close();
                    try {
                        myWCClient.doAdd(dst, false, false, false, SVNDepth.INFINITY, false, false);
                    } catch (SVNException e) {
                    }
                } else if (srcEntry.isFile()) {
                    if (dstEntry == null) {
                        dstEntry = dstParentArea.addEntry(dst.getName());
                    }
                    String srcURL = srcEntry.getURL();
                    String srcCFURL = srcEntry.getCopyFromURL();
                    long srcRevision = srcEntry.getRevision();
                    long srcCFRevision = srcEntry.getCopyFromRevision();
                    if (srcEntry.isScheduledForAddition() && srcEntry.isCopied()) {
                        dstEntry.scheduleForAddition();
                        dstEntry.setCopyFromRevision(srcCFRevision);
                        dstEntry.setCopyFromURL(srcCFURL);
                        dstEntry.setKind(SVNNodeKind.FILE);
                        dstEntry.setRevision(srcRevision);
                        dstEntry.setCopied(true);
                    } else if (!srcEntry.isCopied() && !srcEntry.isScheduledForAddition()) {
                        dstEntry.setCopied(true);
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.FILE);
                        dstEntry.setCopyFromRevision(srcRevision);
                        dstEntry.setCopyFromURL(srcURL);
                    } else {
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.FILE);
                        if (!dstEntry.isScheduledForReplacement()) {
                            dstEntry.setRevision(0);
                        }
                    }
                    dstParentArea.saveEntries(false);
                } else if (srcEntry.isDirectory()) {
                    SVNAdminArea srcArea = wcAccess.open(src, false, 0);
                    srcEntry = srcArea.getEntry(srcArea.getThisDirName(), false);
                    if (dstEntry == null) {
                        dstEntry = dstParentArea.addEntry(dst.getName());
                    }
                    String srcURL = srcEntry.getURL();
                    String dstURL = dstParentEntry.getURL();
                    long srcRevision = srcEntry.getRevision();
                    String repositoryRootURL = srcEntry.getRepositoryRoot();
                    dstURL = SVNPathUtil.append(dstURL, SVNEncodingUtil.uriEncode(dst.getName()));
                    SVNAdminArea dstArea = wcAccess.open(dst, true, SVNWCAccess.INFINITE_DEPTH);
                    if (srcEntry.isScheduledForAddition() && srcEntry.isCopied()) {
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.DIR);
                        dstParentArea.saveEntries(true);
                        dstArea.updateURL(dstURL, true);
                        dstArea.saveEntries(true);
                    } else if (!srcEntry.isCopied() && !srcEntry.isScheduledForAddition()) {
                        dstEntry.setCopied(true);
                        dstEntry.scheduleForAddition();
                        dstEntry.setKind(SVNNodeKind.DIR);
                        dstEntry.setCopyFromRevision(srcRevision);
                        dstEntry.setCopyFromURL(srcURL);
                        dstParentArea.saveEntries(true);
                        SVNEntry dstThisEntry = dstArea.getEntry(dstArea.getThisDirName(), false);
                        dstThisEntry.setCopied(true);
                        dstThisEntry.scheduleForAddition();
                        dstThisEntry.setKind(SVNNodeKind.DIR);
                        dstThisEntry.setCopyFromRevision(srcRevision);
                        dstThisEntry.setURL(dstURL);
                        dstThisEntry.setCopyFromURL(srcURL);
                        dstThisEntry.setRepositoryRoot(repositoryRootURL);
                        updateCopiedDirectory(dstArea, dstArea.getThisDirName(), dstURL, repositoryRootURL, null, -1);
                        dstArea.saveEntries(true);
                    } else {
                        dstParentArea.deleteEntry(dst.getName());
                        dstParentArea.saveEntries(true);
                        wcAccess.close();
                        SVNFileUtil.deleteAll(dst, this);
                        SVNFileUtil.copy(src, dst, false, false);
                        myWCClient.doAdd(dst, false, false, false, SVNDepth.INFINITY, false, false);
                    }
                }
                try {
                    wcAccess.close();
                    myWCClient.doDelete(src, true, false);
                } catch (SVNException e) {
                }
            } finally {
                wcAccess.close();
            }
        }
    }

    /**
     * Copies/moves administrative version control information of a source file
     * to administrative information of a destination file. For example, if you
     * have manually copied/moved a source file to a target one (manually means
     * just in the filesystem, not using version control operations) and then
     * would like to turn this copying/moving into a complete version control
     * copy or move operation, use this method that will finish all the work for
     * you - it will copy/move all the necessary administrative information
     * (kept in the source <i>.svn</i> directory) to the target <i>.svn</i>
     * directory.
     * <p>
     * In that case when you have your files copied/moved in the filesystem, you
     * can not perform standard (version control) copying/moving - since the
     * target already exists and the source may be already deleted. Use this
     * method to overcome that restriction.
     * 
     * @param src
     *            a source file path (was copied/moved to <code>dst</code>)
     * @param dst
     *            a destination file path
     * @param move
     *            if <span class="javakeyword">true</span> then completes moving
     *            <code>src</code> to <code>dst</code>, otherwise completes
     *            copying <code>src</code> to <code>dst</code>
     * @throws SVNException
     *             if one of the following is true:
     *             <ul>
     *             <li><code>move = </code><span class="javakeyword">true</span>
     *             and <code>src</code> still exists <li><code>dst</code> does
     *             not exist <li><code>dst</code> is a directory <li><code>src
     *             </code> is a directory <li><code>src</code> is not under
     *             version control <li><code>dst</code> is already under version
     *             control <li>if <code>src</code> is copied but not scheduled
     *             for addition, and SVNKit is not able to locate the copied
     *             directory root for <code>src</code>
     *             </ul>
     */
    public void doVirtualCopy(File src, File dst, boolean move) throws SVNException {
        SVNFileType srcType = SVNFileType.getType(src);
        SVNFileType dstType = SVNFileType.getType(dst);
        String opName = move ? "move" : "copy";
        if (move && srcType != SVNFileType.NONE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "Cannot perform 'virtual' {0}: ''{1}'' still exists", new Object[] {
                    opName, src
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (dstType == SVNFileType.NONE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "Cannot perform 'virtual' {0}: ''{1}'' does not exist", new Object[] {
                    opName, dst
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (dstType == SVNFileType.DIRECTORY) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, "Cannot perform 'virtual' {0}: ''{1}'' is a directory", new Object[] {
                    opName, dst
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (!move && srcType == SVNFileType.DIRECTORY) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, "Cannot perform 'virtual' {0}: ''{1}'' is a directory", new Object[] {
                    opName, src
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        SVNURL srcRepoRoot = null;
        SVNURL dstRepoRoot = null;
        boolean versionedDst = false;
        SVNWCAccess dstAccess = createWCAccess();
        try {
            dstAccess.probeOpen(dst, false, 0);
            SVNEntry dstEntry = dstAccess.getEntry(dst, false);
            if (dstEntry != null) {
                if (!dstEntry.isScheduledForAddition() && !dstEntry.isScheduledForReplacement()) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_ATTRIBUTE_INVALID, "Cannot perform 'virtual' {0}: ''{1}'' is scheduled neither for addition nor for replacement",
                            new Object[] {
                                    opName, dst
                            });
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                versionedDst = true;
                dstRepoRoot = dstEntry.getRepositoryRootURL();
            }
        } finally {
            dstAccess.close();
        }
        SVNWCAccess srcAccess = createWCAccess();
        String cfURL = null;
        boolean added = false;
        long cfRevision = -1;
        try {
            srcAccess.probeOpen(src, false, 0);
            SVNEntry srcEntry = srcAccess.getEntry(src, false);
            if (srcEntry == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "''{0}'' is not under version control", src);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            srcRepoRoot = srcEntry.getRepositoryRootURL();
            if (isCopiedAsAChild(src, srcEntry)) {
                cfURL = getCopyFromURL(src.getParentFile(), SVNEncodingUtil.uriEncode(src.getName()));
                cfRevision = getCopyFromRevision(src.getParentFile());
                if (cfURL == null || cfRevision < 0) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "Cannot locate copied directory root for ''{0}''", src);
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
                added = false;
            } else {
                cfURL = srcEntry.isCopied() ? srcEntry.getCopyFromURL() : srcEntry.getURL();
                cfRevision = srcEntry.isCopied() ? srcEntry.getCopyFromRevision() : srcEntry.getRevision();
                added = srcEntry.isScheduledForAddition() && !srcEntry.isCopied();
            }
        } finally {
            srcAccess.close();
        }
        if (added && !versionedDst) {
            if (move) {
                myWCClient.doDelete(src, true, false);
            }
            myWCClient.doAdd(dst, true, false, false, SVNDepth.EMPTY, false, false);
            return;
        }
        dstAccess = createWCAccess();
        srcAccess = createWCAccess();
        try {
            SVNAdminArea dstArea = dstAccess.probeOpen(dst, true, 0);
            SVNEntry dstEntry = dstAccess.getEntry(dst, false);
            if (dstEntry != null && !dstEntry.isScheduledForAddition() && !dstEntry.isScheduledForReplacement()) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_ATTRIBUTE_INVALID, "Cannot perform 'virtual' {0}: ''{1}'' is scheduled neither for addition nor for replacement",
                        new Object[] {
                                opName, dst
                        });
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            if (srcRepoRoot != null && dstRepoRoot != null && !dstRepoRoot.equals(srcRepoRoot)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_ATTRIBUTE_INVALID, "Cannot perform 'virtual' {0}: paths belong to different repositories", opName);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            SVNAdminArea srcArea = srcAccess.probeOpen(src, false, 0);
            SVNVersionedProperties srcProps = srcArea.getProperties(src.getName());
            SVNVersionedProperties srcBaseProps = srcArea.getBaseProperties(src.getName());
            SVNVersionedProperties dstProps = dstArea.getProperties(dst.getName());
            SVNVersionedProperties dstBaseProps = dstArea.getBaseProperties(dst.getName());
            dstBaseProps.removeAll();
            srcProps.copyTo(dstProps);
            srcBaseProps.copyTo(dstBaseProps);
            dstEntry = dstArea.addEntry(dst.getName());
            dstEntry.setCopyFromURL(cfURL);
            dstEntry.setCopyFromRevision(cfRevision);
            dstEntry.setCopied(true);
            dstEntry.setKind(SVNNodeKind.FILE);
            File baseSrc = srcArea.getBaseFile(src.getName(), false);
            File baseDst = dstArea.getBaseFile(dst.getName(), false);
            SVNFileUtil.copyFile(baseSrc, baseDst, false);
            if (dstEntry.isScheduledForDeletion()) {
                dstEntry.unschedule();
                dstEntry.scheduleForReplacement();
            } else if (!dstEntry.isScheduledForReplacement()) {
                dstEntry.unschedule();
                dstEntry.scheduleForAddition();
            }
            dstArea.saveEntries(false);
            SVNLog log = dstArea.getLog();
            dstArea.saveVersionedProperties(log, true);
            log.save();
            dstArea.runLogs();
            SVNEvent event = SVNEventFactory.createSVNEvent(dst, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.COPY, null, null, null);
            dispatchEvent(event);
        } finally {
            srcAccess.close();
            dstAccess.close();
        }
        if (move) {
            myWCClient.doDelete(src, true, false);
        }
    }
    
    private boolean isCopiedAsAChild(File path, SVNEntry entry) throws SVNException {
        if (!entry.isScheduledForAddition() && entry.isCopied()) {
            return true;
        }
        if (entry.isScheduledForDeletion() && path != null) {
            return getCopyFromURL(path.getParentFile(), SVNEncodingUtil.uriEncode(entry.getName())) != null;
        }
        return false;
    }

    private void updateCopiedDirectory(SVNAdminArea dir, String name, String newURL, String reposRootURL, String copyFromURL, long copyFromRevision) throws SVNException {
        SVNWCAccess wcAccess = dir.getWCAccess();
        SVNEntry entry = dir.getEntry(name, true);
        if (entry != null) {
            entry.setCopied(true);
            if (newURL != null) {
                entry.setURL(newURL);
            }
            entry.setRepositoryRoot(reposRootURL);
            if (entry.isFile()) {
                if (dir.getWCProperties(name) != null) {
                    dir.getWCProperties(name).removeAll();
                    dir.saveWCProperties(false);
                }
                if (copyFromURL != null) {
                    entry.setCopyFromURL(copyFromURL);
                    entry.setCopyFromRevision(copyFromRevision);
                }
            }
            boolean deleted = false;
            if (entry.isDeleted() && newURL != null) {
                deleted = true;
                entry.setDeleted(false);
                entry.scheduleForDeletion();
                if (entry.isDirectory()) {
                    entry.setKind(SVNNodeKind.FILE);
                }
            }
            if (entry.getLockToken() != null && newURL != null) {
                entry.setLockToken(null);
                entry.setLockOwner(null);
                entry.setLockComment(null);
                entry.setLockCreationDate(null);
            }
            if (!dir.getThisDirName().equals(name) && entry.isDirectory() && !deleted) {
                SVNAdminArea childDir = wcAccess.retrieve(dir.getFile(name));
                if (childDir != null) {
                    String childCopyFromURL = copyFromURL == null ? null : SVNPathUtil.append(copyFromURL, SVNEncodingUtil.uriEncode(entry.getName()));
                    updateCopiedDirectory(childDir, childDir.getThisDirName(), newURL, reposRootURL, childCopyFromURL, copyFromRevision);
                }
            } else if (dir.getThisDirName().equals(name)) {
                dir.getWCProperties(dir.getThisDirName()).removeAll();
                dir.saveWCProperties(false);
                if (copyFromURL != null) {
                    entry.setCopyFromURL(copyFromURL);
                    entry.setCopyFromRevision(copyFromRevision);
                }
                for (Iterator ents = dir.entries(true); ents.hasNext();) {
                    SVNEntry childEntry = (SVNEntry) ents.next();
                    if (dir.getThisDirName().equals(childEntry.getName())) {
                        continue;
                    }
                    String childCopyFromURL = copyFromURL == null ? null : SVNPathUtil.append(copyFromURL, SVNEncodingUtil.uriEncode(childEntry.getName()));
                    String newChildURL = newURL == null ? null : SVNPathUtil.append(newURL, SVNEncodingUtil.uriEncode(childEntry.getName()));
                    updateCopiedDirectory(dir, childEntry.getName(), newChildURL, reposRootURL, childCopyFromURL, copyFromRevision);
                }
                dir.saveEntries(false);
            }
        }
    }

    private String getCopyFromURL(File path, String urlTail) throws SVNException {
        if (path == null) {
            return null;
        }
        SVNWCAccess wcAccess = createWCAccess();
        try {
            wcAccess.probeOpen(path, false, 0);
        } catch (SVNException e) {
            wcAccess.close();
            return null;
        }
        try {
            SVNEntry entry = wcAccess.getEntry(path, false);
            if (entry == null) {
                return null;
            }
            String cfURL = entry.getCopyFromURL();
            if (cfURL != null) {
                return SVNPathUtil.append(cfURL, urlTail);
            }
            urlTail = SVNPathUtil.append(SVNEncodingUtil.uriEncode(path.getName()), urlTail);
            path = path.getParentFile();
            return getCopyFromURL(path, urlTail);
        } finally {
            wcAccess.close();
        }
    }

    private long getCopyFromRevision(File path) throws SVNException {
        if (path == null) {
            return -1;
        }
        SVNWCAccess wcAccess = createWCAccess();
        try {
            wcAccess.probeOpen(path, false, 0);
        } catch (SVNException e) {
            wcAccess.close();
            return -1;
        }
        try {
            SVNEntry entry = wcAccess.getEntry(path, false);
            if (entry == null) {
                return -1;
            }
            long rev = entry.getCopyFromRevision();
            if (rev >= 0) {
                return rev;
            }
            path = path.getParentFile();
            return getCopyFromRevision(path);
        } finally {
            wcAccess.close();
        }
    }

    private static boolean isVersionedFile(File file) {
        SVNWCAccess wcAccess = SVNWCAccess.newInstance(null);
        try {
            SVNAdminArea area = wcAccess.probeOpen(file, false, 0);
            if (area.getEntry(area.getThisDirName(), false) == null) {
                return false;
            }
            SVNFileType type = SVNFileType.getType(file);
            if (type.isFile() || type == SVNFileType.NONE) {
                return area.getEntry(file.getName(), false) != null;
            } else if (type != SVNFileType.NONE && !area.getRoot().equals(file)) {
                return false;
            }
            return true;
        } catch (SVNException e) {
            return false;
        } finally {
            try {
                wcAccess.close();
            } catch (SVNException svne) {
            }
        }
    }
}
