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

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;

import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLock;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.internal.delta.SVNDeltaCombiner;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNLocationEntry;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class FSUpdateContext {

    private File myReportFile;
    private String myTarget;
    private OutputStream myReportOS;
    private FSFile myReportIS;
    private ISVNEditor myEditor;
    private long myTargetRevision;
    private SVNDepth myDepth;
    private PathInfo myCurrentPathInfo;
    private boolean ignoreAncestry;
    private boolean sendTextDeltas;
    private String myTargetPath;
    private boolean isSwitch;
    private boolean mySendCopyFromArgs;
    private FSRoot myTargetRoot;
    private LinkedList myRootsCache;
    private FSFS myFSFS;
    private FSRepository myRepository;
    private SVNDeltaGenerator myDeltaGenerator;
    private SVNDeltaCombiner myDeltaCombiner;

    public FSUpdateContext(FSRepository repository, FSFS owner, long revision, File reportFile, 
            String target, String targetPath, boolean isSwitch, SVNDepth depth, 
            boolean ignoreAncestry, boolean textDeltas, boolean sendCopyFromArgs, 
            ISVNEditor editor) {
        myRepository = repository;
        myFSFS = owner;
        myTargetRevision = revision;
        myReportFile = reportFile;
        myTarget = target;
        myEditor = editor;
        myDepth = depth;
        this.ignoreAncestry = ignoreAncestry;
        sendTextDeltas = textDeltas;
        myTargetPath = targetPath;
        this.isSwitch = isSwitch;
        mySendCopyFromArgs = sendCopyFromArgs;
    }

    public void reset(FSRepository repository, FSFS owner, long revision, File reportFile, String target, String targetPath, boolean isSwitch, SVNDepth depth, boolean ignoreAncestry,
            boolean textDeltas, boolean sendCopyFrom, ISVNEditor editor) throws SVNException {
        dispose();
        myRepository = repository;
        myFSFS = owner;
        myTargetRevision = revision;
        myReportFile = reportFile;
        myTarget = target;
        myEditor = editor;
        myDepth = depth;
        this.ignoreAncestry = ignoreAncestry;
        sendTextDeltas = textDeltas;
        myTargetPath = targetPath;
        this.isSwitch = isSwitch;
        mySendCopyFromArgs = sendCopyFrom;
    }
    
    public void setTargetRoot(FSRoot root) {
        myTargetRoot = root;
    }

    public OutputStream getReportFileForWriting() throws SVNException {
        if (myReportOS == null) {
            myReportOS = SVNFileUtil.openFileForWriting(myReportFile);
        }
        return myReportOS;
    }

    private boolean isIgnoreAncestry() {
        return ignoreAncestry;
    }

    private boolean isSwitch() {
        return isSwitch;
    }

    private boolean isSendTextDeltas() {
        return sendTextDeltas;
    }

    private String getReportTarget() {
        return myTarget;
    }

    private String getReportTargetPath() {
        return myTargetPath;
    }

    public void dispose() throws SVNException {
        SVNFileUtil.closeFile(myReportOS);
        myReportOS = null;

        if (myReportIS != null) {
            myReportIS.close();
            myReportIS = null;
        }

        if (myReportFile != null) {
            SVNFileUtil.deleteFile(myReportFile);
            myReportFile = null;
        }

        if (myDeltaCombiner != null) {
            myDeltaCombiner.reset();
        }

        myTargetRoot = null;
        myRootsCache = null;
    }

    private ISVNEditor getEditor() {
        return myEditor;
    }

    private long getTargetRevision() {
        return myTargetRevision;
    }

    private PathInfo getNextPathInfo() throws IOException, SVNException {
        if (myReportIS == null) {
            myReportIS = new FSFile(myReportFile);
        }
        myCurrentPathInfo = myReportIS.readPathInfoFromReportFile();
        return myCurrentPathInfo;
    }

    private PathInfo getCurrentPathInfo() {
        return myCurrentPathInfo;
    }

    private FSRoot getTargetRoot() throws SVNException {
        if (myTargetRoot == null) {
            myTargetRoot = myFSFS.createRevisionRoot(myTargetRevision);
        }
        return myTargetRoot;
    }

    private LinkedList getRootsCache() {
        if (myRootsCache == null) {
            myRootsCache = new LinkedList();
        }
        return myRootsCache;
    }

    private FSRevisionRoot getSourceRoot(long revision) throws SVNException {
        LinkedList cache = getRootsCache();
        FSRevisionRoot root = null;
        int i = 0;

        for (; i < cache.size() && i < 10; i++) {
            root = (FSRevisionRoot) myRootsCache.get(i);
            if (root.getRevision() == revision) {
                if (i != 0) {
                    myRootsCache.remove(i);
                    myRootsCache.addFirst(root);
                }
                break;
            }
            root = null;
        }

        if (root == null) {
            if (i == 10) {
                myRootsCache.removeLast();
            }
            root = myFSFS.createRevisionRoot(revision);
            myRootsCache.addFirst(root);
        }

        return root;
    }

    public void drive() throws SVNException {
        OutputStream reportOS = getReportFileForWriting();
        try {
            reportOS.write('-');
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        } finally {
            SVNFileUtil.closeFile(reportOS);
            myReportOS = null;
        }

        PathInfo info = null;

        try {
            info = getNextPathInfo();
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        }

        if (info == null || !info.getPath().equals(getReportTarget()) || info.getLinkPath() != null || FSRepository.isInvalidRevision(info.getRevision())) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_REVISION_REPORT, "Invalid report for top level of working copy");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        long sourceRevision = info.getRevision();
        PathInfo lookahead = null;

        try {
            lookahead = getNextPathInfo();
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        }

        if (lookahead != null && lookahead.getPath().equals(getReportTarget())) {
            if ("".equals(getReportTarget())) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_REVISION_REPORT, "Two top-level reports with no target");
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }
            if (lookahead.getRevision() < 0) {
                lookahead.myDepth = info.getDepth();
            }

            info = lookahead;

            try {
                getNextPathInfo();
            } catch (IOException ioe) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
                SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
            }
        }

        String fullTargetPath = getReportTargetPath();
        String fullSourcePath = myRepository != null ?
                SVNPathUtil.getAbsolutePath(SVNPathUtil.append(myRepository.getRepositoryPath(""), getReportTarget())) :
                SVNPathUtil.getAbsolutePath(getReportTarget());
        FSEntry targetEntry = fakeDirEntry(fullTargetPath, getTargetRoot());
        FSRevisionRoot srcRoot = getSourceRoot(sourceRevision);
        FSEntry sourceEntry = fakeDirEntry(fullSourcePath, srcRoot);

        if (FSRepository.isValidRevision(info.getRevision()) && info.getLinkPath() == null && sourceEntry == null) {
            fullSourcePath = null;
        }

        if ("".equals(getReportTarget()) && targetEntry == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_PATH_SYNTAX, "Target path ''{0}'' does not exist", getReportTargetPath());
            SVNErrorManager.error(err, SVNLogType.FSFS);
        } else if ("".equals(getReportTarget()) && (sourceEntry == null || sourceEntry.getType() != SVNNodeKind.DIR || 
                targetEntry.getType() != SVNNodeKind.DIR)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_PATH_SYNTAX, "Cannot replace a directory from within");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        if (myDeltaGenerator == null) {
            myDeltaGenerator = new SVNDeltaGenerator();
        }

        if (myDeltaCombiner == null) {
            myDeltaCombiner = new SVNDeltaCombiner();
        }

        getEditor().targetRevision(getTargetRevision());
        getEditor().openRoot(sourceRevision);

        if ("".equals(getReportTarget())) {
            diffDirs(sourceRevision, fullSourcePath, fullTargetPath, "", info.isStartEmpty(), info.getDepth(), myDepth);
        } else {
            updateEntry(sourceRevision, fullSourcePath, sourceEntry, fullTargetPath, targetEntry, getReportTarget(), info, info.getDepth(), myDepth);
        }

        getEditor().closeDir();
        getEditor().closeEdit();
    }

    private void diffDirs(long sourceRevision, String sourcePath, String targetPath, String editPath, boolean startEmpty, SVNDepth wcDepth, SVNDepth requestedDepth) throws SVNException {
        diffProplists(sourceRevision, startEmpty == true ? null : sourcePath, editPath, targetPath, null, true);
        Map sourceEntries = null;
        
        if (requestedDepth.compareTo(SVNDepth.EMPTY) > 0 || 
                requestedDepth == SVNDepth.UNKNOWN) {
            
            if (sourcePath != null && !startEmpty) {
                FSRevisionRoot sourceRoot = getSourceRoot(sourceRevision);
                FSRevisionNode sourceNode = sourceRoot.getRevisionNode(sourcePath);
                sourceEntries = new SVNHashMap(sourceNode.getDirEntries(myFSFS));
            }
            FSRevisionNode targetNode = getTargetRoot().getRevisionNode(targetPath);

            Map targetEntries = new SVNHashMap(targetNode.getDirEntries(myFSFS));

            while (true) {
                Object[] nextInfo = fetchPathInfo(editPath);
                String entryName = (String) nextInfo[0];
                if (entryName == null) {
                    break;
                }
                PathInfo pathInfo = (PathInfo) nextInfo[1];
                if (pathInfo != null && FSRepository.isInvalidRevision(pathInfo.getRevision()) && pathInfo.getDepth() != SVNDepth.EXCLUDE) {
                    if (sourceEntries != null) {
                        sourceEntries.remove(entryName);
                    }
                    continue;
                }

                String entryEditPath = SVNPathUtil.append(editPath, entryName);
                String entryTargetPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(targetPath, entryName));
                FSEntry targetEntry = (FSEntry) targetEntries.get(entryName);
                String entrySourcePath = sourcePath != null ? SVNPathUtil.getAbsolutePath(SVNPathUtil.append(sourcePath, entryName)) : null;
                FSEntry sourceEntry = sourceEntries != null ? (FSEntry) sourceEntries.get(entryName) : null;
                
                if ((pathInfo == null || pathInfo.getDepth() != SVNDepth.EXCLUDE) && (requestedDepth != SVNDepth.FILES || ((targetEntry == null || 
                        targetEntry.getType() != SVNNodeKind.DIR) && (sourceEntry == null || sourceEntry.getType() != SVNNodeKind.DIR)))) {
                    updateEntry(sourceRevision, entrySourcePath, sourceEntry, 
                                entryTargetPath, targetEntry, entryEditPath, 
                                pathInfo, pathInfo != null ? pathInfo.getDepth() : 
                                getDepthBelow(wcDepth), getDepthBelow(requestedDepth));
                }
                
                targetEntries.remove(entryName);
                if (sourceEntries != null && (pathInfo == null || pathInfo.getDepth() != SVNDepth.EXCLUDE || targetEntry != null)) {
                    sourceEntries.remove(entryName);
                }
            }

            if (sourceEntries != null) {
                FSEntry[] srcEntries = (FSEntry[]) new ArrayList(sourceEntries.values()).toArray(new FSEntry[sourceEntries.size()]);
                for (int i = 0; i < srcEntries.length; i++) {
                    FSEntry srcEntry = srcEntries[i];
                    if (targetEntries.get(srcEntry.getName()) == null) {
                        if (srcEntry.getType() == SVNNodeKind.FILE && 
                                wcDepth.compareTo(SVNDepth.FILES) < 0) {
                            continue;
                        }
                        if (srcEntry.getType() == SVNNodeKind.DIR &&
                                (wcDepth.compareTo(SVNDepth.IMMEDIATES) < 0 || 
                                        requestedDepth == SVNDepth.FILES)) {
                            continue;
                        }
                        String entryEditPath = SVNPathUtil.append(editPath, srcEntry.getName());
                        long deletedRev = getDeletedRevision(SVNPathUtil.append(targetPath, 
                                                                    srcEntry.getName()), 
                                                                    sourceRevision, getTargetRevision());
                        getEditor().deleteEntry(entryEditPath, deletedRev);
                    }
                }
            }

            for (Iterator tgts = targetEntries.values().iterator(); tgts.hasNext();) {
                FSEntry tgtEntry = (FSEntry) tgts.next();
                FSEntry srcEntry = null;
                String entrySourcePath = null;
                if (!isDepthUpgrade(wcDepth, requestedDepth, tgtEntry.getType())) {
                    if (tgtEntry.getType() == SVNNodeKind.FILE && 
                        requestedDepth == SVNDepth.UNKNOWN &&
                        wcDepth.compareTo(SVNDepth.FILES) < 0) {
                        continue;
                    }
                    if (tgtEntry.getType() == SVNNodeKind.DIR && 
                        (wcDepth.compareTo(SVNDepth.IMMEDIATES) < 0 || 
                        requestedDepth == SVNDepth.FILES)) {
                        continue;
                    }
                    srcEntry = sourceEntries != null ? (FSEntry) sourceEntries.get(tgtEntry.getName()) : null;                    
                    entrySourcePath = srcEntry != null ? SVNPathUtil.getAbsolutePath(SVNPathUtil.append(sourcePath, tgtEntry.getName())) : null;
                }

                String entryEditPath = SVNPathUtil.append(editPath, tgtEntry.getName());
                String entryTargetPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(targetPath, tgtEntry.getName()));
                updateEntry(sourceRevision, entrySourcePath, srcEntry, entryTargetPath, 
                            tgtEntry, entryEditPath, null, getDepthBelow(wcDepth), 
                            getDepthBelow(requestedDepth));
            }
        }
    }

    private boolean isDepthUpgrade(SVNDepth wcDepth, SVNDepth requestedDepth, SVNNodeKind kind) {
        if (requestedDepth == SVNDepth.UNKNOWN || 
                wcDepth == SVNDepth.IMMEDIATES) {
            return false;
        }
        int compareResult = requestedDepth.compareTo(wcDepth);
        if (compareResult <= 0) {
            return false;
        }
        
        if (kind == SVNNodeKind.FILE && wcDepth == SVNDepth.FILES) {
            return false;
        }
        if (kind == SVNNodeKind.DIR && wcDepth == SVNDepth.EMPTY &&
                requestedDepth == SVNDepth.FILES) {
            return false;
        }
        return true;
    }
    
    private void diffFiles(long sourceRevision, String sourcePath, String targetPath, String editPath, String lockToken) throws SVNException {
        diffProplists(sourceRevision, sourcePath, editPath, targetPath, lockToken, false);
        String sourceHexDigest = null;
        FSRevisionRoot sourceRoot = null;
        if (sourcePath != null) {
            sourceRoot = getSourceRoot(sourceRevision);

            boolean changed = false;
            if (isIgnoreAncestry()) {
                changed = FSRepositoryUtil.checkFilesDifferent(sourceRoot, sourcePath, getTargetRoot(), 
                        targetPath, myDeltaCombiner);
            } else {
                changed = FSRepositoryUtil.areFileContentsChanged(sourceRoot, sourcePath, getTargetRoot(), targetPath);
            }
            if (!changed) {
                return;
            }
            FSRevisionNode sourceNode = sourceRoot.getRevisionNode(sourcePath);
            sourceHexDigest = sourceNode.getFileMD5Checksum();
        }
        
        FSRepositoryUtil.sendTextDelta(getEditor(), editPath, sourcePath, sourceHexDigest, 
                sourceRoot, targetPath, getTargetRoot(), isSendTextDeltas(), myDeltaCombiner, 
                myDeltaGenerator, myFSFS);
    }

    private void updateEntry(long sourceRevision, String sourcePath, FSEntry sourceEntry, 
            String targetPath, FSEntry targetEntry, String editPath, PathInfo pathInfo, 
            SVNDepth wcDepth, SVNDepth requestedDepth) throws SVNException {
        if (pathInfo != null && pathInfo.getLinkPath() != null && !isSwitch()) {
            targetPath = pathInfo.getLinkPath();
            targetEntry = fakeDirEntry(targetPath, getTargetRoot());
        }

        if (pathInfo != null && FSRepository.isInvalidRevision(pathInfo.getRevision())) {
            sourcePath = null;
            sourceEntry = null;
        } else if (pathInfo != null && sourcePath != null) {
            sourcePath = pathInfo.getLinkPath() != null ? pathInfo.getLinkPath() : sourcePath;
            sourceRevision = pathInfo.getRevision();
            FSRevisionRoot srcRoot = getSourceRoot(sourceRevision);
            sourceEntry = fakeDirEntry(sourcePath, srcRoot);
        }

        if (sourcePath != null && sourceEntry == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "Working copy path ''{0}'' does not exist in repository", editPath);
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        boolean related = false;
        if (sourceEntry != null && targetEntry != null && sourceEntry.getType() == targetEntry.getType()) {
            int distance = sourceEntry.getId().compareTo(targetEntry.getId());
            if (distance == 0 && !PathInfo.isRelevant(getCurrentPathInfo(), editPath) && 
                            (requestedDepth.compareTo(wcDepth) <= 0 || targetEntry.getType() == SVNNodeKind.FILE)) {
                if (pathInfo == null) {
                    return;
                }
                if (!pathInfo.isStartEmpty()) {
                    if (pathInfo.getLockToken() == null) {
                        return;
                    }
                    SVNLock exitsingLock = myFSFS.getLock(targetPath, false, false);
                    if (exitsingLock != null && exitsingLock.getID().equals(pathInfo.getLockToken())) {
                        return;
                    }
                }
            }
            related = distance != -1 || isIgnoreAncestry();
        }

        if (sourceEntry != null && !related) {
            long deletedRev = getDeletedRevision(targetPath, sourceRevision, getTargetRevision());
            if (!SVNRevision.isValidRevisionNumber(deletedRev)) {
                SVNNodeKind kind = getTargetRoot().checkNodeKind(targetPath);
                if (kind != SVNNodeKind.NONE) {
                    deletedRev = getTargetRevision() - 1;
                }
            }
            getEditor().deleteEntry(editPath, deletedRev);
            sourcePath = null;
        }

        if (targetEntry == null) {
            skipPathInfo(editPath);
            return;
        }

        if (targetEntry.getType() == SVNNodeKind.DIR) {
            if (related) {
                getEditor().openDir(editPath, sourceRevision);
            } else {
                getEditor().addDir(editPath, null, SVNRepository.INVALID_REVISION);
            }
            diffDirs(sourceRevision, sourcePath, targetPath, editPath, pathInfo != null ? pathInfo.isStartEmpty() : false, wcDepth, requestedDepth);
            getEditor().closeDir();
        } else {
            if (related) {
                getEditor().openFile(editPath, sourceRevision);
                diffFiles(sourceRevision, sourcePath, targetPath, editPath, pathInfo != null ? pathInfo.getLockToken() : null);
            } else {
                SVNLocationEntry copyFromFile = addFileSmartly(editPath, targetPath);
                String copyFromPath = copyFromFile.getPath();
                long copyFromRevision = copyFromFile.getRevision();
                if (copyFromPath == null) {
                    diffFiles(sourceRevision, sourcePath, targetPath, editPath, 
                            pathInfo != null ? pathInfo.getLockToken() : null);
                } else {
                    diffFiles(copyFromRevision, copyFromPath, targetPath, editPath, 
                            pathInfo != null ? pathInfo.getLockToken() : null);
                }
            }
            FSRevisionNode targetNode = getTargetRoot().getRevisionNode(targetPath);
            String targetHexDigest = targetNode.getFileMD5Checksum();
            getEditor().closeFile(editPath, targetHexDigest);
        }
    }

    private long getDeletedRevision(String targetPath, long sourceRevision, long targetRevision) throws SVNException {
        if (isTransactionTarget()) {
            return getTargetRevision();
        }
        return myFSFS.getDeletedRevision(targetPath, sourceRevision, targetRevision);
    }
    
    private boolean isTransactionTarget() throws SVNException {
        return getTargetRoot() instanceof FSTransactionRoot;
    }

    private SVNLocationEntry addFileSmartly(String editPath, String originalPath) throws SVNException {
        String copyFromPath = null;
        long copyFromRevision = SVNRepository.INVALID_REVISION;
        if (mySendCopyFromArgs) {
            if (!isTransactionTarget()) {
                FSClosestCopy closestCopy = ((FSRevisionRoot) getTargetRoot()).getClosestCopy(originalPath);
                if (closestCopy != null) {
                    FSRevisionRoot closestCopyRoot = closestCopy.getRevisionRoot();
                    String closestCopyPath = closestCopy.getPath();
                    if (originalPath.equals(closestCopyPath)) {
                        FSRevisionNode closestCopyFromNode = closestCopyRoot.getRevisionNode(
                                closestCopyPath);
                        copyFromPath = closestCopyFromNode.getCopyFromPath();
                        copyFromRevision = closestCopyFromNode.getCopyFromRevision();
                    }
                }
            } else if (isTransactionTarget()) {
                FSTransactionRoot txn = (FSTransactionRoot) getTargetRoot();
                FSPathChange change = (FSPathChange) txn.getChangedPaths().get(originalPath);
                if (change != null) {
                    copyFromPath = change.getCopyPath();
                    copyFromRevision = change.getCopyRevision();
                }
            }
        }
        myEditor.addFile(editPath, copyFromPath, copyFromRevision);
        return new SVNLocationEntry(copyFromRevision, copyFromPath);
    }
    
    private Map computeMetaProperties(long revision) throws SVNException {
        Map metaProperties = new SVNHashMap();
        if (FSRepository.isValidRevision(revision)) {
            SVNProperties entryProps = myFSFS.compoundMetaProperties(revision);
            metaProperties.put(SVNProperty.COMMITTED_REVISION, entryProps.getSVNPropertyValue(SVNProperty.COMMITTED_REVISION));
            metaProperties.put(SVNProperty.COMMITTED_DATE, entryProps.getSVNPropertyValue(SVNProperty.COMMITTED_DATE));
            metaProperties.put(SVNProperty.LAST_AUTHOR, entryProps.getSVNPropertyValue(SVNProperty.LAST_AUTHOR));
            metaProperties.put(SVNProperty.UUID, entryProps.getSVNPropertyValue(SVNProperty.UUID));
        } else if (!FSRepository.isValidRevision(revision) && isTransactionTarget()) {
            FSTransactionRoot txnRoot = (FSTransactionRoot) getTargetRoot();
            SVNProperties txnProperties = myFSFS.getTransactionProperties(txnRoot.getTxnID());
            metaProperties.put(SVNProperty.COMMITTED_REVISION, SVNPropertyValue.create(Long.toString(getTargetRevision())));
            metaProperties.put(SVNProperty.COMMITTED_DATE, txnProperties.getSVNPropertyValue(SVNRevisionProperty.DATE));
            metaProperties.put(SVNProperty.LAST_AUTHOR, txnProperties.getSVNPropertyValue(SVNRevisionProperty.AUTHOR));
            metaProperties.put(SVNProperty.UUID, SVNPropertyValue.create(myFSFS.getUUID()));
        }  else {
            metaProperties = null;
        }
        return metaProperties;
    }
    
    private void diffProplists(long sourceRevision, String sourcePath, String editPath, String targetPath, String lockToken, boolean isDir) throws SVNException {
        FSRevisionNode targetNode = getTargetRoot().getRevisionNode(targetPath);
        long createdRevision = targetNode.getCreatedRevision();
        Map metaProperties = computeMetaProperties(createdRevision);
        if (metaProperties != null) {
            SVNPropertyValue committedRevision = (SVNPropertyValue) metaProperties.get(SVNProperty.COMMITTED_REVISION);
            changeProperty(editPath, SVNProperty.COMMITTED_REVISION, committedRevision, isDir);
            SVNPropertyValue committedDate = (SVNPropertyValue) metaProperties.get(SVNProperty.COMMITTED_DATE);
            if (committedDate != null || sourcePath != null) {
                changeProperty(editPath, SVNProperty.COMMITTED_DATE, committedDate, isDir);
            }
            SVNPropertyValue lastAuthor = (SVNPropertyValue) metaProperties.get(SVNProperty.LAST_AUTHOR);
            if (lastAuthor != null || sourcePath != null) {
                changeProperty(editPath, SVNProperty.LAST_AUTHOR, lastAuthor, isDir);
            }
            SVNPropertyValue uuid = (SVNPropertyValue) metaProperties.get(SVNProperty.UUID);
            if (uuid != null || sourcePath != null) {
                changeProperty(editPath, SVNProperty.UUID, uuid, isDir);
            }
        } 
        
        if (lockToken != null) {
            SVNLock lock = myFSFS.getLockHelper(targetPath, false);
            if (lock == null || !lockToken.equals(lock.getID())) {
                changeProperty(editPath, SVNProperty.LOCK_TOKEN,  null, isDir);
            }
        }

        SVNProperties sourceProps = null;
        if (sourcePath != null) {
            FSRevisionRoot sourceRoot = getSourceRoot(sourceRevision);
            FSRevisionNode sourceNode = sourceRoot.getRevisionNode(sourcePath);
            boolean propsChanged = !FSRepositoryUtil.arePropertiesEqual(sourceNode, targetNode);
            if (!propsChanged) {
                return;
            }
            sourceProps = sourceNode.getProperties(myFSFS);
        } else {
            sourceProps = new SVNProperties();
        }

        SVNProperties targetProps = targetNode.getProperties(myFSFS);
        SVNProperties propsDiffs = FSRepositoryUtil.getPropsDiffs(sourceProps, targetProps);
        Object[] names = propsDiffs.nameSet().toArray();
        for (int i = 0; i < names.length; i++) {
            String propName = (String) names[i];
            changeProperty(editPath, propName, propsDiffs.getSVNPropertyValue(propName), isDir);
        }
    }

    private SVNDepth getDepthBelow(SVNDepth depth) {
        return depth == SVNDepth.IMMEDIATES ? SVNDepth.EMPTY : depth;
    }
    
    private Object[] fetchPathInfo(String prefix) throws SVNException {
        Object[] result = new Object[2];
        PathInfo pathInfo = getCurrentPathInfo();
        if (!PathInfo.isRelevant(pathInfo, prefix)) {
            result[0] = null;
            result[1] = null;
        } else {
            String relPath = "".equals(prefix) ? pathInfo.getPath() : pathInfo.getPath().substring(prefix.length() + 1);
            if (relPath.indexOf('/') != -1) {
                result[0] = relPath.substring(0, relPath.indexOf('/'));
                result[1] = null;
            } else {
                result[0] = relPath;
                result[1] = pathInfo;
                try {
                    getNextPathInfo();
                } catch (IOException ioe) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                }
            }
        }
        return result;
    }

    private void changeProperty(String path, String name, SVNPropertyValue value, boolean isDir) throws SVNException {
        if (isDir) {
            getEditor().changeDirProperty(name, value);
        } else {
            getEditor().changeFileProperty(path, name, value);
        }
    }

    private FSEntry fakeDirEntry(String reposPath, FSRoot root) throws SVNException {
        if (root.checkNodeKind(reposPath) == SVNNodeKind.NONE) {
            return null;
        }

        FSRevisionNode node = root.getRevisionNode(reposPath);
        FSEntry dirEntry = new FSEntry(node.getId(), node.getType(), SVNPathUtil.tail(node.getCreatedPath()));
        return dirEntry;
    }

    private void skipPathInfo(String prefix) throws SVNException {
        while (PathInfo.isRelevant(getCurrentPathInfo(), prefix)) {
            try {
                getNextPathInfo();
            } catch (IOException ioe) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
                SVNErrorManager.error(err, SVNLogType.FSFS);
            }
        }
    }

    private void writeSingleString(String s, OutputStream out) throws IOException {
        if (s != null) {
            byte[] b = s.getBytes("UTF-8");
            out.write('+');
            out.write(String.valueOf(b.length).getBytes("UTF-8"));
            out.write(':');
            out.write(b);
        } else {
            out.write('-');
        }
    }
    
    public void writePathInfoToReportFile(String path, String linkPath, String lockToken, long revision, boolean startEmpty, SVNDepth depth) throws SVNException {
        if (depth == null || depth == SVNDepth.UNKNOWN) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS, "Unsupported report depth ''{0}''", depth != null ? depth.getName() : "null");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        String anchorRelativePath = SVNPathUtil.append(getReportTarget(), path);
        String revisionRep = FSRepository.isValidRevision(revision) ? "+" + String.valueOf(revision) + ":" : "-";
        String depthRep = "-";//infinity by default
        if (depth == SVNDepth.EXCLUDE || depth == SVNDepth.EMPTY || depth == SVNDepth.FILES || depth == SVNDepth.IMMEDIATES) {
            depthRep = "+" + getDepthLetter(depth);
        } 
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            writeSingleString(anchorRelativePath, baos);
            writeSingleString(linkPath, baos);
            baos.write(revisionRep.getBytes("UTF-8"));
            baos.write(depthRep.getBytes("UTF-8"));
            baos.write(startEmpty ? '+' : '-');
            writeSingleString(lockToken, baos);
            OutputStream reportOS = getReportFileForWriting();
            reportOS.write(baos.toByteArray());
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        }
    }

    public String getDepthLetter(SVNDepth depth) throws SVNException {
        if (depth == SVNDepth.EXCLUDE) {
            return "X";
        }
        if (depth == SVNDepth.EMPTY) {
            return "E";
        }
        if (depth == SVNDepth.FILES) {
            return "F";
        }
        if (depth == SVNDepth.IMMEDIATES) {
            return "M";
        }
        
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS, "Unsupported report depth ''{0}''", SVNDepth.asString(depth));
        SVNErrorManager.error(err, SVNLogType.FSFS);
        return null;
    }
}
