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

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNCancelException;
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.SVNLogEntry;
import org.tmatesoft.svn.core.SVNLogEntryPath;
import org.tmatesoft.svn.core.SVNMergeInfo;
import org.tmatesoft.svn.core.SVNMergeInfoInheritance;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
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.internal.wc.SVNPropertiesManager;
import org.tmatesoft.svn.core.internal.wc.SVNWCManager;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNWCAccess;
import org.tmatesoft.svn.core.io.SVNLocationEntry;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.DefaultSVNRepositoryPool;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNPathListHandler;
import org.tmatesoft.svn.core.wc.ISVNRepositoryPool;
import org.tmatesoft.svn.core.wc.SVNCommitItem;
import org.tmatesoft.svn.core.wc.SVNDiffClient;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNUpdateClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import org.tmatesoft.svn.util.ISVNDebugLog;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * The <b>SVNBasicClient</b> is the base class of all <b>SVN</b>*<b>Client</b>
 * classes that provides a common interface and realization.
 * <p>
 * All of <b>SVN</b>*<b>Client</b> classes use inherited methods of
 * <b>SVNBasicClient</b> to access Working Copies metadata, to create a driver
 * object to access a repository if it's necessary, etc. In addition
 * <b>SVNBasicClient</b> provides some interface methods - such as those that
 * allow you to set your {@link ISVNEventHandler event handler}, obtain run-time
 * configuration options, and others.
 * 
 * @version 1.3
 * @author TMate Software Ltd.
 * @since 1.2
 */
public class SVNBasicDelegate implements ISVNEventHandler {

    private ISVNRepositoryPool myRepositoryPool;
    private ISVNOptions myOptions;
    private ISVNEventHandler myEventDispatcher;
    private List myPathPrefixesStack;
    private boolean myIsIgnoreExternals;
    private boolean myIsLeaveConflictsUnresolved;
    private ISVNDebugLog myDebugLog;
    private ISVNPathListHandler myPathListHandler;

    public SVNBasicDelegate(final ISVNAuthenticationManager authManager, ISVNOptions options) {
        this(new DefaultSVNRepositoryPool(authManager == null ? SVNWCUtil.createDefaultAuthenticationManager() : authManager, options, 0, false), options);
    }

    protected SVNBasicDelegate(ISVNRepositoryPool repositoryPool, ISVNOptions options) {
        myRepositoryPool = repositoryPool;
        setOptions(options);
        myPathPrefixesStack = new LinkedList();
    }

    /**
     * Gets run-time configuration options used by this object.
     * 
     * @return the run-time options being in use
     */
    public ISVNOptions getOptions() {
        return this.myOptions;
    }

    /**
     * Sets run-time global configuration options to this object.
     * 
     * @param options
     *            the run-time configuration options
     */
    public void setOptions(ISVNOptions options) {
        this.myOptions = options;
    }

    /**
     * Sets externals definitions to be ignored or not during operations.
     * <p>
     * For example, if external definitions are set to be ignored then a
     * checkout operation won't fetch them into a Working Copy.
     * 
     * @param ignore
     *            <span class="javakeyword">true</span> to ignore externals
     *            definitions, <span class="javakeyword">false</span> - not to
     * @see #isIgnoreExternals()
     */
    public void setIgnoreExternals(boolean ignore) {
        myIsIgnoreExternals = ignore;
    }

    /**
     * Determines if externals definitions are ignored.
     * 
     * @return <span class="javakeyword">true</span> if ignored, otherwise <span
     *         class="javakeyword">false</span>
     * @see #setIgnoreExternals(boolean)
     */
    public boolean isIgnoreExternals() {
        return myIsIgnoreExternals;
    }

    /**
     * Sets (or unsets) all conflicted working files to be untouched by update
     * and merge operations.
     * <p>
     * By default when a file receives changes from the repository that are in
     * conflict with local edits, an update operation places two sections for
     * each conflicting snatch into the working file one of which is a user's
     * local edit and the second is the one just received from the repository.
     * Like this:
     * 
     * <pre class="javacode">
     * <<<<<<< .mine
     * user's text
     * =======
     * received text
     * >>>>>>> .r2
     * </pre>
     * 
     * <br />
     * Also the operation creates three temporary files that appear in the same
     * directory as the working file. Now if you call this method with
     * <code>leave</code> set to <span class="javakeyword">true</span>, an
     * update will still create temporary files but won't place those two
     * sections into your working file. And this behaviour also concerns merge
     * operations: any merging to a conflicted file will be prevented. In
     * addition if there is any registered event handler for an
     * <b>SVNDiffClient</b> or <b>SVNUpdateClient</b> instance then the handler
     * will be dispatched an event with the status type set to
     * {@link SVNStatusType#CONFLICTED_UNRESOLVED}.
     * <p>
     * The default value is <span class="javakeyword">false</span> until a
     * caller explicitly changes it calling this method.
     * 
     * @param leave
     *            <span class="javakeyword">true</span> to prevent conflicted
     *            files from merging (all merging operations will be skipped),
     *            otherwise <span class="javakeyword">false</span>
     * @see #isLeaveConflictsUnresolved()
     * @see SVNUpdateClient
     * @see SVNDiffClient
     * @see ISVNEventHandler
     * @deprecated this method should not be used anymore
     */
    public void setLeaveConflictsUnresolved(boolean leave) {
        myIsLeaveConflictsUnresolved = leave;
    }

    /**
     * Determines if conflicted files should be left unresolved preventing from
     * merging their contents during update and merge operations.
     * 
     * @return <span class="javakeyword">true</span> if conflicted files are set
     *         to be prevented from merging, <span
     *         class="javakeyword">false</span> if there's no such restriction
     * @see #setLeaveConflictsUnresolved(boolean)
     * @deprecated this method should not be used anymore
     */
    public boolean isLeaveConflictsUnresolved() {
        return myIsLeaveConflictsUnresolved;
    }

    /**
     * Sets an event handler for this object. This event handler will be
     * dispatched {@link SVNEvent} objects to provide detailed information about
     * actions and progress state of version control operations performed by
     * <b>do</b>*<b>()</b> methods of <b>SVN</b>*<b>Client</b> classes.
     * 
     * @param dispatcher
     *            an event handler
     */
    public void setEventHandler(ISVNEventHandler dispatcher) {
        myEventDispatcher = dispatcher;
    }

    /**
     * Sets a path list handler implementation to this object.
     * 
     * @param handler
     *            handler implementation
     * @since 1.2.0
     */
    public void setPathListHandler(ISVNPathListHandler handler) {
        this.myPathListHandler = handler;
    }

    /**
     * Sets a logger to write debug log information to.
     * 
     * @param log
     *            a debug logger
     */
    public void setDebugLog(ISVNDebugLog log) {
        this.myDebugLog = log;
    }

    /**
     * Returns the debug logger currently in use.
     * <p>
     * If no debug logger has been specified by the time this call occurs, a
     * default one (returned by
     * <code>org.tmatesoft.svn.util.SVNDebugLog.getDefaultLog()</code>) will be
     * created and used.
     * 
     * @return a debug logger
     */
    public ISVNDebugLog getDebugLog() {
        return this.myDebugLog;
    }

    /**
     * Returns the root of the repository.
     * <p/>
     * If <code>path</code> is not <span class="javakeyword">null</span> and
     * <code>pegRevision</code> is either {@link SVNRevision#WORKING} or
     * {@link SVNRevision#BASE}, then attempts to fetch the repository root from
     * the working copy represented by <code>path</code>. If these conditions
     * are not met or if the repository root is not recorded in the working
     * copy, then a repository connection is established and the repository root
     * is fetched from the session.
     * <p/>
     * When fetching the repository root from the working copy and if
     * <code>access</code> is <span class="javakeyword">null</span>, a new
     * working copy access will be created and the working copy will be opened
     * non-recursively for reading only.
     * <p/>
     * All necessary cleanup (session or|and working copy close) will be
     * performed automatically as the routine finishes.
     * 
     * @param path
     *            working copy path
     * @param url
     *            repository url
     * @param pegRevision
     *            revision in which the target is valid
     * @param adminArea
     *            working copy administrative area object
     * @param access
     *            working copy access object
     * @return repository root url
     * @throws SVNException
     * @since 1.2.0
     */
    public SVNURL getReposRoot(File path, SVNURL url, SVNRevision pegRevision, SVNAdminArea adminArea, SVNWCAccess access) throws SVNException {
        SVNURL reposRoot = null;
        if (path != null && (pegRevision == SVNRevision.WORKING || pegRevision == SVNRevision.BASE)) {
            if (access == null) {
                access = createWCAccess();
            }
            boolean needCleanUp = false;
            try {
                if (adminArea == null) {
                    adminArea = access.probeOpen(path, false, 0);
                    needCleanUp = true;
                }
                SVNEntry entry = access.getVersionedEntry(path, false);
                url = getEntryLocation(path, entry, null, SVNRevision.UNDEFINED);
                reposRoot = entry.getRepositoryRootURL();
            } finally {
                if (needCleanUp) {
                    access.closeAdminArea(path);
                }
            }
        }
        if (reposRoot == null) {
            SVNRepository repos = null;
            try {
                repos = createRepository(url, path, null, pegRevision, pegRevision, null);
                reposRoot = repos.getRepositoryRoot(true);
            } finally {
                if (repos != null) {
                    repos.closeSession();
                }
            }
        }
        return reposRoot;
    }

    protected void sleepForTimeStamp() {
        if (myPathPrefixesStack == null || myPathPrefixesStack.isEmpty()) {
            SVNFileUtil.sleepForTimestamp();
        }
    }

    protected SVNRepository createRepository(SVNURL url, File path, SVNWCAccess access, boolean mayReuse) throws SVNException {
        String uuid = null;
        if (access != null) {
            SVNEntry entry = access.getEntry(path, false);
            if (entry != null) {
                uuid = entry.getUUID();
            }
        }
        return createRepository(url, uuid, mayReuse);
    }

    protected SVNRepository createRepository(SVNURL url, String uuid, boolean mayReuse) throws SVNException {
        SVNRepository repository = null;
        if (myRepositoryPool == null) {
            repository = SVNRepositoryFactory.create(url, null);
        } else {
            repository = myRepositoryPool.createRepository(url, mayReuse);
        }
        if (uuid != null) {
            String reposUUID = repository.getRepositoryUUID(true);
            if (!uuid.equals(reposUUID)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_UUID_MISMATCH, "Repository UUID ''{0}'' doesn''t match expected UUID ''{1}''", new Object[] {
                        reposUUID, uuid
                });
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        repository.setDebugLog(getDebugLog());
        repository.setCanceller(getEventDispatcher());
        return repository;
    }

    protected ISVNRepositoryPool getRepositoryPool() {
        return this.myRepositoryPool;
    }

    protected void dispatchEvent(SVNEvent event) throws SVNException {
        dispatchEvent(event, ISVNEventHandler.UNKNOWN);
    }

    protected void dispatchEvent(SVNEvent event, double progress) throws SVNException {
        if (myEventDispatcher != null) {
            try {
                myEventDispatcher.handleEvent(event, progress);
            } catch (SVNException e) {
                throw e;
            } catch (Throwable th) {
                SVNDebugLog.getDefaultLog().logSevere(SVNLogType.WC, th);
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "Error while dispatching event: {0}", new Object[] {
                    th.getMessage()
                }, SVNErrorMessage.TYPE_ERROR, th);
                SVNErrorManager.error(err, th, SVNLogType.DEFAULT);
            }
        }
    }

    /**
     * Removes or adds a path prefix. This method is not intended for users
     * (from an API point of view).
     * 
     * @param prefix
     *            a path prefix
     */
    public void setEventPathPrefix(String prefix) {
        if (prefix == null && !myPathPrefixesStack.isEmpty()) {
            myPathPrefixesStack.remove(myPathPrefixesStack.size() - 1);
        } else if (prefix != null) {
            myPathPrefixesStack.add(prefix);
        }
    }

    protected ISVNEventHandler getEventDispatcher() {
        return this.myEventDispatcher;
    }

    protected SVNWCAccess createWCAccess() {
        return createWCAccess(null);
    }

    protected SVNWCAccess createWCAccess(final String pathPrefix) {
        ISVNEventHandler eventHandler = null;
        if (pathPrefix != null) {
            eventHandler = new ISVNEventHandler() {

                public void handleEvent(SVNEvent event, double progress) throws SVNException {
                    dispatchEvent(event, progress);
                }

                public void checkCancelled() throws SVNCancelException {
                    SVNBasicDelegate.this.checkCancelled();
                }
            };
        } else {
            eventHandler = this;
        }
        SVNWCAccess access = SVNWCAccess.newInstance(eventHandler);
        access.setOptions(myOptions);
        return access;
    }

    /**
     * Dispatches events to the registered event handler (if any).
     * 
     * @param event
     *            the current event
     * @param progress
     *            progress state (from 0 to 1)
     * @throws SVNException
     */
    public void handleEvent(SVNEvent event, double progress) throws SVNException {
        dispatchEvent(event, progress);
    }

    /**
     * Handles a next working copy path with the {@link ISVNPathListHandler path
     * list handler} if any was provided to this object through
     * {@link #setPathListHandler(ISVNPathListHandler)}.
     * <p/>
     * Note: used by <code>SVNKit</code> internals.
     * 
     * @param path
     *            working copy path
     * @throws SVNException
     * @since 1.2.0
     */
    public void handlePathListItem(File path) throws SVNException {
        if (myPathListHandler != null && path != null) {
            myPathListHandler.handlePathListItem(path);
        }
    }

    /**
     * Redirects this call to the registered event handler (if any).
     * 
     * @throws SVNCancelException
     *             if the current operation was cancelled
     */
    public void checkCancelled() throws SVNCancelException {
        if (myEventDispatcher != null) {
            myEventDispatcher.checkCancelled();
        }
    }

    protected long getRevisionNumber(SVNRevision revision, SVNRepository repository, File path) throws SVNException {
        return getRevisionNumber(revision, null, repository, path);
    }

    protected long getRevisionNumber(SVNRevision revision, long[] latestRevisionNumber, SVNRepository repository, File path) throws SVNException {
        if (repository == null && (revision == SVNRevision.HEAD || revision.getDate() != null)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_RA_ACCESS_REQUIRED);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (revision.getNumber() >= 0) {
            return revision.getNumber();
        } else if (revision.getDate() != null) {
            return repository.getDatedRevision(revision.getDate());
        } else if (revision == SVNRevision.HEAD) {
            if (latestRevisionNumber != null && latestRevisionNumber.length > 0 && SVNRevision.isValidRevisionNumber(latestRevisionNumber[0])) {
                return latestRevisionNumber[0];
            }
            long latestRevision = repository.getLatestRevision();
            if (latestRevisionNumber != null && latestRevisionNumber.length > 0) {
                latestRevisionNumber[0] = latestRevision;
            }
            return latestRevision;
        } else if (!revision.isValid()) {
            return -1;
        } else if (revision == SVNRevision.COMMITTED || revision == SVNRevision.WORKING || revision == SVNRevision.BASE || revision == SVNRevision.PREVIOUS) {
            if (path == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_VERSIONED_PATH_REQUIRED);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            SVNWCAccess wcAccess = createWCAccess();
            wcAccess.probeOpen(path, false, 0);
            SVNEntry entry = null;
            try {
                entry = wcAccess.getVersionedEntry(path, false);
            } finally {
                wcAccess.close();
            }
            if (revision == SVNRevision.WORKING || revision == SVNRevision.BASE) {
                return entry.getRevision();
            }
            if (entry.getCommittedRevision() < 0) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Path ''{0}'' has no committed revision", path);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            return revision == SVNRevision.PREVIOUS ? entry.getCommittedRevision() - 1 : entry.getCommittedRevision();
        } else {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Unrecognized revision type requested for ''{0}''", path != null ? path : (Object) repository.getLocation());
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return -1;
    }

    protected SVNRepository createRepository(SVNURL url, File path, SVNAdminArea area, SVNRevision pegRevision, SVNRevision revision, long[] pegRev) throws SVNException {
        if (url == null) {
            SVNURL pathURL = getURL(path);
            if (pathURL == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "''{0}'' has no URL", path);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        SVNRevision startRevision = revision;
        SVNRevision[] resolvedRevisions = resolveRevisions(pegRevision, startRevision, url != null, true);
        pegRevision = resolvedRevisions[0];
        startRevision = resolvedRevisions[1];
        SVNRepositoryLocation[] locations = getLocations(url, path, null, pegRevision, startRevision, SVNRevision.UNDEFINED);
        url = locations[0].getURL();
        long actualRevision = locations[0].getRevisionNumber();
        SVNRepository repository = createRepository(url, area != null ? area.getRoot() : null, area != null ? area.getWCAccess() : null, true);
        actualRevision = getRevisionNumber(SVNRevision.create(actualRevision), repository, path);
        if (actualRevision < 0) {
            actualRevision = repository.getLatestRevision();
        }
        if (pegRev != null && pegRev.length > 0) {
            pegRev[0] = actualRevision;
        }
        return repository;
    }

    protected SVNRevision[] resolveRevisions(SVNRevision pegRevision, SVNRevision revision, boolean isURL, boolean noticeLocalModifications) {
        if (!pegRevision.isValid()) {
            if (isURL) {
                pegRevision = SVNRevision.HEAD;
            } else {
                if (noticeLocalModifications) {
                    pegRevision = SVNRevision.WORKING;
                } else {
                    pegRevision = SVNRevision.BASE;
                }
            }
        }
        if (!revision.isValid()) {
            revision = pegRevision;
        }
        return new SVNRevision[] {
                pegRevision, revision
        };
    }

    protected void elideMergeInfo(SVNWCAccess access, File path, SVNEntry entry, File wcElisionLimitPath) throws SVNException {
        if (wcElisionLimitPath == null || !wcElisionLimitPath.equals(path)) {
            Map mergeInfo = null;
            Map targetMergeInfo = null;
            boolean[] inherited = new boolean[1];
            targetMergeInfo = getWCMergeInfo(path, entry, wcElisionLimitPath, SVNMergeInfoInheritance.INHERITED, false, inherited);
            if (inherited[0] || targetMergeInfo == null) {
                return;
            }
            mergeInfo = getWCMergeInfo(path, entry, wcElisionLimitPath, SVNMergeInfoInheritance.NEAREST_ANCESTOR, false, inherited);
            if (mergeInfo == null && wcElisionLimitPath == null) {
                mergeInfo = getWCOrRepositoryMergeInfo(path, entry, SVNMergeInfoInheritance.NEAREST_ANCESTOR, inherited, true, null);
            }
            if (mergeInfo == null && wcElisionLimitPath != null) {
                return;
            }
            SVNMergeInfoUtil.elideMergeInfo(mergeInfo, targetMergeInfo, path, null, access);
        }
    }

    /**
     * @param path
     *            path relative to the repository location.
     */
    protected Map getReposMergeInfo(SVNRepository repository, String path, long revision, SVNMergeInfoInheritance inheritance, boolean squelchIncapable) throws SVNException {
        Map reposMergeInfo = null;
        try {
            reposMergeInfo = repository.getMergeInfo(new String[] {
                path
            }, revision, inheritance, false);
        } catch (SVNException svne) {
            if (!squelchIncapable || svne.getErrorMessage().getErrorCode() != SVNErrorCode.UNSUPPORTED_FEATURE) {
                throw svne;
            }
        }
        String rootRelativePath = getPathRelativeToRoot(null, repository.getLocation(), repository.getRepositoryRoot(false), null, repository);
        Map targetMergeInfo = null;
        if (reposMergeInfo != null) {
            SVNMergeInfo mergeInfo = (SVNMergeInfo) reposMergeInfo.get(rootRelativePath);
            if (mergeInfo != null) {
                targetMergeInfo = mergeInfo.getMergeSourcesToMergeLists();
            }
        }
        return targetMergeInfo;
    }

    protected Map getWCOrRepositoryMergeInfo(File path, SVNEntry entry, SVNMergeInfoInheritance inherit, boolean[] indirect, boolean reposOnly, SVNRepository repository) throws SVNException {
        Map mergeInfo = null;
        long targetRev[] = {
            -1
        };
        SVNURL url = getEntryLocation(path, entry, targetRev, SVNRevision.WORKING);
        long revision = targetRev[0];
        if (!reposOnly) {
            mergeInfo = getWCMergeInfo(path, entry, null, inherit, false, indirect);
        }
        if (mergeInfo == null) {
            if (!entry.isScheduledForAddition()) {
                Map fileToProp = SVNPropertiesManager.getWorkingCopyPropertyValues(path, entry, SVNProperty.MERGE_INFO, SVNDepth.EMPTY, true);
                SVNPropertyValue mergeInfoProp = (SVNPropertyValue) fileToProp.get(path);
                if (mergeInfoProp == null) {
                    SVNURL oldLocation = null;
                    boolean closeRepository = false;
                    Map reposMergeInfo = null;
                    String repositoryPath = null;
                    try {
                        if (repository == null) {
                            repository = createRepository(url, null, null, false);
                            closeRepository = true;
                        }
                        repositoryPath = getPathRelativeToSession(url, null, repository);
                        oldLocation = repository.getLocation();
                        if (repositoryPath == null) {
                            repositoryPath = "";
                            repository.setLocation(url, false);
                        }
                        reposMergeInfo = getReposMergeInfo(repository, repositoryPath, revision, inherit, true);
                    } finally {
                        if (closeRepository) {
                            repository.closeSession();
                        } else if (oldLocation != null) {
                            repository.setLocation(oldLocation, false);
                        }
                    }
                    if (reposMergeInfo != null) {
                        indirect[0] = true;
                        mergeInfo = reposMergeInfo;
                    }
                }
            }
        }
        return mergeInfo;
    }

    /**
     * mergeInfo must not be null!
     */
    protected Map getWCMergeInfo(File path, SVNEntry entry, File limitPath, SVNMergeInfoInheritance inherit, boolean base, boolean[] inherited) throws SVNException {
        String walkPath = "";
        Map wcMergeInfo = null;
        SVNWCAccess wcAccess = createWCAccess();
        Map mergeInfo = null;
        if (limitPath != null) {
            limitPath = new File(SVNPathUtil.validateFilePath(limitPath.getAbsolutePath())).getAbsoluteFile();
        }
        try {
            while (true) {
                if (inherit == SVNMergeInfoInheritance.NEAREST_ANCESTOR) {
                    wcMergeInfo = null;
                    inherit = SVNMergeInfoInheritance.INHERITED;
                } else {
                    wcMergeInfo = SVNPropertiesManager.parseMergeInfo(path, entry, base);
                }
                if (SVNWCManager.isEntrySwitched(path, entry)) {
                    break;
                }
                path = new File(SVNPathUtil.validateFilePath(path.getAbsolutePath())).getAbsoluteFile();
                if (wcMergeInfo == null && inherit != SVNMergeInfoInheritance.EXPLICIT && path.getParentFile() != null) {
                    if (limitPath != null && limitPath.equals(path)) {
                        break;
                    }
                    walkPath = SVNPathUtil.append(path.getName(), walkPath);
                    path = path.getParentFile();
                    try {
                        wcAccess.open(path, false, 0);
                    } catch (SVNException svne) {
                        if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.WC_NOT_DIRECTORY) {
                            mergeInfo = wcMergeInfo;
                            inherited[0] = false;
                            return mergeInfo;
                        }
                        throw svne;
                    }
                    entry = wcAccess.getEntry(path, false);
                    if (entry != null) {
                        continue;
                    }
                }
                break;
            }
        } finally {
            wcAccess.close();
        }
        inherited[0] = false;
        if (walkPath.length() == 0) {
            mergeInfo = wcMergeInfo;
        } else {
            if (wcMergeInfo != null) {
                mergeInfo = SVNMergeInfoUtil.adjustMergeInfoSourcePaths(null, walkPath, wcMergeInfo);
                inherited[0] = true;
            } else {
                mergeInfo = null;
            }
        }
        if (inherited[0]) {
            mergeInfo = SVNMergeInfoUtil.getInheritableMergeInfo(mergeInfo, null, SVNRepository.INVALID_REVISION, SVNRepository.INVALID_REVISION);
            SVNMergeInfoUtil.removeEmptyRangeLists(mergeInfo);
        }
        return mergeInfo;
    }

    protected long getPathLastChangeRevision(String relPath, long revision, SVNRepository repository) throws SVNException {
        final long[] rev = new long[1];
        rev[0] = SVNRepository.INVALID_REVISION;
        repository.log(new String[] {
            relPath
        }, 1, revision, false, true, 1, false, null, new ISVNLogEntryHandler() {

            public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
                rev[0] = logEntry.getRevision();
            }
        });
        return rev[0];
    }

    protected String getPathRelativeToRoot(File path, SVNURL url, SVNURL reposRootURL, SVNWCAccess wcAccess, SVNRepository repos) throws SVNException {
        if (path != null) {
            boolean cleanUp = false;
            try {
                if (wcAccess == null) {
                    wcAccess = createWCAccess();
                    wcAccess.probeOpen(path, false, 0);
                    cleanUp = true;
                }
                SVNEntry entry = wcAccess.getVersionedEntry(path, false);
                url = getEntryLocation(path, entry, null, SVNRevision.UNDEFINED);
                if (reposRootURL == null) {
                    reposRootURL = entry.getRepositoryRootURL();
                }
            } finally {
                if (cleanUp) {
                    wcAccess.closeAdminArea(path);
                }
            }
        }
        if (reposRootURL == null) {
            reposRootURL = repos.getRepositoryRoot(true);
        }
        String reposRootPath = reposRootURL.getPath();
        String absPath = url.getPath();
        if (!absPath.startsWith(reposRootPath)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, "URL ''{0}'' is not a child of repository root URL ''{1}''", new Object[] {
                    url, reposRootURL
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        absPath = absPath.substring(reposRootPath.length());
        if (!absPath.startsWith("/")) {
            absPath = "/" + absPath;
        }
        return absPath;
    }

    protected String getPathRelativeToSession(SVNURL url, SVNURL sessionURL, SVNRepository repos) {
        if (sessionURL == null) {
            sessionURL = repos.getLocation();
        }
        String reposPath = sessionURL.getPath();
        String absPath = url.getPath();
        if (!absPath.startsWith(reposPath + "/") && !absPath.equals(reposPath)) {
            return null;
        }
        absPath = absPath.substring(reposPath.length());
        if (absPath.startsWith("/")) {
            absPath = absPath.substring(1);
        }
        return absPath;
    }

    protected SVNRepositoryLocation[] getLocations(SVNURL url, File path, SVNRepository repository, SVNRevision revision, SVNRevision start, SVNRevision end) throws SVNException {
        if (!revision.isValid() || !start.isValid()) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION), SVNLogType.DEFAULT);
        }
        long pegRevisionNumber = -1;
        long startRevisionNumber;
        long endRevisionNumber;
        long youngestRevNumber[] = {
            SVNRepository.INVALID_REVISION
        };
        if (path != null) {
            SVNWCAccess wcAccess = SVNWCAccess.newInstance(null);
            try {
                wcAccess.openAnchor(path, false, 0);
                SVNEntry entry = wcAccess.getVersionedEntry(path, false);
                if (entry.getCopyFromURL() != null && revision == SVNRevision.WORKING) {
                    url = entry.getCopyFromSVNURL();
                    pegRevisionNumber = entry.getCopyFromRevision();
                    if (entry.getURL() == null || !entry.getURL().equals(entry.getCopyFromURL())) {
                        repository = null;
                    }
                } else if (entry.getURL() != null) {
                    url = entry.getSVNURL();
                } else {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "''{0}'' has no URL", path);
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            } finally {
                wcAccess.close();
            }
        }
        String repoPath = "";
        Map locations = null;
        SVNURL rootURL = null;
        if (repository == null) {
            repository = createRepository(url, null, null, true);
        } else {
            repoPath = SVNPathUtil.getPathAsChild(repository.getLocation().toString(), url.toString());
            if (repoPath == null) {
                repoPath = "";
            }
        }
        if (pegRevisionNumber < 0) {
            pegRevisionNumber = getRevisionNumber(revision, youngestRevNumber, repository, path);
        }
        if (revision == start && revision == SVNRevision.HEAD) {
            startRevisionNumber = pegRevisionNumber;
        } else {
            startRevisionNumber = getRevisionNumber(start, youngestRevNumber, repository, path);
        }
        if (!end.isValid()) {
            endRevisionNumber = startRevisionNumber;
        } else {
            endRevisionNumber = getRevisionNumber(end, youngestRevNumber, repository, path);
        }
        if (endRevisionNumber == pegRevisionNumber && startRevisionNumber == pegRevisionNumber) {
            SVNRepositoryLocation[] result = new SVNRepositoryLocation[2];
            result[0] = new SVNRepositoryLocation(url, startRevisionNumber);
            result[1] = new SVNRepositoryLocation(url, endRevisionNumber);
            return result;
        }
        rootURL = repository.getRepositoryRoot(true);
        long[] revisionsRange = startRevisionNumber == endRevisionNumber ? new long[] {
            startRevisionNumber
        } : new long[] {
                startRevisionNumber, endRevisionNumber
        };
        try {
            locations = repository.getLocations(repoPath, (Map) null, pegRevisionNumber, revisionsRange);
        } catch (SVNException e) {
            if (e.getErrorMessage() != null && e.getErrorMessage().getErrorCode() == SVNErrorCode.RA_NOT_IMPLEMENTED) {
                locations = getLocations10(repository, pegRevisionNumber, startRevisionNumber, endRevisionNumber);
            } else {
                throw e;
            }
        }
        SVNLocationEntry startPath = (SVNLocationEntry) locations.get(new Long(startRevisionNumber));
        SVNLocationEntry endPath = (SVNLocationEntry) locations.get(new Long(endRevisionNumber));
        if (startPath == null) {
            Object source = path != null ? (Object) path : (Object) url;
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, "Unable to find repository location for ''{0}'' in revision ''{1}''", new Object[] {
                    source, new Long(startRevisionNumber)
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (endPath == null) {
            Object source = path != null ? (Object) path : (Object) url;
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, "The location for ''{0}'' for revision {1} does not exist in the "
                    + "repository or refers to an unrelated object", new Object[] {
                    source, new Long(endRevisionNumber)
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        SVNRepositoryLocation[] result = new SVNRepositoryLocation[2];
        SVNURL startURL = SVNURL.parseURIEncoded(SVNPathUtil.append(rootURL.toString(), SVNEncodingUtil.uriEncode(startPath.getPath())));
        result[0] = new SVNRepositoryLocation(startURL, startRevisionNumber);
        if (end.isValid()) {
            SVNURL endURL = SVNURL.parseURIEncoded(SVNPathUtil.append(rootURL.toString(), SVNEncodingUtil.uriEncode(endPath.getPath())));
            result[1] = new SVNRepositoryLocation(endURL, endRevisionNumber);
        }
        return result;
    }

    private Map getLocations10(SVNRepository repos, final long pegRevision, final long startRevision, final long endRevision) throws SVNException {
        final String path = repos.getRepositoryPath("");
        final SVNNodeKind kind = repos.checkPath("", pegRevision);
        if (kind == SVNNodeKind.NONE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "path ''{0}'' doesn't exist at revision {1}", new Object[] {
                    path, new Long(pegRevision)
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        long logStart = pegRevision;
        logStart = Math.max(startRevision, logStart);
        logStart = Math.max(endRevision, logStart);
        long logEnd = pegRevision;
        logStart = Math.min(startRevision, logStart);
        logStart = Math.min(endRevision, logStart);
        LocationsLogEntryHandler handler = new LocationsLogEntryHandler(path, startRevision, endRevision, pegRevision, kind, getEventDispatcher());
        repos.log(new String[] {
            ""
        }, logStart, logEnd, true, false, handler);
        String pegPath = handler.myPegPath == null ? handler.myCurrentPath : handler.myPegPath;
        String startPath = handler.myStartPath == null ? handler.myCurrentPath : handler.myStartPath;
        String endPath = handler.myEndPath == null ? handler.myCurrentPath : handler.myEndPath;
        if (pegPath == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "path ''{0}'' in revision {1} is an unrelated object", new Object[] {
                    path, new Long(logStart)
            });
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        Map result = new SVNHashMap();
        result.put(new Long(startRevision), new SVNLocationEntry(-1, startPath));
        result.put(new Long(endRevision), new SVNLocationEntry(-1, endPath));
        return result;
    }

    private static String getPreviousLogPath(String path, SVNLogEntry logEntry, SVNNodeKind kind) throws SVNException {
        String prevPath = null;
        SVNLogEntryPath logPath = (SVNLogEntryPath) logEntry.getChangedPaths().get(path);
        if (logPath != null) {
            if (logPath.getType() != SVNLogEntryPath.TYPE_ADDED && logPath.getType() != SVNLogEntryPath.TYPE_REPLACED) {
                return logPath.getPath();
            }
            if (logPath.getCopyPath() != null) {
                return logPath.getCopyPath();
            }
            return null;
        } else if (!logEntry.getChangedPaths().isEmpty()) {
            Map sortedMap = new SVNHashMap();
            sortedMap.putAll(logEntry.getChangedPaths());
            List pathsList = new ArrayList(sortedMap.keySet());
            Collections.sort(pathsList, SVNPathUtil.PATH_COMPARATOR);
            Collections.reverse(pathsList);
            for (Iterator paths = pathsList.iterator(); paths.hasNext();) {
                String p = (String) paths.next();
                if (path.startsWith(p + "/")) {
                    SVNLogEntryPath lPath = (SVNLogEntryPath) sortedMap.get(p);
                    if (lPath.getCopyPath() != null) {
                        prevPath = SVNPathUtil.append(lPath.getCopyPath(), path.substring(p.length()));
                        break;
                    }
                }
            }
        }
        if (prevPath == null) {
            if (kind == SVNNodeKind.DIR) {
                prevPath = path;
            } else {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_UNRELATED_RESOURCES, "Missing changed-path information for ''{0}'' in revision {1}", new Object[] {
                        path, new Long(logEntry.getRevision())
                });
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        return prevPath;
    }

    protected SVNURL getURL(File path) throws SVNException {
        return deriveLocation(path, null, null, SVNRevision.UNDEFINED, null, null);
    }

    protected SVNURL deriveLocation(File path, SVNURL url, long[] pegRevisionNumber, SVNRevision pegRevision, SVNRepository repos, SVNWCAccess access) throws SVNException {
        if (path != null) {
            SVNEntry entry = null;
            if (access != null) {
                entry = access.getVersionedEntry(path, false);
            } else {
                SVNWCAccess wcAccess = createWCAccess();
                try {
                    wcAccess.probeOpen(path, false, 0);
                    entry = wcAccess.getVersionedEntry(path, false);
                } finally {
                    wcAccess.close();
                }
            }
            url = getEntryLocation(path, entry, pegRevisionNumber, pegRevision);
        }
        if (pegRevisionNumber != null && pegRevisionNumber.length > 0 && !SVNRevision.isValidRevisionNumber(pegRevisionNumber[0])) {
            boolean closeRepository = false;
            try {
                if (repos == null) {
                    repos = createRepository(url, null, null, false);
                    closeRepository = true;
                }
                pegRevisionNumber[0] = getRevisionNumber(pegRevision, null, repos, path);
            } finally {
                if (closeRepository) {
                    repos.closeSession();
                }
            }
        }
        return url;
    }

    protected SVNURL getEntryLocation(File path, SVNEntry entry, long[] revNum, SVNRevision pegRevision) throws SVNException {
        SVNURL url = null;
        if (entry.getCopyFromURL() != null && pegRevision == SVNRevision.WORKING) {
            url = entry.getCopyFromSVNURL();
            if (revNum != null && revNum.length > 0) {
                revNum[0] = entry.getCopyFromRevision();
            }
        } else if (entry.getURL() != null) {
            url = entry.getSVNURL();
            if (revNum != null && revNum.length > 0) {
                revNum[0] = entry.getRevision();
            }
        } else {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "Entry for ''{0}'' has no URL", path);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return url;
    }

    protected SVNURL ensureSessionURL(SVNRepository repository, SVNURL url) throws SVNException {
        SVNURL oldURL = repository.getLocation();
        if (url == null) {
            url = repository.getRepositoryRoot(true);
        }
        if (!url.equals(oldURL)) {
            repository.setLocation(url, false);
            return oldURL;
        }
        return null;
    }

    protected int getLevelsToLockFromDepth(SVNDepth depth) {
        return depth == SVNDepth.EMPTY || depth == SVNDepth.FILES ? 0 : (depth == SVNDepth.IMMEDIATES ? 1 : SVNWCAccess.INFINITE_DEPTH);
    }

    protected void setCommitItemAccess(SVNCommitItem item, SVNWCAccess access) {
        item.setWCAccess(access);
    }

    protected void setCommitItemProperty(SVNCommitItem item, String name, SVNPropertyValue value) {
        item.setProperty(name, value);
    }

    protected void setCommitItemFlags(SVNCommitItem item, boolean contentModified, boolean propertiesModified) {
        item.setContentsModified(contentModified);
        item.setPropertiesModified(propertiesModified);
    }

    private static final class LocationsLogEntryHandler implements ISVNLogEntryHandler {

        private String myCurrentPath = null;
        private String myStartPath = null;
        private String myEndPath = null;
        private String myPegPath = null;
        private long myStartRevision;
        private long myEndRevision;
        private long myPegRevision;
        private SVNNodeKind myKind;
        private ISVNEventHandler myEventHandler;

        private LocationsLogEntryHandler(String path, long startRevision, long endRevision, long pegRevision, SVNNodeKind kind, ISVNEventHandler eventHandler) {
            myCurrentPath = path;
            myStartRevision = startRevision;
            myEndRevision = endRevision;
            myPegRevision = pegRevision;
            myEventHandler = eventHandler;
            myKind = kind;
        }

        public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
            if (myEventHandler != null) {
                myEventHandler.checkCancelled();
            }
            if (logEntry.getChangedPaths() == null) {
                return;
            }
            if (myCurrentPath == null) {
                return;
            }
            if (myStartPath == null && logEntry.getRevision() <= myStartRevision) {
                myStartPath = myCurrentPath;
            }
            if (myEndPath == null && logEntry.getRevision() <= myEndRevision) {
                myEndPath = myCurrentPath;
            }
            if (myPegPath == null && logEntry.getRevision() <= myPegRevision) {
                myPegPath = myCurrentPath;
            }
            myCurrentPath = getPreviousLogPath(myCurrentPath, logEntry, myKind);
        }
    }

    protected static class RepositoryReference {

        public RepositoryReference(String url, long rev) {
            URL = url;
            Revision = rev;
        }

        public String URL;
        public long Revision;
    }

    protected static class SVNRepositoryLocation {

        private SVNURL myURL;
        private long myRevision;

        public SVNRepositoryLocation(SVNURL url, long rev) {
            myURL = url;
            myRevision = rev;
        }

        public long getRevisionNumber() {
            return myRevision;
        }

        public SVNURL getURL() {
            return myURL;
        }
    }
}
