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

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;

import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLock;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNDate;
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.admin.SVNAdminArea;
import org.tmatesoft.svn.core.internal.wc.admin.SVNAdminAreaInfo;
import org.tmatesoft.svn.core.internal.wc.admin.SVNEntry;
import org.tmatesoft.svn.core.internal.wc.admin.SVNWCAccess;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNStatusFileProvider;
import org.tmatesoft.svn.core.wc.ISVNStatusHandler;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatus;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNTreeConflictDescription;
import org.tmatesoft.svn.core.wc.SVNWCUtil;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNStatusEditor {
    
    private SVNWCAccess myWCAccess;
    private SVNAdminAreaInfo myAdminInfo;

    private boolean myIsReportAll;
    private boolean myIsNoIgnore;
    private SVNDepth myDepth;
    
    private ISVNStatusHandler myStatusHandler;

    private Map myExternalsMap;
    private Collection myGlobalIgnores;
    
    protected SVNURL myRepositoryRoot;
    private Map myRepositoryLocks;
    private long myTargetRevision;
    private String myWCRootPath;
    private ISVNStatusFileProvider myFileProvider;
    private ISVNStatusFileProvider myDefaultFileProvider;

    public SVNStatusEditor(ISVNOptions options, SVNWCAccess wcAccess, SVNAdminAreaInfo info, boolean noIgnore, boolean reportAll, SVNDepth depth,
            ISVNStatusHandler handler) {
        myWCAccess = wcAccess;
        myAdminInfo = info;
        myIsNoIgnore = noIgnore;
        myIsReportAll = reportAll;
        myDepth = depth;
        myStatusHandler = handler;
        myExternalsMap = new SVNHashMap();
        myGlobalIgnores = getGlobalIgnores(options);
        myTargetRevision = -1;
        myDefaultFileProvider = new DefaultSVNStatusFileProvider();
        myFileProvider = myDefaultFileProvider;
    }
    
    public long getTargetRevision() {
        return myTargetRevision;
    }

    public void targetRevision(long revision) {
        myTargetRevision = revision;
    }

    public SVNCommitInfo closeEdit() throws SVNException {
        try {
            if (hasTarget()) {
                File path = myAdminInfo.getAnchor().getFile(myAdminInfo.getTargetName());
                SVNFileType type = SVNFileType.getType(path);
                if (type == SVNFileType.DIRECTORY) {
                    SVNEntry entry = myWCAccess.getEntry(path, false);
                    if (entry == null) {
                        getDirStatus(null, myAdminInfo.getAnchor(), myAdminInfo.getTargetName(), 
                                SVNDepth.EMPTY, myIsReportAll, true, null, true, myStatusHandler);
                    } else {
                        SVNAdminArea target = myWCAccess.retrieve(path);
                        getDirStatus(null, target, null, 
                                myDepth, myIsReportAll, myIsNoIgnore, null, false, myStatusHandler);
                    }
                } else {
                    getDirStatus(null, myAdminInfo.getAnchor(), myAdminInfo.getTargetName(), SVNDepth.EMPTY, myIsReportAll, true, null, true, myStatusHandler);
                }
            } else {
                getDirStatus(null, myAdminInfo.getAnchor(), null, 
                        myDepth, myIsReportAll, myIsNoIgnore, null, false, myStatusHandler);
            }
        } finally {
            cleanup();
        }
        return null;
    }
    
    public void setRepositoryInfo(SVNURL root, Map repositoryLocks) {
        myRepositoryRoot = root;
        myRepositoryLocks = repositoryLocks;
        
    }
    
    protected void getDirStatus(SVNEntry parentEntry, SVNAdminArea dir, String entryName,
            SVNDepth depth, boolean getAll, boolean noIgnore, Collection ignorePatterns, boolean skipThisDir,
            ISVNStatusHandler handler) throws SVNException {
        myWCAccess.checkCancelled();
        depth = depth == SVNDepth.UNKNOWN ? SVNDepth.INFINITY : depth;
        Map childrenFiles = myFileProvider.getChildrenFiles(dir.getRoot());
        SVNEntry dirEntry = myWCAccess.getEntry(dir.getRoot(), false);

        String externals = dir.getProperties(dir.getThisDirName()).getStringPropertyValue(SVNProperty.EXTERNALS);
        if (externals != null) {
            String path = dir.getRelativePath(myAdminInfo.getAnchor());
            myAdminInfo.addExternal(path, externals, externals);
            myAdminInfo.addDepth(path, dirEntry.getDepth());
            
            SVNExternal[] externalsInfo = SVNExternal.parseExternals(dir.getRelativePath(myAdminInfo.getAnchor()), externals);
            for (int i = 0; i < externalsInfo.length; i++) {
                SVNExternal external = externalsInfo[i];
                myExternalsMap.put(SVNPathUtil.append(path, external.getPath()), external);
            }
        }
        
        if (entryName != null) {
            File file = (File) childrenFiles.get(entryName);
            SVNEntry entry = dir.getEntry(entryName, false);
            if (entry != null) {
                SVNFileType fileType = SVNFileType.getType(file);
                boolean special = fileType == SVNFileType.SYMLINK;
                SVNNodeKind fileKind = SVNFileType.getNodeKind(fileType);
                handleDirEntry(dir, entryName, dirEntry, entry, 
                        fileKind, special, depth, getAll, noIgnore, handler);
            } else if (file != null) {
                if (ignorePatterns == null) {
                    ignorePatterns = getIgnorePatterns(dir, myGlobalIgnores);
                }
                SVNFileType fileType = SVNFileType.getType(file);
                boolean special = fileType == SVNFileType.SYMLINK;
                SVNNodeKind fileKind = SVNFileType.getNodeKind(fileType);
                sendUnversionedStatus(file, entryName, fileKind, special, dir, ignorePatterns, noIgnore, handler);
            } else {
                SVNTreeConflictDescription treeConflict = myWCAccess.getTreeConflict(dir.getFile(entryName));
                if (treeConflict != null) {
                    if (ignorePatterns == null) {
                        ignorePatterns = getIgnorePatterns(dir, myGlobalIgnores);
                    }
                    sendUnversionedStatus(dir.getFile(entryName), entryName, SVNNodeKind.NONE, false, dir, ignorePatterns, true, handler);
                }
            }
            return;
        }

        if (!skipThisDir) {
            SVNStatus status = assembleStatus(dir.getRoot(), dir, dirEntry, parentEntry, 
                    SVNNodeKind.DIR, false, isReportAll(), false);
            if (status != null && handler != null) {
                handler.handleStatus(status);
            }
        }

        if (depth == SVNDepth.EMPTY) {
            return;
        }
        // iterate over files.
        childrenFiles = new TreeMap(childrenFiles);
        for (Iterator files = childrenFiles.keySet().iterator(); files.hasNext();) {
            String fileName = (String) files.next();
            SVNEntry entry = dir.getEntry(fileName, true);
            if (isNameConflict(entry)) {
                continue;
            }
            if ((entry != null && !entry.isHidden()) || SVNFileUtil.getAdminDirectoryName().equals(fileName)) {
                continue;
            }
            File file = (File) childrenFiles.get(fileName);
            if (depth == SVNDepth.FILES && file.isDirectory()) {
                continue;
            }
            
            if (ignorePatterns == null) {
                ignorePatterns = getIgnorePatterns(dir, myGlobalIgnores);
            }
            sendUnversionedStatus(file, fileName, SVNNodeKind.NONE, false, dir, ignorePatterns, noIgnore, 
                    handler);
        }
        
        Map treeConflicts = SVNTreeConflictUtil.readTreeConflicts(dir.getRoot(), dirEntry.getTreeConflictData());
        for (Iterator treeConflictsIter = treeConflicts.keySet().iterator(); treeConflictsIter.hasNext();) {
            File conflictPath = (File) treeConflictsIter.next();
            if (childrenFiles.containsKey(conflictPath.getName()) || dir.getEntry(conflictPath.getName(), false) != null) {
                continue;
            }
            
            if (ignorePatterns == null) {
                ignorePatterns = getIgnorePatterns(dir, myGlobalIgnores);
            }
            
            sendUnversionedStatus(conflictPath, conflictPath.getName(), SVNNodeKind.NONE, false, dir, ignorePatterns, noIgnore, 
                    handler);
        }
       
        for(Iterator entries = dir.entries(true); entries.hasNext();) {
            SVNEntry entry = (SVNEntry) entries.next();
            if (isNameConflict(entry)) {
                SVNStatus status = new SVNStatus(entry.getSVNURL(), dir.getFile(entry.getName()), entry.getKind(),
                        SVNRevision.create(entry.getRevision()), SVNRevision.create(entry.getCommittedRevision()),
                        SVNDate.parseDate(entry.getCommittedDate()), entry.getAuthor(),
                        SVNStatusType.STATUS_NAME_CONFLICT,  SVNStatusType.STATUS_NONE, SVNStatusType.STATUS_NONE, SVNStatusType.STATUS_NONE, 
                        false, entry.isCopied(), false, false, null, null, null, null, 
                        entry.getCopyFromURL(), SVNRevision.create(entry.getCopyFromRevision()),
                        null, null, entry.asMap(), entry.getChangelistName(), dir.getFormatVersion(), null);
                status.setDepth(entry.isDirectory() ? entry.getDepth() : SVNDepth.UNKNOWN);
                status.setEntry(entry);
                status.setRepositoryRootURL(myRepositoryRoot);
                handler.handleStatus(status);                
                continue;
            } else if (entry.isHidden()) {
                continue;
            }
            if (dir.getThisDirName().equals(entry.getName())) {
                continue;
            }
            if (depth == SVNDepth.FILES && entry.isDirectory()) {
                continue;
            }
            File file = (File) childrenFiles.get(entry.getName());
            SVNFileType fileType = SVNFileType.getType(file);
            boolean special = fileType == SVNFileType.SYMLINK;
            SVNNodeKind fileKind = SVNFileType.getNodeKind(fileType);
            handleDirEntry(dir, entry.getName(), dirEntry, entry, 
                    fileKind, special, depth == SVNDepth.INFINITY ? depth : SVNDepth.EMPTY, 
                            getAll, noIgnore, handler);
        }
    }

    public static boolean isNameConflict(SVNEntry entry) {        
        return entry != null && entry.isAbsent() && "nameconflict".equals(entry.getChecksum());
    }

    protected void cleanup() {
        if (hasTarget()) { 
            myAdminInfo.removeExternal("");
            myAdminInfo.removeDepth("");
        }
    }
    
    protected SVNAdminArea getAnchor() {
        return myAdminInfo.getAnchor();
    }

    protected SVNWCAccess getWCAccess() {
        return myWCAccess;
    }
    
    protected SVNDepth getDepth() {
        return myDepth;
    }
    
    protected boolean isReportAll() {
        return myIsReportAll;
    }
    
    protected boolean isNoIgnore() {
        return myIsNoIgnore;
    }
    
    protected SVNAdminAreaInfo getAdminAreaInfo() {
        return myAdminInfo;
    }
    
    protected ISVNStatusHandler getDefaultHandler() {
        return myStatusHandler;
    }
    
    protected boolean hasTarget() {
        return myAdminInfo.getTargetName() != null && !"".equals(myAdminInfo.getTargetName());
    }
    
    protected SVNLock getLock(SVNURL url) {
    	return SVNStatusUtil.getLock(myRepositoryLocks, url, myRepositoryRoot);
    }

    private void handleDirEntry(SVNAdminArea dir, String entryName, SVNEntry dirEntry, SVNEntry entry, SVNNodeKind fileKind, boolean special, 
            SVNDepth depth, boolean getAll, boolean noIgnore, ISVNStatusHandler handler) throws SVNException {
        File path = dir.getFile(entryName);
        
        if (fileKind == SVNNodeKind.DIR) {
            SVNEntry fullEntry = entry;
            if (entry.getKind() == fileKind) {
                fullEntry = myWCAccess.getVersionedEntry(path, false);
            }
            if (fullEntry != entry && (depth == SVNDepth.UNKNOWN || depth == SVNDepth.IMMEDIATES
                    || depth == SVNDepth.INFINITY)) {
                SVNAdminArea childDir = myWCAccess.retrieve(path);
                getDirStatus(dirEntry, childDir, null, depth, getAll, noIgnore, null, false, handler);
            } else if (fullEntry != entry) {
                // get correct dir.
                SVNAdminArea childDir = myWCAccess.retrieve(path);
                SVNStatus status = assembleStatus(path, childDir, fullEntry, dirEntry, fileKind, 
                		special, getAll, false);
                if (status != null && handler != null) {
                    handler.handleStatus(status);
                }
            } else {
                SVNStatus status = assembleStatus(path, dir, fullEntry, dirEntry, fileKind, 
                		special, getAll, false);
                if (status != null && handler != null) {
                    handler.handleStatus(status);
                }
            }
        } else {
            SVNStatus status = assembleStatus(path, dir, entry, dirEntry, fileKind, special, 
            		getAll, false);
            if (status != null && handler != null) {
                handler.handleStatus(status);
            }
        }
    }
    
    private void sendUnversionedStatus(File file, String name, SVNNodeKind fileType, boolean special, 
            SVNAdminArea dir, Collection ignorePatterns, boolean noIgnore, ISVNStatusHandler handler) throws SVNException {
        String path = dir.getRelativePath(myAdminInfo.getAnchor());
        path = SVNPathUtil.append(path, name);  

        boolean isIgnored = isIgnored(ignorePatterns, file, getWCRootRelativePath(ignorePatterns, file));
        
        boolean isExternal = isExternal(path);
        SVNStatus status = assembleStatus(file, dir, null, null, fileType, special, true, 
        		isIgnored);
        if (status != null) {
            if (isExternal) {
                status.setContentsStatus(SVNStatusType.STATUS_EXTERNAL);
            }
            if (handler != null && (noIgnore || !isIgnored || isExternal || status.getRemoteLock() != null)) {
                handler.handleStatus(status);
            }
        }
    }
    
    protected SVNStatus assembleStatus(File file, SVNAdminArea dir, 
            SVNEntry entry, SVNEntry parentEntry, SVNNodeKind fileKind, boolean special, 
            boolean reportAll, boolean isIgnored) throws SVNException {

    	return SVNStatusUtil.assembleStatus(file, dir, entry, parentEntry, fileKind, special, reportAll, 
    			isIgnored, myRepositoryLocks, myRepositoryRoot, myWCAccess);
    }
    
    protected String getWCRootPath() {
        if (myWCRootPath == null) {
            try {
                File root = SVNWCUtil.getWorkingCopyRoot(myAdminInfo.getAnchor().getRoot(), true);
                if (root != null) {
                    myWCRootPath = root.getAbsolutePath().replace(File.separatorChar, '/');
                }
            } catch (SVNException e) {
                // ignore.
            }
        }
        return myWCRootPath;
    }
    
    protected String getWCRootRelativePath(Collection ignorePatterns, File file) {
        boolean needToComputeWCRelativePath = false;
        for (Iterator patterns = ignorePatterns.iterator(); patterns.hasNext();) {
            String pattern = (String) patterns.next();
            if (pattern.startsWith("/")) {
                needToComputeWCRelativePath = true;
                break;
            }
        }
        if (!needToComputeWCRelativePath) {
            return null;
        }
        String rootRelativePath = null;
        if (getWCRootPath() != null) {
            rootRelativePath = file.getAbsolutePath().replace(File.separatorChar, '/');
            rootRelativePath = SVNPathUtil.getPathAsChild(getWCRootPath(), rootRelativePath);
            if (rootRelativePath != null && !rootRelativePath.startsWith("/")) {
                rootRelativePath = "/" + rootRelativePath;
            }
        }
        return rootRelativePath;
    }
    
    private boolean isExternal(String path) {
        if (!myExternalsMap.containsKey(path)) {
            // check if path is external parent.            
            for (Iterator paths = myExternalsMap.keySet().iterator(); paths.hasNext();) {
                String externalPath = (String) paths.next();
                if (externalPath.startsWith(path + "/")) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
    
    public static Collection getIgnorePatterns(SVNAdminArea dir, Collection globalIgnores) throws SVNException {
        String localIgnores = dir.getProperties("").getStringPropertyValue(SVNProperty.IGNORE);
        if (localIgnores != null) {
            Collection patterns = new SVNHashSet();
            patterns.addAll(globalIgnores);
            for(StringTokenizer tokens = new StringTokenizer(localIgnores, "\r\n"); tokens.hasMoreTokens();) {
                String token = tokens.nextToken().trim();
                if (token.length() > 0) {
                    patterns.add(token);
                }
            }
            return patterns;
        }
        return globalIgnores;
    }
    
    public static Collection getGlobalIgnores(ISVNOptions options) {
        if (options != null) {
            String[] ignores = options.getIgnorePatterns();
            if (ignores != null) {
                Collection patterns = new SVNHashSet();
                for (int i = 0; i < ignores.length; i++) {
                    patterns.add(ignores[i]);
                }
                return patterns;
            }
        }
        return Collections.EMPTY_SET;
    }
    
    public static boolean isIgnored(Collection patterns, File file) {
        return isIgnored(patterns, file, null);
    }
    
    public static boolean isIgnored(Collection patterns, File file, String relativePath) {
        String name = file.getName();
        String dirName = null;
        boolean isDirectory = SVNFileType.getType(file) == SVNFileType.DIRECTORY;
        if (isDirectory) {
            dirName = name + "/";
        }
        
        for (Iterator ps = patterns.iterator(); ps.hasNext();) {
            String pattern = (String) ps.next();
            if (pattern.startsWith("/") && relativePath != null) {
                if (DefaultSVNOptions.matches(pattern, relativePath) || 
                   (isDirectory && DefaultSVNOptions.matches(pattern, relativePath + "/"))) {
                    return true;
                }
                continue;
            }
            
            if (DefaultSVNOptions.matches(pattern, name)) {
                return true;
            } else if (isDirectory && DefaultSVNOptions.matches(pattern, dirName)) {
                return true;
            }
        }
        return false;
    }

    public void setFileProvider(ISVNStatusFileProvider fileProvider) {
        myFileProvider = new WrapperSVNStatusFileProvider(myDefaultFileProvider, fileProvider);
    }

    private static class WrapperSVNStatusFileProvider implements ISVNStatusFileProvider {
        private final ISVNStatusFileProvider myDefault;
        private final ISVNStatusFileProvider myDelegate;

        private WrapperSVNStatusFileProvider(ISVNStatusFileProvider defaultProvider, ISVNStatusFileProvider delegate) {
            myDefault = defaultProvider;
            myDelegate = delegate;
        }

        public Map getChildrenFiles(File parent) {
            final Map result = myDelegate.getChildrenFiles(parent);
            if (result != null) {
                return result;
            }
            return myDefault.getChildrenFiles(parent);
        }
    }

    private static class DefaultSVNStatusFileProvider implements ISVNStatusFileProvider {
        public Map getChildrenFiles(File parent) {
            File[] children = SVNFileListUtil.listFiles(parent);
            if (children != null) {
                Map map = new SVNHashMap();
                for (int i = 0; i < children.length; i++) {
                    map.put(children[i].getName(), children[i]);
                }
                return map;
            }
            return Collections.EMPTY_MAP;
        }
    }
}
