package org.tmatesoft.svn.core.internal.wc17.db;

import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.schema.SqlJetConflictAction;
import org.tmatesoft.sqljet.core.table.ISqlJetCursor;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetDb;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetInsertStatement;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetSelectStatement;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetStatement;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNSkel;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc17.SVNWCUtils;
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.SVNWCDb.InsertWorking;
import org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.ReposInfo;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.AdditionInfo;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.DeletionInfo;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.NodeInfo;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema.ACTUAL_NODE__Fields;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema.NODES__Fields;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbStatements;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.util.SVNLogType;

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

import static org.tmatesoft.svn.core.internal.wc17.db.SvnWcDbStatementUtil.*;

public class SvnWcDbCopy extends SvnWcDbShared {

    private enum CopyInfo {
        copyFromId, 
        copyFromRelpath,
        copyFromRev,
        status,
        kind,
        opRoot,
        haveWork
    }

    public static void copyFile(SVNWCDbDir pdh, File localRelpath,
            SVNProperties props, long changedRev, SVNDate changedDate,
            String changedAuthor, File originalReposRelPath,
            SVNURL originalRootUrl, String originalUuid, long originalRevision,
            SvnChecksum checksum,
            boolean updateActualProps, SVNProperties newActualProps,
            SVNSkel conflict, SVNSkel workItems) throws SVNException {

        InsertWorking iw = pdh.getWCRoot().getDb().new InsertWorking();
        iw.status = SVNWCDbStatus.Normal;
        iw.kind = SVNWCDbKind.File;
        iw.props = props;
        iw.changedAuthor = changedAuthor;
        iw.changedDate = changedDate;
        iw.changedRev = changedRev;
        
        if (originalRootUrl != null) {
            long reposId = pdh.getWCRoot().getDb().createReposId(pdh.getWCRoot().getSDb(), originalRootUrl, originalUuid);
            iw.originalReposId = reposId;
            iw.originalReposRelPath = originalReposRelPath;
            iw.originalRevision = originalRevision;
        }
        long[] depths = getOpDepthForCopy(pdh.getWCRoot(), localRelpath, iw.originalReposId, originalReposRelPath, originalRevision);
        iw.opDepth = depths[0];
        iw.notPresentOpDepth = depths[1];
        iw.checksum = checksum;
        iw.workItems = workItems;
        if (updateActualProps) {
            iw.updateActualProps = true;
            iw.newActualProps = newActualProps;
        }
        iw.conflict = conflict;

        iw.wcRoot = pdh.getWCRoot();
        iw.localRelpath = localRelpath;
        
        pdh.getWCRoot().getSDb().runTransaction(iw);
        pdh.flushEntries(pdh.getWCRoot().getAbsPath());
    }

    public static void copyDir(SVNWCDbDir pdh, File localRelpath,
            SVNProperties props, long changedRev, SVNDate changedDate, String changedAuthor, File originalReposRelPath, SVNURL originalRootUrl, String originalUuid,
            long originalRevision, List<File> children, boolean isMove, SVNDepth depth, SVNSkel conflict, SVNSkel workItems) throws SVNException {

        InsertWorking iw = pdh.getWCRoot().getDb().new InsertWorking();
        iw.status = SVNWCDbStatus.Normal;
        iw.kind = SVNWCDbKind.Dir;
        iw.props = props;
        iw.changedAuthor = changedAuthor;
        iw.changedDate = changedDate;
        iw.changedRev = changedRev;
        
        if (originalRootUrl != null) {
            long reposId = pdh.getWCRoot().getDb().createReposId(pdh.getWCRoot().getSDb(), originalRootUrl, originalUuid);
            iw.originalReposId = reposId;
            iw.originalReposRelPath = originalReposRelPath;
            iw.originalRevision = originalRevision;
        }
        long[] depths = getOpDepthForCopy(pdh.getWCRoot(), localRelpath, iw.originalReposId, originalReposRelPath, originalRevision);
        iw.opDepth = depths[0];
        iw.notPresentOpDepth = depths[1];
        iw.children = children;
        iw.depth = depth;
        iw.workItems = workItems;
        iw.movedHere = isMove && (depths[2] == 0 || iw.opDepth == depths[2]);
        iw.conflict = conflict;

        iw.wcRoot = pdh.getWCRoot();
        iw.localRelpath = localRelpath;
        
        pdh.getWCRoot().getSDb().runTransaction(iw);
        pdh.flushEntries(pdh.getWCRoot().getAbsPath());
    }

    private static void copyShadowedLayer(SVNWCDbDir srcPdh, File srcRelpath, long srcOpDepth, 
            SVNWCDbDir dstPdh, File dstRelpath, long dstOpDepth, long delOpDepth, long reposId, File reposRelPath, long revision, int moveOpDepth) throws SVNException {
        Structure<NodeInfo> depthInfo = null;
        try {
            depthInfo = SvnWcDbReader.getDepthInfo(srcPdh.getWCRoot(), srcRelpath, srcOpDepth, 
                    NodeInfo.status, NodeInfo.kind, NodeInfo.revision, NodeInfo.reposRelPath, NodeInfo.reposId);
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                throw e;
            }
            return;
        }
        SVNWCDbStatus status = depthInfo.get(NodeInfo.status);
        if (srcOpDepth == 0) {
            long nodeRevision = depthInfo.lng(NodeInfo.revision);
            long nodeReposId = depthInfo.lng(NodeInfo.reposId);
            File nodeReposRelPath = depthInfo.get(NodeInfo.reposRelPath);
            
            if (status == SVNWCDbStatus.NotPresent
                    || status == SVNWCDbStatus.Excluded
                    || status == SVNWCDbStatus.ServerExcluded
                    || nodeRevision != revision
                    || nodeReposId != reposId
                    || !nodeReposRelPath.equals(reposRelPath)) {
                
                ReposInfo reposInfo = srcPdh.getWCRoot().getDb().fetchReposInfo(srcPdh.getWCRoot().getSDb(), nodeReposId);
                nodeReposId = dstPdh.getWCRoot().getDb().createReposId(dstPdh.getWCRoot().getSDb(), 
                        SVNURL.parseURIEncoded(reposInfo.reposRootUrl), reposInfo.reposUuid);
                
                InsertWorking iw = dstPdh.getWCRoot().getDb().new InsertWorking();
                iw.opDepth = dstOpDepth;
                if (status != SVNWCDbStatus.Excluded) {
                    iw.status = SVNWCDbStatus.NotPresent;
                } else {
                    iw.status = SVNWCDbStatus.Excluded;
                }
                iw.kind = depthInfo.get(NodeInfo.kind);
                iw.originalReposId = nodeReposId;
                iw.originalRevision = nodeRevision;
                iw.originalReposRelPath = nodeReposRelPath;
                
                iw.changedRev = -1;
                iw.depth = SVNDepth.INFINITY;

                iw.wcRoot = dstPdh.getWCRoot();
                iw.localRelpath = dstRelpath;
                
                dstPdh.getWCRoot().getSDb().runTransaction(iw);

                return;
            }
        }
        SVNWCDbStatus dstPresence = null;
        switch (status) {
        case Normal:
        case Added:
        case MovedHere:
        case Copied:
            dstPresence = SVNWCDbStatus.Normal;
            break;
        case Deleted:
        case NotPresent:
            dstPresence = SVNWCDbStatus.NotPresent;
            break;
        case Excluded:
            dstPresence = SVNWCDbStatus.Excluded;
            break;
        case ServerExcluded:
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "Cannot copy ''{0}'' excluded by server", srcPdh.getWCRoot().getAbsPath(srcRelpath));
            SVNErrorManager.error(err, SVNLogType.WC);
            break;
        default:
            SVNErrorMessage err2 = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "Cannot handle status of ''{0}''", srcPdh.getWCRoot().getAbsPath(srcRelpath));
            SVNErrorManager.error(err2, SVNLogType.WC);
        }
        
        if (dstPresence == SVNWCDbStatus.Normal && srcPdh.getWCRoot().getSDb() == dstPdh.getWCRoot().getSDb()) {
            SVNSqlJetStatement stmt;
            stmt = new InsertWorkingNodeCopy(srcPdh.getWCRoot().getSDb(), srcOpDepth);
            try {
                stmt.bindf("issisti",
                        srcPdh.getWCRoot().getWcId(),
                        srcRelpath,
                        dstRelpath,
                        dstOpDepth,
                        SVNFileUtil.getFileDir(dstRelpath),
                        SvnWcDbStatementUtil.getPresenceText(dstPresence),
                        srcOpDepth);
                if (dstOpDepth == moveOpDepth) {
                    stmt.bindLong(7, 1);
                }
                stmt.done();
            } finally {
                stmt.reset();
            }
            
            InsertWorking iw = dstPdh.getWCRoot().getDb().new InsertWorking();
            iw.opDepth = delOpDepth;
            iw.status = SVNWCDbStatus.BaseDeleted;
            iw.kind = depthInfo.get(NodeInfo.kind);
            iw.changedRev = -1;
            iw.depth = SVNDepth.INFINITY;
            iw.wcRoot = dstPdh.getWCRoot();
            iw.localRelpath = dstRelpath;
            
            dstPdh.getWCRoot().getSDb().runTransaction(iw);
        } else {
            if (dstPresence == SVNWCDbStatus.Normal) {
                dstPresence = SVNWCDbStatus.NotPresent;
            }

            InsertWorking iw = dstPdh.getWCRoot().getDb().new InsertWorking();
            iw.opDepth = dstOpDepth;
            iw.status = dstPresence;
            iw.kind = depthInfo.get(NodeInfo.kind);
            iw.changedRev = -1;
            iw.depth = SVNDepth.INFINITY;
            iw.wcRoot = dstPdh.getWCRoot();
            iw.localRelpath = dstRelpath;
            
            dstPdh.getWCRoot().getSDb().runTransaction(iw);            
        }
        List<String> children = srcPdh.getWCRoot().getDb().gatherRepoChildren(srcPdh.getWCRoot(), srcRelpath, srcOpDepth);
        for (String name : children) {
            File srcChildRelpath = SVNFileUtil.createFilePath(srcRelpath, name);
            File dstChildRelpath = SVNFileUtil.createFilePath(dstRelpath, name);
            File childReposRelPath = null;
            if (reposRelPath != null) {
                childReposRelPath = SVNFileUtil.createFilePath(reposRelPath, name);
            }
            copyShadowedLayer(srcPdh, srcChildRelpath, srcOpDepth, dstPdh, dstChildRelpath, dstOpDepth, delOpDepth, reposId, childReposRelPath, revision, moveOpDepth);
        }
    }

    public static void copyShadowedLayer(SVNWCDbDir srcPdh, File localSrcRelpath, SVNWCDbDir dstPdh, File localDstRelpath, boolean isMove) throws SVNException {
        boolean dstLocked = false;
        begingWriteTransaction(srcPdh.getWCRoot());
        try {
            if (srcPdh.getWCRoot().getSDb() != dstPdh.getWCRoot().getSDb()) {
                begingWriteTransaction(dstPdh.getWCRoot());
                dstLocked = true;
            }
            File srcParentRelPath = SVNFileUtil.getFileDir(localSrcRelpath);
            File dstParentRelPath = SVNFileUtil.getFileDir(localDstRelpath);
            
            long srcOpDepth = getOpDepthOf(srcPdh.getWCRoot(), srcParentRelPath);
            long dstOpDepth = getOpDepthOf(dstPdh.getWCRoot(), dstParentRelPath);
            long delOpDepth = SVNWCUtils.relpathDepth(localDstRelpath);
            
            Structure<NodeInfo> depthInfo = SvnWcDbReader.getDepthInfo(srcPdh.getWCRoot(), srcParentRelPath, srcOpDepth, 
                    NodeInfo.revision, NodeInfo.reposRelPath, NodeInfo.reposId);
            File reposRelpath = depthInfo.get(NodeInfo.reposRelPath);
            if (reposRelpath == null) {
                return;
            }
            reposRelpath = SVNFileUtil.createFilePath(reposRelpath, SVNFileUtil.getFileName(localSrcRelpath));
            copyShadowedLayer(srcPdh, localSrcRelpath, srcOpDepth, dstPdh, localDstRelpath, dstOpDepth, delOpDepth, 
                    depthInfo.lng(NodeInfo.reposId), reposRelpath, depthInfo.lng(NodeInfo.revision), isMove ? (int)dstOpDepth : 0);
            
            depthInfo.release();
        } catch (SVNException e) {
            try {
                rollbackTransaction(srcPdh.getWCRoot());
            } finally {
                if (dstLocked) {
                    rollbackTransaction(dstPdh.getWCRoot());
                }
            }
        } finally {
            try {
                commitTransaction(srcPdh.getWCRoot());
            } finally {
                if (dstLocked) {
                    commitTransaction(dstPdh.getWCRoot());
                }
            }
        }
    }

    public static void copy(SVNWCDbDir srcPdh, File localSrcRelpath, SVNWCDbDir dstPdh, File localDstRelpath, File dstOpRootRelPath, boolean isMove, SVNSkel workItems) throws SVNException {
        boolean dstLocked = false;
        begingWriteTransaction(srcPdh.getWCRoot());
        try {
            if (srcPdh.getWCRoot().getSDb() != dstPdh.getWCRoot().getSDb()) {
                begingWriteTransaction(dstPdh.getWCRoot());
                dstLocked = true;
            }
            doCopy(srcPdh, localSrcRelpath, dstPdh, localDstRelpath, dstOpRootRelPath, isMove, workItems);
        } catch (SVNException e) {
            try {
                rollbackTransaction(srcPdh.getWCRoot());
            } finally {
                if (dstLocked) {
                    rollbackTransaction(dstPdh.getWCRoot());
                }
            }
        } finally {
            try {
                commitTransaction(srcPdh.getWCRoot());
            } finally {
                if (dstLocked) {
                    commitTransaction(dstPdh.getWCRoot());
                }
            }
        }
        
    }

    public static SVNNodeKind readKind(ISVNWCDb db, File path, boolean showDeleted, boolean showHidden) throws SVNException {
        SVNNodeKind kind = db.readKind(path, true, showDeleted, showHidden);
        return kind == SVNNodeKind.UNKNOWN ? SVNNodeKind.NONE : kind;
    }

    private static void doCopy(SVNWCDbDir srcPdh, File localSrcRelpath, SVNWCDbDir dstPdh, File localDstRelpath, File dstOpRootRelPath, boolean isMove, SVNSkel workItems) throws SVNException {
        int moveOpDepth = isMove ? SVNWCUtils.relpathDepth(dstOpRootRelPath) : 0;

        Structure<CopyInfo> copyInfo = getCopyInfo(srcPdh.getWCRoot(), localSrcRelpath);
        File copyFromRelpath = copyInfo.<File>get(CopyInfo.copyFromRelpath);
        long[] dstOpDepths = getOpDepthForCopy(dstPdh.getWCRoot(), localDstRelpath,
                copyInfo.lng(CopyInfo.copyFromId), copyFromRelpath, copyInfo.lng(CopyInfo.copyFromRev));
        
        SVNWCDbStatus status = copyInfo.get(CopyInfo.status);
        SVNWCDbStatus dstPresence = null;
        boolean opRoot = copyInfo.is(CopyInfo.opRoot);
        
        switch (status) {
        case Normal:
        case Added:
        case MovedHere:
        case Copied:
            dstPresence = SVNWCDbStatus.Normal;
            break;
        case Deleted:
            if (opRoot) {
                try {
                    Structure<NodeInfo> dstInfo = SvnWcDbReader.readInfo(dstPdh.getWCRoot(), localDstRelpath, NodeInfo.status);
                    SVNWCDbStatus dstStatus = dstInfo.get(NodeInfo.status);
                    dstInfo.release();
                    if (dstStatus == SVNWCDbStatus.Deleted) {
                        dstPdh.getWCRoot().getDb().addWorkQueue(dstPdh.getWCRoot().getAbsPath(), workItems);
                        return;
                    }
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                        throw e;
                    }
                }
            } else {
                if (copyFromRelpath == null) {
                    SVNWCDb.addWorkItems(dstPdh.getWCRoot().getSDb(), workItems);
                    return;
                }
            }
            //"break;" is absent by intention
        case NotPresent:
        case Excluded:
            if (dstOpDepths[1] > 0) {
                dstOpDepths[0] = dstOpDepths[1];
                dstOpDepths[1] = -1;
            }
            if (status == SVNWCDbStatus.Excluded) {
                dstPresence = SVNWCDbStatus.Excluded;
            } else {
                dstPresence = SVNWCDbStatus.NotPresent;
            }
            break;
        case ServerExcluded:
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "Cannot copy ''{0}'' excluded by server", srcPdh.getWCRoot().getAbsPath(localSrcRelpath));
            SVNErrorManager.error(err, SVNLogType.WC);
        default:
            SVNErrorMessage err1 = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS, "Cannot handle status of ''{0}''", srcPdh.getWCRoot().getAbsPath(localSrcRelpath));
            SVNErrorManager.error(err1, SVNLogType.WC);
        }
        
        SVNWCDbKind kind = copyInfo.get(CopyInfo.kind);
        List<String> children = null;
        if (kind == SVNWCDbKind.Dir) {
            long opDepth = getOpDepthOf(srcPdh.getWCRoot(), localSrcRelpath);
            children = srcPdh.getWCRoot().getDb().gatherRepoChildren(srcPdh.getWCRoot(), localSrcRelpath, opDepth);
        }
        
        if (srcPdh.getWCRoot() == dstPdh.getWCRoot()) {
            File dstParentRelpath = SVNFileUtil.getFileDir(localDstRelpath);
            SVNSqlJetStatement stmt = srcPdh.getWCRoot().getSDb().getStatement(SVNWCDbStatements.INSERT_WORKING_NODE_COPY_FROM);
            try {
                stmt.bindf("issist", srcPdh.getWCRoot().getWcId(), localSrcRelpath, localDstRelpath, dstOpDepths[0], dstParentRelpath,
                        SvnWcDbStatementUtil.getPresenceText(dstPresence));
                if (moveOpDepth > 0) {
                    if (SVNWCUtils.relpathDepth(localDstRelpath) == moveOpDepth) {
                        if (!(status == SVNWCDbStatus.Added || status == SVNWCDbStatus.Copied && opRoot)) {
                            stmt.bindLong(7, 1);
                        }
                    } else {
                        SVNSqlJetStatement infoStmt = dstPdh.getWCRoot().getSDb().getStatement(SVNWCDbStatements.SELECT_NODE_INFO);
                        try {
                            infoStmt.bindf("is", dstPdh.getWCRoot().getWcId(), dstParentRelpath);
                            boolean haveRow = infoStmt.next();
                            assert haveRow;
                            if (infoStmt.getColumnBoolean(NODES__Fields.moved_here) && dstOpDepths[0] == dstOpDepths[2]) {
                                stmt.bindLong(7, 1);
                            } else {
                                infoStmt.reset();

                                infoStmt = dstPdh.getWCRoot().getSDb().getStatement(SVNWCDbStatements.SELECT_NODE_INFO);
                                infoStmt.bindf("is", dstPdh.getWCRoot().getWcId(), localSrcRelpath);
                                haveRow = infoStmt.next();
                                assert haveRow;
                                if (infoStmt.getColumnBoolean(NODES__Fields.moved_here)) {
                                    stmt.bindLong(7, 1);
                                }
                            }
                        } finally {
                            infoStmt.reset();
                        }
                    }
                }
                stmt.done();
            } finally {
                stmt.reset();
            }
            
            copyActual(srcPdh, localSrcRelpath, dstPdh, localDstRelpath);
            
            if (dstOpDepths[1] > 0) {
                stmt = srcPdh.getWCRoot().getSDb().getStatement(SVNWCDbStatements.INSERT_NODE);
                try {
                    stmt.bindf("isisisrtnt",
                            srcPdh.getWCRoot().getWcId(),
                            localDstRelpath,
                            dstOpDepths[1],
                            dstParentRelpath,
                            copyInfo.lng(CopyInfo.copyFromId),
                            copyInfo.get(CopyInfo.copyFromRelpath),
                            copyInfo.lng(CopyInfo.copyFromRev),
                            SvnWcDbStatementUtil.getPresenceText(SVNWCDbStatus.NotPresent),
                            SvnWcDbStatementUtil.getKindText(kind));
                    stmt.done();
                } finally {
                    stmt.reset();
                }
            }
            if (kind == SVNWCDbKind.Dir && dstPresence == SVNWCDbStatus.Normal) {
                List<File> fileChildren = new LinkedList<File>();
                for (String childName : children) {
                    fileChildren.add(new File(childName));
                }
                srcPdh.getWCRoot().getDb().insertIncompleteChildren(
                        srcPdh.getWCRoot().getSDb(), srcPdh.getWCRoot().getWcId(),
                        localDstRelpath, copyInfo.lng(CopyInfo.copyFromId), copyFromRelpath, copyInfo.lng(CopyInfo.copyFromRev), fileChildren, dstOpDepths[0]);
            }
        } else {
            crossDbCopy(srcPdh, localSrcRelpath, dstPdh, localDstRelpath, dstPresence, dstOpDepths[0], dstOpDepths[1], kind, children, 
                    copyInfo.lng(CopyInfo.copyFromId), copyInfo.<File>get(CopyInfo.copyFromRelpath), copyInfo.lng(CopyInfo.copyFromRev));
        }
        dstPdh.getWCRoot().getDb().addWorkQueue(dstPdh.getWCRoot().getAbsPath(), workItems);
    }
    
    private static void crossDbCopy(SVNWCDbDir srcPdh, File localSrcRelpath,
            SVNWCDbDir dstPdh, File localDstRelpath, SVNWCDbStatus dstPresence,
            long dstOpDepth, long dstNpOpDepth, SVNWCDbKind kind, List<String> children, long copyFromId,
            File copyFromRelpath, long copyFromRev) throws SVNException {
        Structure<NodeInfo> nodeInfo = 
                SvnWcDbShared.readInfo(srcPdh.getWCRoot(), localSrcRelpath, NodeInfo.changedRev, NodeInfo.changedDate, NodeInfo.changedAuthor,
                        NodeInfo.depth, NodeInfo.checksum);
        SVNProperties properties = SvnWcDbProperties.readPristineProperties(srcPdh.getWCRoot(), localSrcRelpath);
        
        InsertWorking iw = dstPdh.getWCRoot().getDb().new InsertWorking();
        iw.status = dstPresence;
        iw.kind = kind;
        iw.props = properties;
        iw.changedRev = nodeInfo.lng(NodeInfo.changedRev);
        iw.changedDate = nodeInfo.get(NodeInfo.changedDate);
        iw.changedAuthor = nodeInfo.text(NodeInfo.changedAuthor);
        iw.opDepth = dstOpDepth;
        iw.checksum = nodeInfo.get(NodeInfo.checksum);
        List<File> childrenAsFiles = null;
        if (children != null) {
            childrenAsFiles = new ArrayList<File>();
            for (String name : children) {
                childrenAsFiles.add(new File(name));
            }
        }
        iw.children = childrenAsFiles;
        iw.depth = nodeInfo.get(NodeInfo.depth);
        iw.notPresentOpDepth = dstNpOpDepth;

        iw.originalReposId = copyFromId;
        iw.originalRevision = copyFromRev;
        iw.originalReposRelPath = copyFromRelpath;
        
        iw.wcRoot = dstPdh.getWCRoot();
        iw.localRelpath = localDstRelpath;
        
        dstPdh.getWCRoot().getSDb().runTransaction(iw);
        
        copyActual(srcPdh, localSrcRelpath, dstPdh, localDstRelpath);
        nodeInfo.release();
    }

    private static void copyActual(SVNWCDbDir srcPdh, File localSrcRelpath, SVNWCDbDir dstPdh, File localDstRelpath) throws SVNException {
        SVNSqlJetStatement stmt = srcPdh.getWCRoot().getSDb().getStatement(SVNWCDbStatements.SELECT_ACTUAL_NODE);
        stmt.bindf("is", srcPdh.getWCRoot().getWcId(), localSrcRelpath);
        try {
            if (stmt.next()) {
                String changelist = getColumnText(stmt, ACTUAL_NODE__Fields.changelist);
                byte[] properties = getColumnBlob(stmt, ACTUAL_NODE__Fields.properties);
                
                if (changelist != null || properties != null) {
                    reset(stmt);
                    stmt = dstPdh.getWCRoot().getSDb().getStatement(SVNWCDbStatements.INSERT_ACTUAL_NODE);
                    try {
                        stmt.bindf("issbsb",
                                dstPdh.getWCRoot().getWcId(),
                                localDstRelpath,
                                SVNFileUtil.getFileDir(localDstRelpath),
                                properties, changelist, null);
                        stmt.done();
                    } finally {
                        stmt.reset();
                    }
                }
            }
        } finally {
            reset(stmt);
        }
    }

    private static Structure<CopyInfo> getCopyInfo(SVNWCDbRoot wcRoot, File localRelPath) throws SVNException {
        Structure<CopyInfo> result = Structure.obtain(CopyInfo.class);
        result.set(CopyInfo.haveWork, false);
        result.set(CopyInfo.copyFromRev, -1);
        
        Structure<NodeInfo> nodeInfo = SvnWcDbReader.readInfo(wcRoot, localRelPath, NodeInfo.status, NodeInfo.kind, NodeInfo.revision, NodeInfo.reposRelPath,
                NodeInfo.reposId, NodeInfo.opRoot, NodeInfo.haveWork);

        nodeInfo.from(NodeInfo.kind, NodeInfo.status, NodeInfo.reposId, NodeInfo.haveWork, NodeInfo.opRoot)
            .into(result, CopyInfo.kind, CopyInfo.status, CopyInfo.copyFromId, CopyInfo.haveWork, CopyInfo.opRoot);
        SVNWCDbStatus status = nodeInfo.get(NodeInfo.status);
        File reposRelpath = nodeInfo.get(NodeInfo.reposRelPath);
        long revision = nodeInfo.lng(NodeInfo.revision);
        
        nodeInfo.release();

        if (status == SVNWCDbStatus.Excluded) {
            File parentRelpath = SVNFileUtil.getFileDir(localRelPath);
            String name = SVNFileUtil.getFileName(localRelPath);
            
            Structure<CopyInfo> parentCopyInfo = getCopyInfo(wcRoot, parentRelpath);
            parentCopyInfo.from(CopyInfo.copyFromId, CopyInfo.copyFromRev)
                .into(result, CopyInfo.copyFromId, CopyInfo.copyFromRev);
            
            if (parentCopyInfo.get(CopyInfo.copyFromRelpath) != null) {
                result.set(CopyInfo.copyFromRelpath, 
                        SVNFileUtil.createFilePath(parentCopyInfo.<File>get(CopyInfo.copyFromRelpath), name));
            }
            
            parentCopyInfo.release();
        } else if (status == SVNWCDbStatus.Added) {
            Structure<AdditionInfo> additionInfo = scanAddition(wcRoot, localRelPath, AdditionInfo.opRootRelPath, 
                    AdditionInfo.originalReposRelPath, AdditionInfo.originalReposId, AdditionInfo.originalRevision);
            additionInfo.from(AdditionInfo.originalReposRelPath, AdditionInfo.originalReposId, AdditionInfo.originalRevision, AdditionInfo.status)
                .into(result, CopyInfo.copyFromRelpath, CopyInfo.copyFromId, CopyInfo.copyFromRev, CopyInfo.status);
            
            if (additionInfo.get(AdditionInfo.originalReposRelPath) != null) {
                File opRootRelPath = additionInfo.get(AdditionInfo.opRootRelPath);
                File copyFromRelPath = additionInfo.get(AdditionInfo.originalReposRelPath); 
                File relPath = SVNFileUtil.createFilePath(copyFromRelPath, SVNWCUtils.skipAncestor(opRootRelPath, localRelPath));
                result.set(CopyInfo.copyFromRelpath, relPath);
            }
            
            additionInfo.release();
        } else if (status == SVNWCDbStatus.Deleted) {
            Structure<DeletionInfo> deletionInfo = scanDeletion(wcRoot, localRelPath);
            if (deletionInfo.get(DeletionInfo.workDelRelPath) != null) {
                File parentDelRelpath = SVNFileUtil.getFileDir(deletionInfo.<File>get(DeletionInfo.workDelRelPath));

                Structure<AdditionInfo> additionInfo = scanAddition(wcRoot, parentDelRelpath, AdditionInfo.opRootRelPath, 
                        AdditionInfo.originalReposRelPath, AdditionInfo.originalReposId, AdditionInfo.originalRevision);
                
                additionInfo.from(AdditionInfo.originalReposRelPath, AdditionInfo.originalReposId, AdditionInfo.originalRevision)
                    .into(result, CopyInfo.copyFromRelpath, CopyInfo.copyFromId, CopyInfo.copyFromRev);
                File opRootRelPath = additionInfo.get(AdditionInfo.opRootRelPath);
                File copyFromRelPath = additionInfo.get(AdditionInfo.originalReposRelPath); 
                File relPath = SVNFileUtil.createFilePath(copyFromRelPath, SVNWCUtils.skipAncestor(opRootRelPath, localRelPath));
                result.set(CopyInfo.copyFromRelpath, relPath);

                additionInfo.release();
            } else if (deletionInfo.get(DeletionInfo.baseDelRelPath) != null) {
                Structure<NodeInfo> baseInfo = getDepthInfo(wcRoot, localRelPath, 0, NodeInfo.revision, NodeInfo.reposRelPath, NodeInfo.reposId);
                baseInfo.from(NodeInfo.revision, NodeInfo.reposRelPath, NodeInfo.reposId).
                    into(result, CopyInfo.copyFromRev, CopyInfo.copyFromRelpath, CopyInfo.copyFromId);
                baseInfo.release();
            }
            deletionInfo.release();
        } else {
            result.set(CopyInfo.copyFromRelpath, reposRelpath);
            result.set(CopyInfo.copyFromRev, revision);
        }
        
        return result;
    }
    
    private static long[] getOpDepthForCopy(SVNWCDbRoot wcRoot, File localRelpath, long copyFromReposId, File copyFromRelpath, long copyFromRevision) throws SVNException {
        File parentRelPath = SVNFileUtil.getFileDir(localRelpath);
        long[] result = new long[] {SVNWCUtils.relpathDepth(localRelpath), -1, SVNWCUtils.relpathDepth(parentRelPath)};
        if (copyFromRelpath == null) {
            return result;
        }
        
        long minOpDepth = 1;
        long incompleteOpDepth = -1;
        
        SVNSqlJetStatement stmt = wcRoot.getSDb().getStatement(SVNWCDbStatements.SELECT_WORKING_NODE);
        try {
            bindf(stmt, "is", wcRoot.getWcId(), localRelpath);
            if (stmt.next()) {
                SVNWCDbStatus status = getColumnPresence(stmt);
                minOpDepth = getColumnInt64(stmt, NODES__Fields.op_depth);
                if (status == SVNWCDbStatus.Incomplete) {
                    incompleteOpDepth = minOpDepth;
                }
            }
        } finally {
            reset(stmt);
        }
        bindf(stmt, "is", wcRoot.getWcId(), parentRelPath);
        if (stmt.next()) {
            SVNWCDbStatus presence = SvnWcDbStatementUtil.getColumnPresence(stmt, NODES__Fields.presence);

            long parentOpDepth = getColumnInt64(stmt, NODES__Fields.op_depth);
            result[2] = parentOpDepth;
            if (parentOpDepth < minOpDepth) {
                reset(stmt);
                return result;
            }

            assert presence == SVNWCDbStatus.Normal;

            if (incompleteOpDepth < 0 || incompleteOpDepth == parentOpDepth) {
                long parentCopyFromReposId = getColumnInt64(stmt, NODES__Fields.repos_id);
                File parentCopyFromRelpath = getColumnPath(stmt, NODES__Fields.repos_path);
                long parentCopyFromRevision = getColumnInt64(stmt, NODES__Fields.revision);
                if (parentCopyFromReposId == copyFromReposId) {
                    if (copyFromRevision == parentCopyFromRevision &&
                            copyFromRelpath.equals(SVNFileUtil.createFilePath(parentCopyFromRelpath, localRelpath.getName()))) {
                        result[0] = parentOpDepth;
                    } else if (incompleteOpDepth > 0) {
                        result[1] = incompleteOpDepth;
                    }
                }
            }
        }
        reset(stmt);
        return result;
    }
    
    private static long getOpDepthOf(SVNWCDbRoot wcRoot, File localRelpath) throws SVNException {
        SVNSqlJetStatement stmt = wcRoot.getSDb().getStatement(SVNWCDbStatements.SELECT_NODE_INFO);
        bindf(stmt, "is", wcRoot.getWcId(), localRelpath);
        try {
            if (stmt.next()) {
                return getColumnInt64(stmt, NODES__Fields.op_depth); 
            }
        } finally {
            reset(stmt);
        }        
        return 0;
        
    }

    private static class InsertWorkingNodeCopy extends SVNSqlJetInsertStatement {

        private SelectNodeToCopy select;

        public InsertWorkingNodeCopy(SVNSqlJetDb sDb, boolean base) throws SVNException {
            this(sDb, base ? 0 : -1);
        }

        public InsertWorkingNodeCopy(SVNSqlJetDb sDb, long depth) throws SVNException {
            super(sDb, SVNWCDbSchema.NODES, SqlJetConflictAction.REPLACE);
            select = new SelectNodeToCopy(sDb, depth);
        }

        @Override
        protected Map<String, Object> getInsertValues() throws SVNException {
            // run select once and return values.
            select.bindf("is", getBind(1), getBind(2));
            try {
                if (select.next()) {
                    Map<String, Object> values = new HashMap<String, Object>();
                    values.put(NODES__Fields.wc_id.toString(), select.getColumn(NODES__Fields.wc_id));
                    values.put(NODES__Fields.local_relpath.toString(), getBind(3));
                    values.put(NODES__Fields.op_depth.toString(), getBind(4));
                    values.put(NODES__Fields.parent_relpath.toString(), getBind(5));
                    values.put(NODES__Fields.repos_id.toString(), select.getColumn(NODES__Fields.repos_id));
                    values.put(NODES__Fields.repos_path.toString(), select.getColumn(NODES__Fields.repos_path));
                    values.put(NODES__Fields.revision.toString(), select.getColumn(NODES__Fields.revision));
                    values.put(NODES__Fields.presence.toString(), getBind(6));
                    values.put(NODES__Fields.depth.toString(), select.getColumn(NODES__Fields.depth));
                    values.put(NODES__Fields.moved_here.toString(), getBind(7));
                    values.put(NODES__Fields.kind.toString(), select.getColumn(NODES__Fields.kind));
                    
                    values.put(NODES__Fields.changed_revision.toString(), select.getColumn(NODES__Fields.changed_revision));
                    values.put(NODES__Fields.changed_date.toString(), select.getColumn(NODES__Fields.changed_date));
                    values.put(NODES__Fields.changed_author.toString(), select.getColumn(NODES__Fields.changed_author));
                    values.put(NODES__Fields.checksum.toString(), select.getColumn(NODES__Fields.checksum));
                    values.put(NODES__Fields.properties.toString(), select.getColumn(NODES__Fields.properties));
                    values.put(NODES__Fields.translated_size.toString(), select.getColumn(NODES__Fields.translated_size));
                    values.put(NODES__Fields.last_mod_time.toString(), select.getColumn(NODES__Fields.last_mod_time));
                    values.put(NODES__Fields.symlink_target.toString(), select.getColumn(NODES__Fields.symlink_target));
                    values.put(NODES__Fields.moved_to.toString(), getMovedTo(sDb));
                    return values;
                }                
            } finally {
                select.reset();
            }
            return null;
        }

        private String getMovedTo(SVNSqlJetDb sDb) throws SVNException {
            SVNSqlJetStatement stmt = sDb.getStatement(SVNWCDbStatements.SELECT_MOVED_TO);
            try {
                stmt.bindf("isi", getBind(1), getBind(3), getBind(4));
                boolean next = stmt.next();
                return next ? stmt.getColumnString(NODES__Fields.moved_to) : null;
            } finally {
                stmt.reset();
            }
        }
    }

    /**
     * SELECT wc_id, ?3 (local_relpath), ?4 (op_depth), ?5 (parent_relpath),
     * repos_id, repos_path, revision, ?6 (presence), depth,
     * kind, changed_revision, changed_date, changed_author, checksum, properties,
     * translated_size, last_mod_time, symlink_target
     * FROM nodes
     * 
     * WHERE wc_id = ?1 AND local_relpath = ?2 AND op_depth > 0
     * ORDER BY op_depth DESC
     * LIMIT 1
     * 
     * or for base:
     * 
     * FROM nodes
     * WHERE wc_id = ?1 AND local_relpath = ?2 AND op_depth = 0

     * @author alex
     *
     */
    private static class SelectNodeToCopy extends SVNSqlJetSelectStatement {

        private long limit;
        private long depth;

        public SelectNodeToCopy(SVNSqlJetDb sDb, long depth) throws SVNException {
            super(sDb, SVNWCDbSchema.NODES);
            this.depth = depth;
        }

        @Override
        protected Object[] getWhere() throws SVNException {
            if (depth >= 0) {
                return new Object[] {getBind(1), getBind(2), depth};
            } 
            return super.getWhere();
        }
        
        @Override
        protected boolean isFilterPassed() throws SVNException {
            limit++;
            return super.isFilterPassed() && limit == 1;
        }

        @Override
        protected ISqlJetCursor openCursor() throws SVNException {
            if (depth == 0) {
                return super.openCursor();
            }
            try {
                return super.openCursor().reverse();
            } catch (SqlJetException e) {
                SVNSqlJetDb.createSqlJetError(e);
            }
            return null;
        }
        
        
    }
}
