/*
 * ====================================================================
 * 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.wc.admin;

import java.io.File;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

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.SVNNodeKind;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNHashSet;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.internal.wc.ISVNFileFetcher;
import org.tmatesoft.svn.core.internal.wc.ISVNUpdateEditor;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNMergeCallback;
import org.tmatesoft.svn.core.internal.wc.SVNMergeCallback15;
import org.tmatesoft.svn.core.internal.wc.SVNMergeDriver;
import org.tmatesoft.svn.core.internal.wc.SVNObjectsPool;
import org.tmatesoft.svn.core.internal.wc.SVNUpdateEditor;
import org.tmatesoft.svn.core.internal.wc.SVNUpdateEditor15;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.SVNDiffOptions;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNTreeConflictDescription;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNWCAccess implements ISVNEventHandler {

    public static final int INFINITE_DEPTH = -1;

    private ISVNEventHandler myEventHandler;
    private ISVNOptions myOptions;
    private Map myAdminAreas;
    private Map myCleanupHandlers;
    private SVNObjectsPool myObjectsPool;
    
    private File myAnchor;

    public static SVNWCAccess newInstance(ISVNEventHandler eventHandler) {
        return new SVNWCAccess(eventHandler);
    }
    
    private static boolean isObjectsPoolEnabled() {
        return Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty("svnkit.entry.pool", "true"));
    }

    private SVNWCAccess(ISVNEventHandler handler) {
        myEventHandler = handler;
        if (isObjectsPoolEnabled()) {
            myObjectsPool = new SVNObjectsPool();
        }
    }

    public void setEventHandler(ISVNEventHandler handler) {
        myEventHandler = handler;
    }

    public ISVNEventHandler getEventHandler() {
        return myEventHandler;
    }

    public void checkCancelled() throws SVNCancelException {
        if (myEventHandler != null) {
            myEventHandler.checkCancelled();
        }
    }

    public void handleEvent(SVNEvent event) throws SVNException {
        handleEvent(event, ISVNEventHandler.UNKNOWN);
    }

    public void registerCleanupHandler(SVNAdminArea area, ISVNCleanupHandler handler) {
        if (area == null || handler == null) {
            return;
        }
        if (myCleanupHandlers == null) {
            myCleanupHandlers = new SVNHashMap();
        }
        myCleanupHandlers.put(area, handler);
    }

    public void handleEvent(SVNEvent event, double progress) throws SVNException {
        if (myEventHandler != null) {
            try {
                myEventHandler.handleEvent(event, progress);
            } catch (SVNException e) {
                throw e;
            } catch (Throwable th) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "Error while dispatching event: {0}", th.getMessage());
                SVNErrorManager.error(err, th, SVNLogType.WC);
            }
        }
    }

    public void setOptions(ISVNOptions options) {
        myOptions = options;
    }

    public ISVNOptions getOptions() {
        if (myOptions == null) {
            myOptions = new DefaultSVNOptions();
        }
        return myOptions;
    }

    public void setAnchor(File anchor) {
        myAnchor = anchor;
    }

    public File getAnchor() {
        return myAnchor;
    }

    public SVNAdminAreaInfo openAnchor(File path, boolean writeLock, int depth) throws SVNException {
        File parent = path.getParentFile();
        if (parent == null || "..".equals(path.getName())) {
            SVNAdminArea anchor = open(path, writeLock, depth);
            return new SVNAdminAreaInfo(this, anchor, anchor, "");
        }

        String name = path.getName();
        SVNAdminArea parentArea = null;
        SVNAdminArea targetArea = null;
        SVNException parentError = null;

        try {
            parentArea = open(parent, writeLock, false, 0);
        } catch (SVNException svne) {
            if (writeLock && svne.getErrorMessage().getErrorCode() == SVNErrorCode.WC_LOCKED) {
                try {
                    parentArea = open(parent, false, false, 0);
                } catch (SVNException svne2) {
                    throw svne;
                }
                parentError = svne;
            } else if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_DIRECTORY) {
                throw svne;
            }
        }

        try {
            targetArea = open(path, writeLock, false, depth);
        } catch (SVNException svne) {
            if (parentArea == null || svne.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_DIRECTORY) {
                try {
                    close();
                } catch (SVNException svne2) {
                    //
                }
                throw svne;
            }
        }

        if (parentArea != null && targetArea != null) {
            SVNEntry parentEntry = null;
            SVNEntry targetEntry = null;
            SVNEntry targetInParent = null;
            try {
                targetInParent = parentArea.getEntry(name, false);
                targetEntry = targetArea.getEntry(targetArea.getThisDirName(), false);
                parentEntry = parentArea.getEntry(parentArea.getThisDirName(), false);
            } catch (SVNException svne) {
                try {
                    close();
                } catch (SVNException svne2) {
                    //
                }
                throw svne;
            }

            SVNURL parentURL = parentEntry != null ? parentEntry.getSVNURL() : null;
            SVNURL targetURL = targetEntry != null ? targetEntry.getSVNURL() : null;
            String encodedName = SVNEncodingUtil.uriEncode(name);
            if (targetInParent == null || (parentURL != null && targetURL != null &&
                    (!parentURL.equals(targetURL.removePathTail()) || !encodedName.equals(SVNPathUtil.tail(targetURL.getURIEncodedPath()))))) {
                if (myAdminAreas != null) {
                    myAdminAreas.remove(parent);
                }
                try {
                    doClose(parentArea, false);
                } catch (SVNException svne) {
                    try {
                        close();
                    } catch (SVNException svne2) {
                        //
                    }
                    throw svne;
                }
                parentArea = null;
            }
        }

        if (parentArea != null) {
            if (parentError != null && targetArea != null) {
                if (parentError.getErrorMessage().getErrorCode() == SVNErrorCode.WC_LOCKED) {
                    // try to work without 'anchor'
                    try {
                        doClose(parentArea, false);
                    } catch (SVNException svne) {
                        try {
                            close();
                        } catch (SVNException svne2) {
                            //
                        }
                        throw svne;
                    }
                    parentArea = null;
                } else {
                    try {
                        close();
                    } catch (SVNException svne) {
                        //
                    }
                    throw parentError;
                }
            }
        }

        if (targetArea == null) {
            SVNEntry targetEntry = null;
            try {
                targetEntry = parentArea.getEntry(name, false);
            } catch (SVNException svne) {
                try {
                    close();
                } catch (SVNException svne2) {
                    //
                }
                throw svne;
            }
            if (targetEntry != null && targetEntry.isDirectory()) {
                if (myAdminAreas != null) {
                    myAdminAreas.put(path, null);
                }
            }
        }
        SVNAdminArea anchor = parentArea != null ? parentArea : targetArea;
        SVNAdminArea target = targetArea != null ? targetArea : parentArea;
        return new SVNAdminAreaInfo(this, anchor, target, parentArea == null ? "" : name);
    }

    public SVNAdminArea open(File path, boolean writeLock, int depth) throws SVNException {
        return open(path, writeLock, false, depth);
    }

    public SVNAdminArea open(File path, boolean writeLock, boolean stealLock, int depth) throws SVNException {
        return open(path, writeLock, stealLock, true, depth, Level.FINE);
    }

    public SVNAdminArea open(File path, boolean writeLock, boolean stealLock, boolean upgradeFormat, int depth, Level logLevel) throws SVNException {
        Map tmp = new SVNHashMap();
        SVNAdminArea area;
        try {
            area = doOpen(path, writeLock, stealLock, upgradeFormat, depth, tmp, logLevel);
        } finally {
            for(Iterator paths = tmp.keySet().iterator(); paths.hasNext();) {
                Object childPath = paths.next();
                SVNAdminArea childArea = (SVNAdminArea) tmp.get(childPath);
                myAdminAreas.put(childPath, childArea);
            }
        }
        return area;
    }

	public SVNAdminArea probeOpen(File path, boolean writeLock, int depth) throws SVNException {
		return probeOpen(path, writeLock, depth, Level.FINE);
	}

    public SVNAdminArea probeOpen(File path, boolean writeLock, int depth, Level logLevel) throws SVNException {
        File dir = probe(path, logLevel);
        if (dir == null) {
            // we tried to open root which is not wc.
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY, "''{0}'' is not a working copy", path);
            SVNErrorManager.error(err, logLevel, SVNLogType.WC);
        }
        if (!path.equals(dir)) {
            depth = 0;
        }
        SVNAdminArea adminArea = null;
        try {
            adminArea = open(dir, writeLock, false, true, depth, logLevel);
        } catch (SVNException svne) {
            SVNFileType childKind = SVNFileType.getType(path);
            SVNErrorCode errCode = svne.getErrorMessage().getErrorCode();
            if (!path.equals(dir) && (childKind == SVNFileType.DIRECTORY || childKind == SVNFileType.NONE) && errCode == SVNErrorCode.WC_NOT_DIRECTORY) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY, "''{0}'' is not a working copy", path);
                SVNErrorManager.error(err, logLevel, SVNLogType.WC);
            } else {
                throw svne;
            }
        }
        return adminArea;
    }

    public SVNAdminArea probeTry(File path, boolean writeLock, int depth) throws SVNException {
        SVNAdminArea adminArea = null;
        try {
            adminArea = probeRetrieve(path);
        } catch (SVNException svne) {
            if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.WC_NOT_LOCKED) {
                try {
                    adminArea = probeOpen(path, writeLock, depth);
                } catch (SVNException svne2) {
                    if (svne2.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_DIRECTORY) {
                        throw svne2;
                    }
                }
            } else {
                throw svne;
            }
        }
        return adminArea;
    }

    public void close() throws SVNException {
        if (myAdminAreas != null) {
            doClose(myAdminAreas, false);
            myAdminAreas.clear();
        }
        myCleanupHandlers = null;
        
        if (getObjectsPool() != null) {
            getObjectsPool().clear();
        }
    }

    public void closeAdminArea(File path) throws SVNException {
        if (myAdminAreas != null) {
            SVNAdminArea area = (SVNAdminArea) myAdminAreas.get(path);
            if (area != null) {
                doClose(area, false);
                myAdminAreas.remove(path);
            }
        }
    }

    private SVNAdminArea doOpen(File path, boolean writeLock, boolean stealLock, boolean upgradeFormat, int depth, Map tmp, Level logLevel) throws SVNException {
        // no support for 'under consturction here' - it will go to adminAreaFactory.
        tmp = tmp == null ? new SVNHashMap() : tmp;
        if (myAdminAreas != null) {
            SVNAdminArea existing = (SVNAdminArea) myAdminAreas.get(path);
            if (myAdminAreas.containsKey(path) && existing != null) {
                SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.WC_LOCKED, "Working copy ''{0}'' locked", path);
                SVNErrorManager.error(error, SVNLogType.WC);
            }
        } else {
            myAdminAreas = new SVNHashMap();
        }

        SVNAdminArea area = SVNAdminAreaFactory.open(path, logLevel);
        area.setWCAccess(this);

        if (writeLock) {
            area.lock(stealLock);
            if (upgradeFormat) {
                area = SVNAdminAreaFactory.upgrade(area);
            }
        }
        tmp.put(path, area);

        if (depth != 0) {
            if (depth > 0) {
                depth--;
            }
            for(Iterator entries = area.entries(false); entries.hasNext();) {
                try {
                    checkCancelled();
                } catch (SVNCancelException e) {
                    doClose(tmp, false);
                    throw e;
                }

                SVNEntry entry = (SVNEntry) entries.next();
                if (entry.getKind() != SVNNodeKind.DIR  || area.getThisDirName().equals(entry.getName())) {
                    continue;
                }
                if (entry.getDepth() == SVNDepth.EXCLUDE) {
                    continue;
                }
                File childPath = new File(path, entry.getName());
                try {
                    // this method will put created area into our map.
                    doOpen(childPath, writeLock, stealLock, upgradeFormat, depth, tmp, logLevel);
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_NOT_DIRECTORY) {
                        doClose(tmp, false);
                        throw e;
                    }
                    // only for missing!
                    tmp.put(childPath, null);
                }

                SVNAdminArea childArea = (SVNAdminArea) tmp.get(childPath);
                if (childArea != null) {
                    SVNEntry childRootEntry = childArea.getEntry(childArea.getThisDirName(), false);
                    SVNEntry thisRootEntry = area.getEntry(childArea.getThisDirName(), false);

                    String childRoot = childRootEntry.getRepositoryRoot();
                    String expectedRoot = thisRootEntry.getRepositoryRoot();

                    if (childRoot != null && !childRoot.equals(expectedRoot)) {
                        Map toClose = new SVNHashMap();
                        toClose.put(childPath, childArea);
                        String childPathAbs = childPath.getAbsolutePath().replace(File.separatorChar, '/');
                        for (Iterator paths = tmp.keySet().iterator(); paths.hasNext();) {
                            File p = (File) paths.next();
                            String pAbs = p.getAbsolutePath().replace(File.separatorChar, '/');
                            if (SVNPathUtil.isAncestor(childPathAbs, pAbs)) {
                                toClose.put(p, tmp.get(p));
                                paths.remove();
                            }
                        }
                        tmp.put(childPath, null);
                        doClose(toClose, false);
                    }
                }
            }
        }
        return area;
    }

    private void doClose(Map adminAreas, boolean preserveLocks) throws SVNException {
        Set closedAreas = new SVNHashSet();
        while(!adminAreas.isEmpty()) {
            Map copy = new SVNHashMap(adminAreas);
            try {
                for (Iterator paths = copy.keySet().iterator(); paths.hasNext();) {
                    File path = (File) paths.next();
                    SVNAdminArea adminArea = (SVNAdminArea) copy.get(path);
                    if (adminArea == null) {
                        closedAreas.add(path);
                        continue;
                    }
                    doClose(adminArea, preserveLocks);
                    closedAreas.add(path);
                }
            } finally {
                for (Iterator paths = closedAreas.iterator(); paths.hasNext();) {
                    File path = (File) paths.next();
                    adminAreas.remove(path);
                }
            }
        }
    }

    private void doClose(SVNAdminArea adminArea, boolean preserveLocks) throws SVNException {
        if (adminArea == null) {
            return;
        }
        if (myCleanupHandlers != null) {
            ISVNCleanupHandler handler = (ISVNCleanupHandler) myCleanupHandlers.remove(adminArea);
            if (handler != null) {
                handler.cleanup(adminArea);
            }
        }
        if (!preserveLocks && adminArea.isLocked()) {
            adminArea.unlock();
        }

        adminArea.close();
    }

    public SVNAdminArea probeRetrieve(File path) throws SVNException {
        File dir = probe(path, Level.FINE);
        if (dir == null) {
            // we tried to open root which is not wc.
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_DIRECTORY, "''{0}'' is not a working copy", path);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return retrieve(dir);
    }

    public boolean isMissing(File path) {
        if (myAdminAreas != null) {
            return myAdminAreas.containsKey(path) && myAdminAreas.get(path) == null;
        }
        return false;
    }

    public boolean isLocked(File path) throws SVNException {
        File lockFile = new File(path, SVNFileUtil.getAdminDirectoryName());
        lockFile = new File(lockFile, "lock");
        if (SVNFileType.getType(lockFile) == SVNFileType.FILE) {
            return true;
        } else if (SVNFileType.getType(lockFile) == SVNFileType.NONE) {
            return false;
        }
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_LOCKED,
                "Lock file ''{0}'' is not a regular file", lockFile);
        SVNErrorManager.error(err, SVNLogType.WC);
        return false;
    }

    public boolean isWCRoot(File path) throws SVNException {
        SVNEntry entry = getEntry(path, false);
        File parent = path.getParentFile();
        if (parent == null && entry != null) {
            return true;
        }
        SVNAdminArea parentArea = getAdminArea(parent);
        SVNWCAccess tmpAccess = null;
        SVNWCAccess access = this;
        try {
            if (parentArea == null) {
                tmpAccess = new SVNWCAccess(null);
                try {
                    parentArea = tmpAccess.probeOpen(parent, false, 0, Level.FINEST);
                } catch (SVNException svne) {
                    return true;
                }
                access = tmpAccess;
            }

            SVNEntry parentEntry = access.getEntry(parent, false);
            if (parentEntry == null || !parentEntry.isThisDir()) {
                return true;
            }

            if (parentEntry.getURL() == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL,
                        "''{0}'' has no ancestry information", parent);
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            // what about switched paths?
            /*
            if (entry != null && entry.getURL() != null) {
                if (!entry.getURL().equals(SVNPathUtil.append(parentEntry.getURL(), SVNEncodingUtil.uriEncode(path.getName())))) {
                    return true;
                }
            }*/
            entry = parentArea.getEntry(path.getName(), false);
            if (entry == null) {
                return true;
            }
        } finally {
            if (tmpAccess != null) {
                try {
                    tmpAccess.close();
                } catch (SVNException svne) {
                    //
                }
            }
        }
        return false;
    }

    public SVNTreeConflictDescription getTreeConflict(File path) throws SVNException {
        File parent = path.getParentFile();
        if (parent == null) {
            return null;
        }
        boolean closeParentArea = false;
        SVNAdminArea parentArea = null;
        try {
            parentArea = retrieve(parent);
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_NOT_LOCKED) {
                e = null;
                try {
                    parentArea = open(parent, false, 0);
                    closeParentArea = true;
                } catch (SVNException internal) {
                    if (internal.getErrorMessage().getErrorCode() == SVNErrorCode.WC_NOT_DIRECTORY
                            || internal.getErrorMessage().getErrorCode() == SVNErrorCode.WC_UNSUPPORTED_FORMAT) {
                        return null;
                    }
                    e = internal;
                }
            }
            if (e != null) {
                throw e;
            }
        }
        SVNTreeConflictDescription treeConflict = parentArea.getTreeConflict(path.getName());
        if (closeParentArea) {
            closeAdminArea(parent);
        }
        return treeConflict;
    }

    public boolean hasTreeConflict(File path) throws SVNException {
        SVNTreeConflictDescription treeConflict = getTreeConflict(path);
        return treeConflict != null;
    }

    public SVNEntry getEntry(File path, boolean showHidden) throws SVNException {
        SVNAdminArea adminArea = getAdminArea(path);
        String entryName = null;
        if (adminArea == null) {
            adminArea = getAdminArea(path.getParentFile());
            entryName = path.getName();
        } else {
            entryName = adminArea.getThisDirName();
        }

        if (adminArea != null) {
            return adminArea.getEntry(entryName, showHidden);
        }
        return null;
    }

    public SVNEntry getVersionedEntry(File path, boolean showHidden) throws SVNException {
        SVNEntry entry = getEntry(path, showHidden);
        if (entry == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND,
                    "''{0}'' is not under version control", path);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return entry;

    }

    public void setRepositoryRoot(File path, SVNURL reposRoot) throws SVNException {
        SVNEntry entry = getEntry(path, false);
        if (entry == null) {
            return;
        }
        SVNAdminArea adminArea = null;
        String name = null;
        if (entry.isFile()) {
            adminArea = getAdminArea(path.getParentFile());
            name = path.getName();
        } else {
            adminArea = getAdminArea(path);
            name = adminArea != null ? adminArea.getThisDirName() : null;
        }

        if (adminArea == null) {
            return;
        }
        if (adminArea.tweakEntry(name, null, reposRoot.toString(), -1, false)) {
            adminArea.saveEntries(false);
        }
    }

    public SVNAdminArea[] getAdminAreas() {
        if (myAdminAreas != null) {
            return (SVNAdminArea[]) myAdminAreas.values().toArray(new SVNAdminArea[myAdminAreas.size()]);
        }
        return new SVNAdminArea[0];
    }

    /**
     * Ugrades SVNAdminArea associated with the path and cached in this SVNWCAccess instance.
     * Updates caches if upgrade was done.
     *
     * @param  path                           path associated with already retrieved and locked SVNAdminArea
     * @return                                newly created SVNAdminArea object if upgrade was done or already cached SVNAdminArea instance otherwise.
     * @throws SVNException
     */
    public SVNAdminArea upgrade(File path) throws SVNException {
        SVNAdminArea upgradedArea = null;
        if (myAdminAreas != null) {
            SVNAdminArea area = (SVNAdminArea) myAdminAreas.get(path);
            if (area != null) {
                ISVNCleanupHandler cleanupHandler = null;
                if (myCleanupHandlers != null) {
                    cleanupHandler = (ISVNCleanupHandler) myCleanupHandlers.get(area);
                }
                upgradedArea = SVNAdminAreaFactory.upgrade(area);
                if (upgradedArea != area) {
                    myAdminAreas.put(path, upgradedArea);
                    if (cleanupHandler != null) {
                        myCleanupHandlers.remove(area);
                        myCleanupHandlers.put(upgradedArea, cleanupHandler);
                    }
                }
            }
        }
        return upgradedArea;
    }

    public SVNAdminArea retrieve(File path) throws SVNException {
        SVNAdminArea adminArea = getAdminArea(path);
        if (adminArea == null) {
            SVNEntry subEntry = null;
            try {
                SVNAdminArea dirAdminArea = getAdminArea(path.getParentFile());
                if (dirAdminArea != null) {
                    subEntry = dirAdminArea.getEntry(path.getName(), true);
                }
            } catch (SVNException svne) {
                subEntry = null;
            }
            SVNFileType type = SVNFileType.getType(path);
            if (subEntry != null) {
                if (subEntry.getKind() == SVNNodeKind.DIR && type == SVNFileType.FILE) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "Expected ''{0}'' to be a directory but found a file", path);
                    SVNErrorManager.error(err, SVNLogType.WC);
                } else if (subEntry.getKind() == SVNNodeKind.FILE && type == SVNFileType.DIRECTORY) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "Expected ''{0}'' to be a file but found a directory", path);
                    SVNErrorManager.error(err, SVNLogType.WC);
                }
            }
            File adminDir = new File(path, SVNFileUtil.getAdminDirectoryName());
            SVNFileType wcType = SVNFileType.getType(adminDir);

            if (type == SVNFileType.NONE) {
                SVNErrorMessage childErr = SVNErrorMessage.create(SVNErrorCode.WC_PATH_NOT_FOUND, "Directory ''{0}'' is missing", path);
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "Directory ''{0}'' is missing", path);
                err.setChildErrorMessage(childErr);
                SVNErrorManager.error(err, SVNLogType.WC);
            } else if (type == SVNFileType.DIRECTORY && wcType == SVNFileType.NONE) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "Directory ''{0}'' containing working copy admin area is missing", adminDir);
                SVNErrorManager.error(err, SVNLogType.WC);
            } else if (type == SVNFileType.DIRECTORY && wcType == SVNFileType.DIRECTORY) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "Unable to lock ''{0}''", path);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "Working copy ''{0}'' is not locked", path);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return adminArea;
    }

    //analogous to retrieve_internal
    public SVNAdminArea getAdminArea(File path) {
        //internal retrieve
        SVNAdminArea adminArea = null;
        if (myAdminAreas != null) {
            adminArea = (SVNAdminArea) myAdminAreas.get(path);
        }
        return adminArea;
    }

    public void walkEntries(File path, ISVNEntryHandler handler, boolean showHidden, SVNDepth depth) throws SVNException {
        walkEntries(path, handler, showHidden, false, depth);
    }

    public void walkEntries(File path, ISVNEntryHandler handler, boolean showHidden, boolean includeTC, SVNDepth depth) throws SVNException {
        // wrap handler into tc handler
        if (includeTC) {
            handler = new TCEntryHandler(path, this, handler, depth);
        }
        SVNEntry entry = getEntry(path, showHidden);
        if (entry == null) {
            if (includeTC) {
                SVNTreeConflictDescription tc = getTreeConflict(path);
                if (tc != null) {
                    handler.handleEntry(path, null);
                    return;
                }
            }
            handler.handleError(path, SVNErrorMessage.create(SVNErrorCode.UNVERSIONED_RESOURCE,
                    "''{0}'' is not under version control", path));
            return;
        }

        if (entry.isFile()) {
            try {
                handler.handleEntry(path, entry);
            } catch (SVNException svne) {
                handler.handleError(path, svne.getErrorMessage());
            }
        } else if (entry.isDirectory()) {
            SVNAdminArea adminArea = entry.getAdminArea();
            try {
                adminArea.walkThisDirectory(handler, includeTC ? true : showHidden, depth);
            } catch (SVNException svne) {
                handler.handleError(path, svne.getErrorMessage());
            }
        } else {
           handler.handleError(path, SVNErrorMessage.create(SVNErrorCode.NODE_UNKNOWN_KIND,
                   "''{0}'' has an unrecognized node kind", path));
        }
    }

    private static boolean ourNeverDescendIntoSymlinks = Boolean.getBoolean("svnkit.symlinks.neverDescend");

    private File probe(File path, Level logLevel) throws SVNException {
        int wcFormat = -1;
        SVNFileType type = SVNFileType.getType(path);
        boolean eligible = type == SVNFileType.DIRECTORY;
        // only treat as directories those, that are not versioned in parent wc.
        if (eligible) {
            wcFormat = SVNAdminAreaFactory.checkWC(path, true, logLevel);
        } else if (type == SVNFileType.SYMLINK && path.isDirectory()) {
            // either wc root which is a link or link within wc.
            // check for being root.
            eligible = !ourNeverDescendIntoSymlinks && isWCRoot(path);
            if (eligible) {
                wcFormat = SVNAdminAreaFactory.checkWC(path, true, logLevel);
            }
        } else {
            wcFormat = 0;
        }

        //non wc
        if (!eligible || wcFormat == 0) {
            if ("..".equals(path.getName()) || ".".equals(path.getName())) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_BAD_PATH, "Path ''{0}'' ends in ''{1}'', which is unsupported for this operation", new Object[]{path, path.getName()});
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            path = path.getParentFile();
        }
        return path;
    }

    public static boolean matchesChangeList(Collection changeLists, SVNEntry entry) {
        return changeLists == null || changeLists.isEmpty() || (entry != null && entry.getChangelistName() != null && changeLists.contains(entry.getChangelistName()));
    }

    int getMaxFormatVersion() {
        int maxVersion = -1;
        for (Iterator iterator = myAdminAreas.values().iterator(); iterator.hasNext();) {
            SVNAdminArea adminArea = (SVNAdminArea) iterator.next();
            if (adminArea != null && adminArea.getFormatVersion() > maxVersion) {
                maxVersion = adminArea.getFormatVersion();
            }
        }
        return maxVersion;
    }

    public ISVNUpdateEditor createUpdateEditor(SVNAdminAreaInfo info, String switchURL,
            boolean allowUnversionedObstructions, boolean depthIsSticky, SVNDepth depth,
            String[] preservedExtensions, ISVNFileFetcher fileFetcher, boolean lockOnDemand) throws SVNException {
        int maxVersion = getMaxFormatVersion();
        if (0 < maxVersion && maxVersion < SVNAdminAreaFactory.WC_FORMAT_16) {
            return SVNUpdateEditor15.createUpdateEditor(info, switchURL, allowUnversionedObstructions, depthIsSticky, depth, preservedExtensions, fileFetcher, lockOnDemand);
        }
        return SVNUpdateEditor.createUpdateEditor(info, switchURL, allowUnversionedObstructions, depthIsSticky, depth, preservedExtensions, fileFetcher, lockOnDemand);
    }

    public SVNMergeCallback createMergeCallback(SVNMergeDriver mergeDriver, SVNAdminArea adminArea, SVNURL url,
            SVNDiffOptions mergeOptions, Map conflictedPaths, boolean force, boolean dryRun) {
        int maxVersion = getMaxFormatVersion();
        if (maxVersion < SVNAdminAreaFactory.WC_FORMAT_16) {
            return new SVNMergeCallback15(adminArea, url, force, dryRun,
                    mergeOptions, conflictedPaths, mergeDriver);
        }
        return new SVNMergeCallback(adminArea, url, force, dryRun, mergeOptions, conflictedPaths, mergeDriver);
    }

    private static class TCEntryHandler implements ISVNEntryHandler {

        private ISVNEntryHandler myDelegate;
        private SVNDepth myDepth;
        private File myTargetPath;
        private SVNWCAccess myWCAccess;

        public TCEntryHandler(File target, SVNWCAccess wcAccess, ISVNEntryHandler delegate, SVNDepth depth) {
            myDelegate = delegate;
            myDepth = depth;
            myTargetPath = target;
            myWCAccess = wcAccess;
        }

        public void handleEntry(File path, SVNEntry entry) throws SVNException {
            myDelegate.handleEntry(path, entry);
            if (entry == null || !entry.isDirectory() || entry.isHidden()) {
                return;
            }
            boolean checkChildren = false;
            if (myDepth == SVNDepth.IMMEDIATES || myDepth == SVNDepth.FILES) {
                checkChildren = path.equals(myTargetPath);
            } else if (myDepth == SVNDepth.INFINITY || myDepth == SVNDepth.EXCLUDE || myDepth == SVNDepth.UNKNOWN) {
                checkChildren = true;
            } else {
                return;
            }
            if (!checkChildren) {
                return;
            }
            Map tcs = entry.getTreeConflicts();
            for(Iterator paths = tcs.keySet().iterator(); paths.hasNext();) {
                File p = (File) paths.next();
                SVNTreeConflictDescription tc = (SVNTreeConflictDescription) tcs.get(p);
                if (tc.getNodeKind() == SVNNodeKind.DIR && myDepth == SVNDepth.FILES) {
                    continue;
                }
                SVNEntry conflictEntry = myWCAccess.getEntry(p, true);
                if (conflictEntry == null || conflictEntry.isDeleted()) {
                    myDelegate.handleEntry(p, null);
                }
            }
        }

        public void handleError(File path, SVNErrorMessage error) throws SVNException {
            if (error != null && error.getErrorCode() == SVNErrorCode.UNVERSIONED_RESOURCE) {
                SVNTreeConflictDescription tc = myWCAccess.getTreeConflict(path);
                if (tc != null) {
                    myDelegate.handleEntry(path, null);
                    return;
                }
            }
            myDelegate.handleError(path, error);
        }
    }

    public SVNObjectsPool getObjectsPool() {
        return myObjectsPool;
    }
}
