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

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

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.wc.SVNExternalsUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNSkel;
import org.tmatesoft.svn.core.internal.wc.*;
import org.tmatesoft.svn.core.internal.wc17.SVNStatusEditor17;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.SVNWCUtils;
import org.tmatesoft.svn.core.internal.wc17.db.*;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbKind;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbStatus;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbRepositoryInfo;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbRepositoryInfo.RepositoryInfoField;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.AdditionInfo;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.ExternalNodeInfo;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.NodeInfo;
import org.tmatesoft.svn.core.internal.wc2.SvnWcGeneration;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.*;
import org.tmatesoft.svn.core.wc2.ISvnObjectReceiver;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.core.wc2.SvnCopy;
import org.tmatesoft.svn.core.wc2.SvnCopySource;
import org.tmatesoft.svn.core.wc2.SvnGetStatus;
import org.tmatesoft.svn.core.wc2.SvnOperationFactory;
import org.tmatesoft.svn.core.wc2.SvnScheduleForAddition;
import org.tmatesoft.svn.core.wc2.SvnStatus;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNLogType;

public class SvnNgWcToWcCopy extends SvnNgOperationRunner<Void, SvnCopy> {

    @Override
    public boolean isApplicable(SvnCopy operation, SvnWcGeneration wcGeneration) throws SVNException {
        return areAllSourcesLocal(operation) && operation.getFirstTarget().isLocal();
    }
    
    private boolean areAllSourcesLocal(SvnCopy operation) {
        for(SvnCopySource source : operation.getSources()) {
            if (source.getSource().isFile()) {
                if (operation.isMove()) {
                    continue;
                }
                if (isLocalRevision(source.getRevision()) && isLocalRevision(source.getSource().getResolvedPegRevision())) {
                    continue;
                }
            }
            return false;
        }
        return true;
    }
    
    private boolean isLocalRevision(SVNRevision revision) {
        return revision == SVNRevision.WORKING || revision == SVNRevision.UNDEFINED;
    }

    @Override
    protected Void run(SVNWCContext context) throws SVNException {
        boolean timestampSleep = false;

        Collection<SvnCopySource> sources = getOperation().getSources();
        try {
            timestampSleep = tryRun(context, sources, getFirstTarget());
            return null;
        } catch (SVNException e) {
            SVNErrorCode code = e.getErrorMessage().getErrorCode();
            if (!getOperation().isFailWhenDstExists()
                    && getOperation().getSources().size() == 1 
                    && (code == SVNErrorCode.ENTRY_EXISTS || code == SVNErrorCode.FS_ALREADY_EXISTS)) {
                SvnCopySource source = sources.iterator().next();
                timestampSleep = tryRun(context, sources, new File(getFirstTarget(), source.getSource().getFile().getName()));
                return null;
            }
            throw e;            
        } finally {
            if (timestampSleep) {
                sleepForTimestamp();
            }
        }        
    }

    protected boolean tryRun(SVNWCContext context, Collection<SvnCopySource> sources, File target) throws SVNException {
        if (getOperation().isDisjoint()) {
            return disjointCopy(context, target);
        } else {
            return copy(context, sources, target);
        }
    }

    /**
     * The method performs "disjoint" copy (see SVNCopyClient#doCopy(File))
     * The algorithm is:
     * 1. Create a fake working copy
     * 2. Move wc.db from the nested working copy to the fake
     * 3. Move all pristine files to the parent working copy
     * 4. Perform metadata copying
     * @param context
     * @param nestedWC
     * @return
     * @throws SVNException
     */
    private boolean disjointCopy(SVNWCContext context, File nestedWC) throws SVNException {
        nestedWC = new File(nestedWC.getAbsolutePath().replace(File.separatorChar, '/'));
        final File nestedWCParent = nestedWC.getParentFile();

        checkForDisjointCopyPossibility(context, nestedWC, nestedWCParent);
        context.getDb().close();

        final File wcRoot = context.getDb().getWCRoot(nestedWCParent);

        final File tempDirectory = new File(getAdminDirectory(wcRoot), "tmp");
        SVNFileUtil.ensureDirectoryExists(tempDirectory);
        final File fakeWorkingCopyDirectory = SVNFileUtil.createUniqueDir(tempDirectory, "disjoint-copy", "tmp", true);
        
        moveWcDb(nestedWC, fakeWorkingCopyDirectory);
        copyPristineFiles(nestedWC, wcRoot, true);
        SVNFileUtil.deleteAll(getAdminDirectory(nestedWC), true);
        context.getDb().forgetDirectoryTemp(nestedWC);

        File lockRoot = null;
        try {
            lockRoot = context.acquireWriteLock(wcRoot, true, true);
            copy(context, fakeWorkingCopyDirectory, nestedWC, true);
        } finally {
            if (lockRoot != null) {
                context.releaseWriteLock(lockRoot);
            }
        }

        return true;
    }

    private void checkForDisjointCopyPossibility(SVNWCContext context, File nestedWC, File nestedWCParent) throws SVNException {
        SVNFileType nestedWCType = SVNFileType.getType(nestedWC);

        if (nestedWCType != SVNFileType.DIRECTORY) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                    "This kind of copy can be run on a root of a disjoint wc directory only");
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (nestedWCParent == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                    "{0} seems to be not a disjoint wc since it has no parent", nestedWC);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (!(context.getDb() instanceof SVNWCDb)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                    "Unsupported working copy format", nestedWC);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        SVNWCDb wcdb = (SVNWCDb) context.getDb();

        if (hasMetadataInParentWc(wcdb, nestedWC, nestedWCParent)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                    "Entry ''{0}'' already exists in parent directory", nestedWC.getName());
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        final ISVNWCDb.WCDbBaseInfo baseInfo = context.getDb().getBaseInfo(nestedWC,
                ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRootUrl, ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRelPath);

        final ISVNWCDb.WCDbBaseInfo parentBaseInfo = context.getDb().getBaseInfo(nestedWCParent,
                ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRootUrl, ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRelPath);

        if (baseInfo.reposRootUrl != null && parentBaseInfo.reposRootUrl != null &&
                !baseInfo.reposRootUrl.equals(parentBaseInfo.reposRootUrl)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                    "Cannot copy to ''{0}'', as it is not from repository ''{1}''; it is from ''{2}''",
                    new Object[] { nestedWCParent, baseInfo.reposRootUrl,
                    baseInfo.reposRootUrl });
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        SVNWCContext.ScheduleInternalInfo parentSchedule = context.getNodeScheduleInternal(nestedWCParent, true, true);

        if (parentSchedule.schedule == SVNWCContext.SVNWCSchedule.delete) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                    "Cannot copy to ''{0}'', as it is scheduled for deletion", nestedWCParent);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        SVNWCContext.ScheduleInternalInfo schedule = context.getNodeScheduleInternal(nestedWC, true, true);

        if (schedule.schedule == SVNWCContext.SVNWCSchedule.delete) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                    "Cannot copy ''{0}'', as it is scheduled for deletion", nestedWC);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        File relativePath = baseInfo.reposRelPath;
        File parentRelativePath = parentBaseInfo.reposRelPath;

        if (relativePath == null || parentRelativePath == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                    "Cannot copy ''{0}'': cannot resolve its relative path, perhaps it is obstructed", nestedWC);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (SVNPathUtil.isAncestor(relativePath.getPath(), parentRelativePath.getPath())) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                    "Cannot copy path ''{0}'' into its own child ''{1}",
                    new Object[] { nestedWC, nestedWCParent });
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if ((schedule.schedule == SVNWCContext.SVNWCSchedule.add && !schedule.copied) ||
                context.getNodeUrl(nestedWC) == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                    "Cannot copy or move ''{0}'': it is not in repository yet; " +
                    "try committing first", nestedWC);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
    }

    private void moveWcDb(File sourceWc, File targetWc) throws SVNException {
        final File sourceWcDbFile = getWCDbFile(sourceWc);
        final File targetWcDbFile = getWCDbFile(targetWc);

        SVNFileUtil.ensureDirectoryExists(targetWcDbFile.getParentFile());
        SVNFileUtil.rename(sourceWcDbFile, targetWcDbFile);
    }

    private void copyPristineFiles(File sourceWc, File targetWc, boolean move) throws SVNException {
        final File sourcePristineDirectory = getPristineDirectory(sourceWc);
        final File targetPristineDirectory = getPristineDirectory(targetWc);

        final File[] sourceDirectories = SVNFileListUtil.listFiles(sourcePristineDirectory);
        if (sourceDirectories != null) {
            for (File sourceDirectory : sourceDirectories) {
                final File targetDirectory = new File(targetPristineDirectory, sourceDirectory.getName());
                SVNFileUtil.ensureDirectoryExists(targetDirectory);
                final File[] sourcePristineFiles = SVNFileListUtil.listFiles(sourceDirectory);
                if (sourcePristineFiles != null) {
                    for (File sourcePristineFile : sourcePristineFiles) {
                        final File targetPristineFile = new File(targetDirectory, sourcePristineFile.getName());
                        if (!targetPristineFile.exists()) {
                            if (move) {
                                SVNFileUtil.rename(sourcePristineFile, targetPristineFile);
                            } else {
                                SVNFileUtil.copyFile(sourcePristineFile, targetPristineFile, false);
                            }
                        }
                    }
                }
            }
        }

        SVNFileUtil.deleteAll(sourcePristineDirectory, true);
    }

    private File getPristineDirectory(File workingCopyDirectory) {
        return new File(getAdminDirectory(workingCopyDirectory), "pristine");
    }

    private File getWCDbFile(File nestedWC) {
        return new File(getAdminDirectory(nestedWC), "wc.db");
    }

    private File getAdminDirectory(File parentWC) {
        final String adminDirectoryName = SVNFileUtil.getAdminDirectoryName();
        return new File(parentWC, adminDirectoryName);
    }

    private boolean hasMetadataInParentWc(SVNWCDb wcdb, File nestedWC, File nestedWCParent) throws SVNException {
        SVNWCDb.DirParsedInfo parsedInfo = wcdb.obtainWcRoot(nestedWCParent);
        SVNWCDbDir wcDbDir = parsedInfo == null ? null : parsedInfo.wcDbDir;
        SVNWCDbRoot wcdbRoot = wcDbDir == null ? null : wcDbDir.getWCRoot();

        if (wcdbRoot == null) {
            return false;
        }

        try {
            wcdb.readInfo(wcdbRoot, new File(SVNPathUtil.getRelativePath(nestedWCParent.getPath(),nestedWC.getPath())));
            return true;
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() == SVNErrorCode.ENTRY_NOT_FOUND || e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND) {
                return false;
            } else {
                throw e;
            }
        }
    }

    private boolean copy(SVNWCContext context, Collection<SvnCopySource> sources, File target) throws SVNException {
        boolean sleepForTimeStamp = false;
        Collection<SvnCopyPair> copyPairs = new ArrayList<SvnCopyPair>();

        if (sources.size() > 1) {
            if (getOperation().isFailWhenDstExists()) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_MULTIPLE_SOURCES_DISALLOWED);
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }
            for (SvnCopySource copySource : sources) {
                SvnCopyPair copyPair = new SvnCopyPair();
                copyPair.source = copySource.getSource().getFile();
                String baseName = copyPair.source.getName();
                copyPair.dst = new File(target, baseName);
                copyPairs.add(copyPair);
            }
        } else if (sources.size() == 1) {
            SvnCopyPair copyPair = new SvnCopyPair();
            SvnCopySource source = sources.iterator().next(); 
            copyPair.source = new File(SVNPathUtil.validateFilePath(source.getSource().getFile().getAbsolutePath()));
            copyPair.dst = target;
            copyPairs.add(copyPair);
        }
        
        for (SvnCopyPair pair : copyPairs) {
            File src = pair.source;
            File dst = pair.dst;
            if (getOperation().isMove() && src.getAbsolutePath().equals(dst.getAbsolutePath())) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, 
                        "Cannot move path ''{0}'' into itself", src);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            if (SVNWCUtils.isChild(src, dst)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Cannot copy path ''{0}'' into its own child ''{1}''",
                    src, dst);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        if (getOperation().isMove()) {
            for (SvnCopyPair pair : copyPairs) {
                File src = pair.source;
                try {
                    Structure<ExternalNodeInfo> externalInfo = SvnWcDbExternals.readExternal(context, src, src, ExternalNodeInfo.kind, ExternalNodeInfo.definingAbsPath);
                    if (externalInfo.hasValue(ExternalNodeInfo.kind) && externalInfo.get(ExternalNodeInfo.kind) != SVNNodeKind.NONE) {
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CANNOT_MOVE_FILE_EXTERNAL, 
                                "Cannot move the external at ''{0}''; please edit the svn:externals property on ''{1}''.", src, externalInfo.get(ExternalNodeInfo.definingAbsPath));
                        SVNErrorManager.error(err, SVNLogType.WC);
                    }
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                        throw e;
                    }
                }
            }
        } 
        verifyPaths(copyPairs, getOperation().isMakeParents(), getOperation().isMove());
        if (getOperation().isMove()) {
            sleepForTimeStamp = move(copyPairs);
        } else {
            File dstAncestor = null;
            if (copyPairs.size() >= 1) {
                dstAncestor = copyPairs.iterator().next().dst;
            } 
            if (copyPairs.size() > 1 && dstAncestor != null) {
                dstAncestor = SVNFileUtil.getParentFile(dstAncestor);
            }
            dstAncestor = dstAncestor.getAbsoluteFile();
            dstAncestor = context.acquireWriteLock(dstAncestor, false, true);
            try {
                for (SvnCopyPair copyPair : copyPairs) {
                    Map<String, SVNPropertyValue> pinnedExternals = null;

                    checkCancelled();
                    if (getOperation().isPinExternals()) {
                        final Structure<StructureFields.NodeOriginInfo> nodeOrigin = getWcContext().getNodeOrigin(copyPair.source, false, StructureFields.NodeOriginInfo.reposRootUrl);
                        final SVNURL reposRootUrl = nodeOrigin.get(StructureFields.NodeOriginInfo.reposRootUrl);
                        pinnedExternals = resolvePinnedExternals(getOperation().getExternalsToPin(), copyPair, null, reposRootUrl);
                    }

                    File dstPath = SVNFileUtil.createFilePath(copyPair.dstParent, copyPair.baseName);
                    sleepForTimeStamp = sleepForTimeStamp || copy(context, copyPair.source, dstPath, getOperation().isMetadataOnly() || getOperation().isVirtual());

                    if (pinnedExternals != null) {
                        for (Map.Entry<String, SVNPropertyValue> entry : pinnedExternals.entrySet()) {
                            String dstRelPath = entry.getKey();
                            SVNPropertyValue externalsPropval = entry.getValue();

                            File localAbsPath = SVNFileUtil.createFilePath(copyPair.dst, dstRelPath);
                            SvnNgPropertiesManager.setProperty(getWcContext(), localAbsPath, SVNProperty.EXTERNALS, externalsPropval, SVNDepth.EMPTY, true, null, null);
                        }
                    }
                }
            } finally {
                context.releaseWriteLock(dstAncestor);
            }
        }
        
        return false;
    }

    private Map<String, SVNPropertyValue> resolvePinnedExternals(Map<SvnTarget, List<SVNExternal>> externalsToPin, SvnCopyPair copyPair, SVNRepository svnRepository, SVNURL reposRootUrl) throws SVNException {
        return SVNExternalsUtil.resolvePinnedExternals(getWcContext(), getRepositoryAccess(), externalsToPin, SvnTarget.fromFile(copyPair.source), SvnTarget.fromFile(copyPair.dst), -1, svnRepository, reposRootUrl);
    }

    private boolean move(Collection<SvnCopyPair> pairs) throws SVNException {
        for (SvnCopyPair copyPair : pairs) {
            Collection<File> lockPaths = new HashSet<File>();
            Collection<File> lockedPaths = new HashSet<File>();
            
            checkCancelled();
            File sourceParent = new File(SVNPathUtil.validateFilePath(SVNFileUtil.getParentFile(copyPair.source).getAbsolutePath()));
            if (sourceParent.equals(copyPair.dstParent) ||
                    SVNWCUtils.isChild(sourceParent, copyPair.dstParent)) {
                lockPaths.add(sourceParent);
            } else if (SVNWCUtils.isChild(copyPair.dstParent, sourceParent)) {
                lockPaths.add(copyPair.dstParent);
            } else {
                lockPaths.add(sourceParent);
                lockPaths.add(copyPair.dstParent);
            }
            try {
                for (File file : lockPaths) {
                    lockedPaths.add(getWcContext().acquireWriteLock(file, false, true));
                }
                
                move(getWcContext(), copyPair.source, SVNFileUtil.createFilePath(copyPair.dstParent, copyPair.baseName), getOperation().isMetadataOnly() || getOperation().isVirtual());
            } finally {
                for (File file : lockedPaths) {
                    getWcContext().releaseWriteLock(file);
                }
            }
        }
        return false;
    }

    private void verifyPaths(Collection<SvnCopyPair> copyPairs, boolean makeParents, boolean move) throws SVNException {
        for (SvnCopyPair copyPair : copyPairs) {
            SVNNodeKind dstKind = SvnWcDbCopy.readKind(getWcContext().getDb(), copyPair.dst, false, true);
            if (dstKind != SVNNodeKind.NONE) {
                SVNWCContext.NodePresence nodePresence = getWcContext().getNodePresence(copyPair.dst, false);
                if (nodePresence.isExcluded || nodePresence.isServerExcluded) {
                    SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_OBSTRUCTED_UPDATE, "Path ''{0}'' exists, but is excluded", copyPair.dst);
                    SVNErrorManager.error(errorMessage, SVNLogType.WC);
                } else {
                    SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "Path ''{0}'' already exists", copyPair.dst);
                    SVNErrorManager.error(errorMessage, SVNLogType.WC);
                }
            }

            SVNFileType srcType = SVNFileType.getType(copyPair.source);
            SVNFileType dstType = SVNFileType.getType(copyPair.dst);

            if (!getOperation().isMetadataOnly()) {
                if (getOperation().isVirtual()) {
                    verifyPathsExistenceForVirtualCopy(copyPair.source, copyPair.dst, srcType, dstType, copyPair, move);
                } else {
                    final boolean caseOnlyRename = verifyPaths(srcType, dstType, copyPair, copyPairs.size(), move);
                    if (caseOnlyRename) {
                        return;
                    }
                }
            }
            copyPair.dstParent = new File(SVNPathUtil.validateFilePath(SVNFileUtil.getParentFile(copyPair.dst).getAbsolutePath()));
            copyPair.baseName = SVNFileUtil.getFileName(copyPair.dst);

            SVNNodeKind dstParentKind = SvnWcDbCopy.readKind(getWcContext().getDb(), copyPair.dstParent, false, true);
            if (makeParents && (dstParentKind == SVNNodeKind.NONE || getOperation().isVirtual())) {
                SVNFileUtil.ensureDirectoryExists(copyPair.dstParent);
                
                SvnScheduleForAddition add = getOperation().getOperationFactory().createScheduleForAddition();
                add.setSingleTarget(SvnTarget.fromFile(copyPair.dstParent));
                add.setDepth(getOperation().isVirtual() ? SVNDepth.EMPTY : SVNDepth.INFINITY);
                add.setIncludeIgnored(true);
                add.setForce(false);
                add.setAddParents(true);
                add.setSleepForTimestamp(false);
                
                try {
                    add.run();
                } catch (SVNException e) {
                    if (dstParentKind == SVNNodeKind.NONE) {
                        SVNFileUtil.deleteAll(copyPair.dstParent, true);
                    }
                    throw e;
                }
            } else if (dstParentKind != SVNNodeKind.DIR) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_PATH_NOT_FOUND, "Path ''{0}'' is not a directory", copyPair.dstParent);
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            dstParentKind = SVNFileType.getNodeKind(SVNFileType.getType(copyPair.dstParent));

            if (dstParentKind != SVNNodeKind.DIR) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_MISSING, "Path ''{0}'' is not a directory", copyPair.dstParent);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
    }

    private boolean verifyPaths(SVNFileType srcType, SVNFileType dstType, SvnCopyPair copyPair, int copyPairsCount, boolean move) throws SVNException {
        if (srcType == SVNFileType.NONE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.NODE_UNKNOWN_KIND, "Path ''{0}'' does not exist", copyPair.source);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (dstType != SVNFileType.NONE) {
            if (move && copyPairsCount == 1) {
                File srcDir = SVNFileUtil.getFileDir(copyPair.source);
                File dstDir = SVNFileUtil.getFileDir(copyPair.dst);
                if (srcDir.equals(dstDir)) {
                    // check if it is case-only rename
                    if (copyPair.source.getName().equalsIgnoreCase(copyPair.dst.getName())) {
                        copyPair.dstParent = new File(SVNPathUtil.validateFilePath(dstDir.getAbsolutePath()));
                        copyPair.baseName = SVNFileUtil.getFileName(copyPair.dst);
                        return true;
                    }
                }

            }
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, "Path ''{0}'' already exists", copyPair.dst);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return false;
    }

    private void verifyPathsExistenceForVirtualCopy(File source, File dst, SVNFileType srcType, SVNFileType dstType, SvnCopyPair copyPair, boolean move) throws SVNException {
        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, copyPair.source
            });
            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, copyPair.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, copyPair.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, copyPair.source
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        final SvnStatus dstStatus = getStatus(dst);
        if (dstStatus != null &&
                (dstStatus.getNodeStatus() != SVNStatusType.STATUS_UNVERSIONED &&
                        dstStatus.getNodeStatus() != SVNStatusType.STATUS_ADDED &&
                        dstStatus.getNodeStatus() != SVNStatusType.STATUS_REPLACED)) {
            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);
        }

        final SvnStatus sourceStatus = getStatus(source);
        if (sourceStatus == null || sourceStatus.getNodeStatus() == SVNStatusType.STATUS_UNVERSIONED)  {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "''{0}'' is not under version control", source);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
    }

    private SvnStatus getStatus(File interestingFile) throws SVNException {
        final String interestingPath = SVNFileUtil.getFilePath(interestingFile);

        final SvnStatus[] status2 = new SvnStatus[1];

        final SvnOperationFactory operationFactory = getOperation().getOperationFactory();
        final SvnGetStatus status = operationFactory.createGetStatus();
        status.setDepth(SVNDepth.INFINITY);
        status.setRemote(false);
        status.setReportAll(true);
        status.setReportIgnored(true);
        status.setReportExternals(false);
        status.setApplicalbeChangelists(null);
        status.addTarget(SvnTarget.fromFile(interestingFile.getParentFile()));
        status.setReceiver(new ISvnObjectReceiver<SvnStatus>() {
            public void receive(SvnTarget target, SvnStatus svnStatus) throws SVNException {
                if (svnStatus == null) {
                    return;
                }
                final File path = svnStatus.getPath();
                if (path == null) {
                    return;
                }
                final String statusPath = SVNFileUtil.getFilePath(path);
                if (statusPath.equals(interestingPath)) {
                    status2[0] = svnStatus;
                }
            }
        });
        status.run();
        return status2[0];
    }

    public void move(SVNWCContext context, File source, File dst, boolean metadataOnly) throws SVNException {
        boolean moveDegradedToCopy = copy(context, source, dst, true);
        if (!metadataOnly) {
            SVNFileUtil.rename(source, dst);
        }
        Structure<NodeInfo> nodeInfo = context.getDb().readInfo(source, NodeInfo.kind, NodeInfo.conflicted);
        if (nodeInfo.get(NodeInfo.kind) == SVNWCDbKind.Dir) {
            removeAllConflictMarkers(context.getDb(), source, dst);
        }
        if (nodeInfo.is(NodeInfo.conflicted)) {
            removeAllConflictMarkers(context.getDb(), source, dst);
        }
        SvnNgRemove.delete(getWcContext(), source, moveDegradedToCopy ? null : dst, true, false, this);
    }

    private void removeAllConflictMarkers(ISVNWCDb db, File srcDirAbsPath, File wcDirAbsPath) throws SVNException {
        Map<String, ISVNWCDb.SVNWCDbInfo> children = new HashMap<String, ISVNWCDb.SVNWCDbInfo>();
        Set<String> conflicts = new HashSet<String>();
        db.readChildren(srcDirAbsPath, children, conflicts);

        for (Map.Entry<String, ISVNWCDb.SVNWCDbInfo> entry : children.entrySet()) {
            String name = entry.getKey();
            ISVNWCDb.SVNWCDbInfo info = entry.getValue();

            if (info.conflicted) {
                removeNodeConflictMarkers(db, SVNFileUtil.createFilePath(srcDirAbsPath, name), SVNFileUtil.createFilePath(wcDirAbsPath, name));
            }
            if (info.kind == SVNWCDbKind.Dir) {
                removeAllConflictMarkers(db, SVNFileUtil.createFilePath(srcDirAbsPath, name), SVNFileUtil.createFilePath(wcDirAbsPath, name));
            }

        }
    }

    private void removeNodeConflictMarkers(ISVNWCDb db, File srcAbsPath, File nodeAbsPath) throws SVNException {
        SVNSkel conflict = db.readConflict(srcAbsPath);
        if (conflict != null) {
            File srcDir = SVNFileUtil.getParentFile(srcAbsPath);
            File dstDir = SVNFileUtil.getParentFile(nodeAbsPath);

            List<File> markers = SvnWcDbConflicts.readConflictMarkers((SVNWCDb) db, srcAbsPath, conflict);
            if (markers != null) {
                for (File marker : markers) {
                    File childRelPath = SVNFileUtil.skipAncestor(srcDir, marker);
                    if (childRelPath != null) {
                        File childAbsPath = SVNFileUtil.createFilePath(dstDir, childRelPath);
                        SVNFileUtil.deleteFile(childAbsPath);
                    }
                }
            }
        }
    }

    protected boolean copy(SVNWCContext context, File source, File dst, boolean metadataOnly) throws SVNException {
        SvnCopy operation = getOperation();
        boolean isMove = operation != null && operation.isMove();
        boolean allowMixedRevisions = operation == null || operation.isAllowMixedRevisions();

        File dstDirectory = SVNFileUtil.getParentFile(dst);
        
        Structure<NodeInfo> srcInfo = null;
        try {
            srcInfo = context.getDb().readInfo(source, NodeInfo.status, NodeInfo.kind, NodeInfo.reposRelPath,
                    NodeInfo.reposRootUrl, NodeInfo.reposUuid, NodeInfo.checksum, NodeInfo.conflicted);
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "''{0}'' is not under version control", source);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            throw e;
        }

        File srcWcRootAbsPath = context.getDb().getWCRoot(source);

        ISVNWCDb.SVNWCDbStatus srcStatus = srcInfo.get(NodeInfo.status);
        switch (srcStatus) {
        case Deleted:
            if (!metadataOnly) {
                SVNErrorMessage err1 = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS,
                        "Deleted node ''{0}'' can''t be copied.", source);
                SVNErrorManager.error(err1, SVNLogType.WC);
            }
            break;
        case Excluded:
        case ServerExcluded:
        case NotPresent:
            SVNErrorMessage err2 = SVNErrorMessage.create(SVNErrorCode.WC_PATH_NOT_FOUND, 
                    "The node ''{0}'' was not found.", source);
            SVNErrorManager.error(err2, SVNLogType.WC);
        default:
            break;
        }

        if (isMove && source.equals(srcWcRootAbsPath)) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "''{0}'' is the root of a working copy and cannot be moved", source);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }
        File srcReposRelPath = srcInfo.get(NodeInfo.reposRelPath);
        if (isMove && srcReposRelPath != null && "".equals(SVNFileUtil.getFilePath(srcReposRelPath))) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "''{0}'' represents the repository root and cannot be moved", source);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }

        Structure<NodeInfo> dstDirInfo = null;
        try {
            dstDirInfo = context.getDb().
                readInfo(dstDirectory, NodeInfo.status, NodeInfo.reposRootUrl, NodeInfo.reposUuid);
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_PATH_NOT_FOUND) {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "''{0}'' is not under version control", dstDirectory);
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }
        }

        SVNURL dstReposRootUrl = dstDirInfo.get(NodeInfo.reposRootUrl);
        String dstReposUuid = dstDirInfo.get(NodeInfo.reposUuid);
        SVNWCDbStatus dstDirStatus = dstDirInfo.get(NodeInfo.status);
        
        dstDirInfo.release();
        
        SVNURL srcReposRootUrl = srcInfo.get(NodeInfo.reposRootUrl);
        String srcReposUuid = srcInfo.get(NodeInfo.reposUuid);

        File dstWcRootAbsPath = context.getDb().getWCRoot(dstDirectory);

        if (srcReposRootUrl == null) {
            if (srcStatus == SVNWCDbStatus.Added) {
                Structure<AdditionInfo> additionInfo = SvnWcDbShared.scanAddition((SVNWCDb) context.getDb(), source);
                srcReposRootUrl = additionInfo.get(AdditionInfo.reposRootUrl);
                srcReposUuid = additionInfo.get(AdditionInfo.reposUuid);
                additionInfo.release();
            } else {
                WCDbRepositoryInfo reposInfo = context.getDb().scanBaseRepository(source, RepositoryInfoField.rootUrl, RepositoryInfoField.uuid);
                srcReposRootUrl = reposInfo.rootUrl;
                srcReposUuid = reposInfo.uuid;
            }
        }
        
        if (dstReposRootUrl == null) {
            if (dstDirStatus == SVNWCDbStatus.Added) {
                Structure<AdditionInfo> additionInfo = SvnWcDbShared.scanAddition((SVNWCDb) context.getDb(), dstDirectory);
                dstReposRootUrl = additionInfo.get(AdditionInfo.reposRootUrl);
                dstReposUuid = additionInfo.get(AdditionInfo.reposUuid);
                additionInfo.release();
            } else {
                WCDbRepositoryInfo reposInfo = context.getDb().scanBaseRepository(dstDirectory, RepositoryInfoField.rootUrl, RepositoryInfoField.uuid);
                dstReposRootUrl = reposInfo.rootUrl;
                dstReposUuid = reposInfo.uuid;
            }
        }
        
        if ((srcReposRootUrl != null && dstReposRootUrl != null && !srcReposRootUrl.equals(dstReposRootUrl)) || 
                (srcReposUuid != null && dstReposUuid != null && !srcReposUuid.equals(dstReposUuid))) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE, 
                    "Cannot copy to ''{0}'', as it is not from repository ''{1}''; it is from ''{2}''", dst, srcReposRootUrl, dstReposRootUrl);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (dstDirStatus == SVNWCDbStatus.Deleted) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE, 
                    "Cannot copy to ''{0}'', as it is scheduled for deletion", dst);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        
        try {
            Structure<NodeInfo> dstInfo = context.getDb().readInfo(dst, NodeInfo.status);
            SVNWCDbStatus dstStatus = dstInfo.get(NodeInfo.status);
            switch (dstStatus) {
            case Excluded:
                SVNErrorMessage err1 = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, 
                        "''{0}'' is already under version control but is excluded.", dst);
                SVNErrorManager.error(err1, SVNLogType.WC);
                break;
            case ServerExcluded:
                SVNErrorMessage err2 = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, 
                        "''{0}'' is already under version control", dst);
                SVNErrorManager.error(err2, SVNLogType.WC);
                break;
            case Deleted:
            case NotPresent:
                break;
            default:
                if (!metadataOnly) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS,
                            "There is already a versioned item ''{0}''", dst);
                    SVNErrorManager.error(err, SVNLogType.WC);
                    break;
                }
            }
            dstInfo.release();
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                throw e;
            }
        }
        
        if (!metadataOnly) {
            SVNFileType dstType = SVNFileType.getType(dst);
            if (dstType != SVNFileType.NONE) {
                SVNErrorMessage err2 = SVNErrorMessage.create(SVNErrorCode.ENTRY_EXISTS, 
                        "''{0}'' already exists and is in the way", dst);
                SVNErrorManager.error(err2, SVNLogType.WC);
            }
        }
        
        File tmpDir = context.getDb().getWCRootTempDir(dstDirectory);
        boolean withinOneWc = srcWcRootAbsPath.equals(dstWcRootAbsPath);

        boolean moveDegradedToCopy = false;

        if (isMove && !withinOneWc) {
            moveDegradedToCopy = true;
            isMove = false;
        }

        if (!withinOneWc) {
            SvnWcDbPristines.transferPristine((SVNWCDb)context.getDb(), source, dstWcRootAbsPath);
        }

        if (srcInfo.get(NodeInfo.kind) == SVNWCDbKind.File || srcInfo.get(NodeInfo.kind) == SVNWCDbKind.Symlink) {
            final boolean shouldCopyBaseData = shouldCopyBaseData(context, source, metadataOnly, srcStatus);

            if (shouldCopyBaseData && getOperation().isVirtual()) {//we check for "virtual" to preserve current behaviour in this case
                copyBaseDataOfFile(context, source, dst);
            } else {
                copyVersionedFile(context, source, dst, dst, tmpDir, metadataOnly, srcInfo.is(NodeInfo.conflicted), isMove, true);
            }
        } else {
            if (isMove && srcStatus == SVNWCDbStatus.Normal) {
                long[] minMaxRevisions = context.getDb().minMaxRevisions(source, false);
                long minRevision = minMaxRevisions[0];
                long maxRevision = minMaxRevisions[1];
                if (SVNRevision.isValidRevisionNumber(minRevision) && SVNRevision.isValidRevisionNumber(maxRevision) && minRevision != maxRevision) {
                    if (!allowMixedRevisions) {
                        SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_MIXED_REVISIONS,
                                "Cannot move mixed-revision subtree ''{0}'' [{1}:{2}]; try updating it first", source, minRevision, maxRevision);
                        SVNErrorManager.error(errorMessage, SVNLogType.WC);
                    }
                    isMove = false;
                    moveDegradedToCopy = true;
                }
            }
            if (srcStatus == SVNWCDbStatus.Deleted && metadataOnly) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, "Cannot perform 'virtual' {0}: ''{1}'' is a directory", new Object[]{
                        isMove ? "move" : "copy", source
                });
                SVNErrorManager.error(err, SVNLogType.WC);
            } else {
                copyVersionedDirectory(context, source, dst, dst, tmpDir, metadataOnly, isMove, true);
            }
        }

        if (isMove) {
            context.getDb().opHandleMoveBack(dst, source, null);
        }
        context.wqRun(dst);
        return moveDegradedToCopy;
    }

    private boolean shouldCopyBaseData(SVNWCContext context, File source, boolean metadataOnly, SVNWCDbStatus srcStatus) throws SVNException {
        if (!metadataOnly) {
            return false;
        }

        if (srcStatus == SVNWCDbStatus.Deleted) {
            return true;
        }

        final SvnStatus svnStatus = SVNStatusEditor17.internalStatus(context, source);
        return svnStatus != null && svnStatus.getNodeStatus() == SVNStatusType.STATUS_REPLACED;
    }

    private void copyBaseDataOfFile(SVNWCContext context, File source, File dst) throws SVNException {
        final SVNProperties pristineProps = context.getPristineProps(source);

        final ISVNWCDb.WCDbBaseInfo baseInfo = context.getDb().getBaseInfo(source,
                ISVNWCDb.WCDbBaseInfo.BaseInfoField.changedAuthor, ISVNWCDb.WCDbBaseInfo.BaseInfoField.changedDate, ISVNWCDb.WCDbBaseInfo.BaseInfoField.changedRev,
                ISVNWCDb.WCDbBaseInfo.BaseInfoField.checksum, ISVNWCDb.WCDbBaseInfo.BaseInfoField.revision,
                ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRootUrl, ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposUuid);

        final String changedAuthor = baseInfo.changedAuthor;
        final SVNDate changedDate = baseInfo.changedDate;
        final long changedRev = baseInfo.changedRev;
        final SvnChecksum checksum = baseInfo.checksum;
        final long revision = baseInfo.revision;
        final SVNURL reposRootUrl = baseInfo.reposRootUrl;
        final String reposUuid = baseInfo.reposUuid;


        context.getDb().opCopyFile(dst, pristineProps, changedRev, changedDate, changedAuthor,
                context.getNodeReposRelPath(source.getAbsoluteFile()), reposRootUrl, reposUuid, revision, checksum, false, null, null, null);

        final SVNEvent event = SVNEventFactory.createSVNEvent(dst, SVNNodeKind.FILE, null, SVNRepository.INVALID_REVISION, SVNEventAction.COPY, null, null, null);
        handleEvent(event);
    }

    private void copyVersionedDirectory(SVNWCContext wcContext, File srcAbsPath, File dstAbsPath, File dstOpRootAbsPath, File tmpDirAbsPath, boolean metadataOnly, boolean isMove, boolean notify) throws SVNException {
        SVNSkel workItems = null;
        File dirAbsPath = SVNFileUtil.getParentFile(dstAbsPath);
        SVNNodeKind diskKind = SVNNodeKind.UNKNOWN;

        if (!metadataOnly) {
            CopyToTmpDir copyToTmpDir = copyToTmpDir(srcAbsPath, dstAbsPath, tmpDirAbsPath, false, false);
            workItems = copyToTmpDir.workItem;
            diskKind = copyToTmpDir.kind;
        }
        wcContext.getDb().opCopy(srcAbsPath, dstAbsPath, dstOpRootAbsPath, isMove, workItems);

        if (notify && wcContext.getEventHandler() != null) {
            SVNEvent event = SVNEventFactory.createSVNEvent(dstAbsPath, SVNNodeKind.DIR, null, SVNRepository.INVALID_REVISION, SVNEventAction.ADD, SVNEventAction.ADD, null, null);
            if (workItems != null) {
                wcContext.wqRun(dirAbsPath);
            }
            wcContext.getEventHandler().handleEvent(event, UNKNOWN);
        }

        Set<String> diskChildren = null;
        if (!metadataOnly && diskKind == SVNNodeKind.DIR) {
            File[] files = SVNFileListUtil.listFiles(srcAbsPath);
            if (files != null) {
                diskChildren = new HashSet<String>();
                for (File file : files) {
                    diskChildren.add(SVNFileUtil.getFileName(file));
                }
            } else {
                diskChildren = null;
            }
        } else {
            diskChildren = null;
        }

        Map<String, ISVNWCDb.SVNWCDbInfo> versionedChildren = new HashMap<String, ISVNWCDb.SVNWCDbInfo>();
        Set<String> conflictedChildren = new HashSet<String>();
        wcContext.getDb().readChildren(srcAbsPath, versionedChildren, conflictedChildren);

        for (final Map.Entry<String, ISVNWCDb.SVNWCDbInfo> entry : versionedChildren.entrySet()) {
            if (wcContext.getEventHandler() != null) {
                wcContext.getEventHandler().checkCancelled();
            }
            String childName = entry.getKey();
            ISVNWCDb.SVNWCDbInfo info = entry.getValue();

            File childSrcAbsPath = SVNFileUtil.createFilePath(srcAbsPath, childName);
            File childDstAbsPath = SVNFileUtil.createFilePath(dstAbsPath, childName);

            if (info.opRoot) {
                wcContext.getDb().opCopyShadowedLayer(childSrcAbsPath, childDstAbsPath, isMove);
            }

            if (info.status == SVNWCDbStatus.Normal || info.status == SVNWCDbStatus.Added) {
                if (info.kind == SVNWCDbKind.File) {
                    if (!info.fileExternal) {
                        copyVersionedFile(wcContext, childSrcAbsPath, childDstAbsPath, dstOpRootAbsPath, tmpDirAbsPath, metadataOnly, info.conflicted, isMove, false);
                    }
                } else if (info.kind == SVNWCDbKind.Dir) {
                    copyVersionedDirectory(wcContext, childSrcAbsPath, childDstAbsPath, dstOpRootAbsPath, tmpDirAbsPath, metadataOnly, isMove, false);
                } else {
                    SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.NODE_UNEXPECTED_KIND, "cannot handle node kind for ''{0}''", childSrcAbsPath);
                    SVNErrorManager.error(errorMessage, SVNLogType.WC);
                }
            } else if (info.status == SVNWCDbStatus.Deleted || info.status == SVNWCDbStatus.NotPresent || info.status == SVNWCDbStatus.Excluded) {
                wcContext.getDb().opCopy(childSrcAbsPath, childDstAbsPath, dstOpRootAbsPath, isMove, null);
            } else if (info.status == SVNWCDbStatus.Incomplete) {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "Cannot handle status of ''{0}''", childSrcAbsPath);
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            } else {
                assert (info.status == SVNWCDbStatus.ServerExcluded);
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "Cannot copy ''{0}'' excluded by server", childSrcAbsPath);
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }
            if (diskChildren != null && (info.status == SVNWCDbStatus.Normal || info.status == SVNWCDbStatus.Added)) {
                diskChildren.remove(childName);
            }
        }

        if (diskChildren != null && diskChildren.size() > 0) {
            List<File> markerFiles = wcContext.getDb().getConflictMarkerFiles(srcAbsPath);

            workItems = null;

            for (String name : diskChildren) {
                name = SVNPathUtil.tail(name);

                if (name.equals(SVNFileUtil.getAdminDirectoryName())) {
                    continue;
                }

                if (wcContext.getEventHandler() != null) {
                    wcContext.getEventHandler().checkCancelled();
                }

                File unverSrcAbsPath = SVNFileUtil.createFilePath(srcAbsPath, name);
                File unverDstAbsPath = SVNFileUtil.createFilePath(dstAbsPath, name);

                if (markerFiles != null && markerFiles.contains(unverSrcAbsPath)) {
                    continue;
                }
                CopyToTmpDir copyToTmpDir = copyToTmpDir(unverSrcAbsPath, unverDstAbsPath, tmpDirAbsPath, true, true);
                SVNSkel workItem = copyToTmpDir.workItem;

                if (workItem != null) {
                    workItems = SVNWCContext.wqMerge(workItems, workItem);
                }
            }

            wcContext.getDb().addWorkQueue(dstAbsPath, workItems);
        }
    }

    private void copyVersionedFile(SVNWCContext wcContext, File srcAbsPath, File dstAbsPath, File dstOpRootAbsPath, File tmpDirAbsPath, boolean metadataOnly, boolean conflicted, boolean isMove, boolean notify) throws SVNException {
        SVNSkel workItems = null;
        if (!metadataOnly) {
            File mySrcAbsPath = srcAbsPath;
            boolean handleAsUnversioned = false;
            if (conflicted) {
                SVNSkel conflict = wcContext.getDb().readConflict(srcAbsPath);

                File conflictWorking;
                try {
                    Structure<SvnWcDbConflicts.TextConflictInfo> conflictInfoStructure = SvnWcDbConflicts.readTextConflict(wcContext.getDb(), srcAbsPath, conflict);
                    conflictWorking = conflictInfoStructure.get(SvnWcDbConflicts.TextConflictInfo.mineAbsPath);
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_MISSING) {
                        throw e;
                    }
                    conflictWorking = null;
                }

                if (conflictWorking != null) {
                    if (SVNFileType.getType(conflictWorking) == SVNFileType.FILE) {
                        handleAsUnversioned = true;
                        mySrcAbsPath = conflictWorking;
                    }
                }
            }
            CopyToTmpDir copyToTmpDir = copyToTmpDir(mySrcAbsPath, dstAbsPath, tmpDirAbsPath, true, handleAsUnversioned);
            workItems = copyToTmpDir.workItem;
        }
        
        wcContext.getDb().opCopy(srcAbsPath, dstAbsPath, dstOpRootAbsPath, isMove, workItems);
        wcContext.wqRun(SVNFileUtil.getParentFile(dstAbsPath));
        if (notify) {
            if (workItems != null) {
                getWcContext().wqRun(dstAbsPath);
            }
            SVNEvent event = SVNEventFactory.createSVNEvent(dstAbsPath, SVNNodeKind.FILE, null, -1, SVNEventAction.ADD, SVNEventAction.ADD, null, null, 1, 1);
            handleEvent(event, -1);
        }
    }
    
    private CopyToTmpDir copyToTmpDir(File srcAbsPath, File dstAbsPath, File tmpDirAbsPath, boolean fileCopy, boolean unversioned) throws SVNException {
        boolean deleteOnClose = false;

        CopyToTmpDir copyToTmpDir = new CopyToTmpDir();
        copyToTmpDir.workItem = null;

        SVNFileType type = SVNFileType.getType(srcAbsPath);
        copyToTmpDir.kind = SVNFileType.getNodeKind(type);

        boolean isSpecial = type == SVNFileType.SYMLINK;

        if (copyToTmpDir.kind == SVNNodeKind.NONE) {
            return copyToTmpDir;
        } else if (copyToTmpDir.kind == SVNNodeKind.UNKNOWN) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.NODE_UNEXPECTED_KIND, "Source ''{0}'' is unexpected kind", srcAbsPath);
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        } else if (copyToTmpDir.kind == SVNNodeKind.DIR) {
            deleteOnClose = true;
        } else {
            deleteOnClose = false;
        }
        File dstTmpAbsPath = null;
        try {
            if (fileCopy && !unversioned) {
                boolean modified = getWcContext().isTextModified(srcAbsPath, false) || getWcContext().isPropsModified(srcAbsPath);
                if (!modified) {
                    copyToTmpDir.workItem = getWcContext().wqBuildFileInstall(dstAbsPath, null, false, true);
                    return copyToTmpDir;
                }
            }

            dstTmpAbsPath = copyToTmpDir.kind == SVNNodeKind.DIR ? SVNFileUtil.createUniqueDir(tmpDirAbsPath, SVNFileUtil.getFileName(srcAbsPath), ".tmp", false) : SVNFileUtil.createUniqueFile(tmpDirAbsPath, SVNFileUtil.getFileName(srcAbsPath), ".tmp", false);
            if (copyToTmpDir.kind == SVNNodeKind.DIR) {
                if (fileCopy) {
                    SVNFileUtil.copyDirectory(srcAbsPath, dstTmpAbsPath, false, getOperation().getEventHandler());
                } else {
                    SVNFileUtil.ensureDirectoryExists(dstTmpAbsPath);
                }
            } else if (!isSpecial) {
                SVNFileUtil.copyFile(srcAbsPath, dstTmpAbsPath, false, true);
            } else {
                SVNFileUtil.deleteFile(dstTmpAbsPath);
                SVNFileUtil.copySymlink(srcAbsPath, dstTmpAbsPath);
            }

            if (fileCopy) {
                SVNFileUtil.setReadonly(dstTmpAbsPath, false);
            }
            copyToTmpDir.workItem = getWcContext().wqBuildFileMove(dstAbsPath, dstTmpAbsPath, dstAbsPath);
            return copyToTmpDir;
        } finally {
            if (dstTmpAbsPath != null && deleteOnClose && copyToTmpDir.kind == SVNNodeKind.FILE) {
                SVNFileUtil.deleteFile(dstTmpAbsPath);
            }
        }
    }

    private static class CopyToTmpDir {
        SVNSkel workItem;
        SVNNodeKind kind;
    }
    
    private static class SvnCopyPair {
        File source;
        File dst;
        File dstParent;
        
        String baseName;
    }
}
