package org.tmatesoft.svn.core.internal.wc2.ng;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.logging.Level;

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.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetDb;
import org.tmatesoft.svn.core.internal.util.SVNSkel;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.internal.wc.IOExceptionWrapper;
import org.tmatesoft.svn.core.internal.wc.ISVNFileContentFetcher;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNEventFactory;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNPropertiesManager;
import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.db.*;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbKind;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbStatus;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.NodeInfo;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc.SVNPropertyData;
import org.tmatesoft.svn.core.wc2.ISvnAddParameters;
import org.tmatesoft.svn.core.wc2.ISvnObjectReceiver;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.core.wc2.hooks.ISvnPropertyValueProvider;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

public class SvnNgPropertiesManager {

    public static Collection<String> getGlobalIgnores(ISVNOptions options) {
        Collection<String> allPatterns = new HashSet<String>();
        String[] ignores = options.getIgnorePatterns();
        for (int i = 0; ignores != null && i < ignores.length; i++) {
            allPatterns.add(ignores[i]);
        }
        return allPatterns;
        
    }
    
    public static Collection<String> getEffectiveIgnores(SVNWCContext context, File absPath, Collection<String> globalIgnores) {
        Collection<String> allPatterns = new HashSet<String>();
        if (globalIgnores != null) {
            allPatterns.addAll(globalIgnores);
        } else {
            allPatterns.addAll(getGlobalIgnores(context.getOptions()));
        }
        
        if (context != null && absPath != null) {
            try {
                String ignoreProperty = context.getProperty(absPath, SVNProperty.IGNORE);
                if (ignoreProperty != null) {
                    for (StringTokenizer tokens = new StringTokenizer(ignoreProperty, "\r\n"); tokens.hasMoreTokens();) {
                        String token = tokens.nextToken().trim();
                        if (token.length() > 0) {
                            allPatterns.add(token);
                        }
                    }
                }
                ignoreProperty = context.getProperty(absPath, SVNProperty.INHERITABLE_IGNORES);
                if (ignoreProperty != null) {
                    for (StringTokenizer tokens = new StringTokenizer(ignoreProperty, "\r\n"); tokens.hasMoreTokens();) {
                        String token = tokens.nextToken().trim();
                        if (token.length() > 0) {
                            allPatterns.add(token);
                        }
                    }
                }

                SVNWCDb db = (SVNWCDb) context.getDb();
                SVNWCDb.DirParsedInfo parsed = db.parseDir(absPath, SVNSqlJetDb.Mode.ReadOnly);
                List<Structure<StructureFields.InheritedProperties>> inheritedProperties = SvnWcDbProperties.readInheritedProperties(parsed.wcDbDir.getWCRoot(), parsed.localRelPath, SVNProperty.INHERITABLE_IGNORES);
                for (Structure<StructureFields.InheritedProperties> inheritedProperty : inheritedProperties) {
                    SVNProperties properties = inheritedProperty.get(StructureFields.InheritedProperties.properties);
                    String path = inheritedProperty.get(StructureFields.InheritedProperties.pathOrURL);
                    ignoreProperty = properties.getStringValue(SVNProperty.INHERITABLE_IGNORES);
                    if (ignoreProperty != null) {
                        for (StringTokenizer tokens = new StringTokenizer(ignoreProperty, "\r\n"); tokens.hasMoreTokens();) {
                            String token = tokens.nextToken().trim();
                            if (token.length() > 0) {
                                allPatterns.add(token);
                            }
                        }
                    }
                }
            } catch (SVNException e) {
            }
        }
        return allPatterns;
    }

    public static boolean isIgnored(String name, Collection<String> patterns) {
        if (patterns == null) {
            return false;
        }
        for (Iterator<String> ps = patterns.iterator(); ps.hasNext();) {
            String pattern = (String) ps.next();
            if (DefaultSVNOptions.matches(pattern, name)) {
                return true;
            }
        }
        return false;
    }

    public static Map<String, Map<String, String>> parseAutoProperties(SVNPropertyValue autoProperties, Map<String, Map<String, String>> target) {
        String autoPropertiesString = SVNPropertyValue.getPropertyAsString(autoProperties);
        target = target == null ? new HashMap<String, Map<String, String>>() : target;
        if (autoPropertiesString == null) {
            return target;
        }
        String[] lines = autoPropertiesString.split("\n");
        for (String line : lines) {
            line = line.trim();
            if (line.length() == 0) {
                continue;
            }
            int pos = line.indexOf('=');
            if (pos < 0) {
                continue;
            }
            String pattern = line.substring(0, pos).trim();
            String keyValuePairsString = line.substring(pos + 1).trim();

            Map<String, String> properties = target.get(pattern);
            if (properties == null) {
                properties = new HashMap<String, String>();
                target.put(pattern, properties);
            }

            String[] keyValuePairs = keyValuePairsString.split(";");
            for (String keyValuePair : keyValuePairs) {
                keyValuePair = keyValuePair.trim();
                if (keyValuePair.length() == 0) {
                    continue;
                }
                String propertyName;
                String propertyValue;
                pos = keyValuePair.indexOf('=');
                if (pos < 0) {
                    propertyName = keyValuePair.trim();
                    propertyValue = "*";
                } else {
                    propertyName = keyValuePair.substring(0, pos).trim();
                    propertyValue = keyValuePair.substring(pos + 1).trim();
                }
                properties.put(propertyName, propertyValue);
            }

        }
        return target;
    }

    public static Map<String, String> getMatchedAutoProperties(String fileName, Map<String, Map<String, String>> autoProperties) {
        if (autoProperties == null) {
            return Collections.emptyMap();
        }
        Map<String, String> matchedAutoProperties = new HashMap<String, String>();
        for (Map.Entry<String, Map<String, String>> entry : autoProperties.entrySet()) {
            String pattern = entry.getKey();
            if (DefaultSVNOptions.matches(pattern, fileName)) {
                Map<String, String> properties = entry.getValue();
                matchedAutoProperties.putAll(properties);
            }
        }
        return matchedAutoProperties;
    }

    public static void setProperty(final SVNWCContext context, File path, final String propertyName, final SVNPropertyValue propertyValue, SVNDepth depth, final boolean skipChecks,
            final ISVNEventHandler eventHandler, Collection<String> changelists) throws SVNException {
        setProperty(context, path, propertyName, propertyValue, null, depth, skipChecks, eventHandler, null, changelists);
    }

    public static void setProperty(final SVNWCContext context, File path, final String propertyName, final SVNPropertyValue propertyValue, SVNDepth depth, final boolean skipChecks, 
            final ISVNEventHandler eventHandler, final ISvnObjectReceiver<SVNPropertyData> receiver, Collection<String> changelists) throws SVNException {
        setProperty(context, path, propertyName, propertyValue, null, depth, skipChecks, eventHandler, receiver, changelists);
    }

    public static void setProperty(final SVNWCContext context, File path, final String propertyName, final SVNPropertyValue propertyValue, final ISvnPropertyValueProvider pvProvider, SVNDepth depth, final boolean skipChecks, 
            final ISVNEventHandler eventHandler, final ISvnObjectReceiver<SVNPropertyData> receiver, final Collection<String> changelists) throws SVNException {
        if (propertyName != null) {
            if (SVNProperty.isEntryProperty(propertyName)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_PROP_KIND, "Property ''{0}'' is an entry property", propertyName);
                SVNErrorManager.error(err, SVNLogType.WC);
            } else if (SVNProperty.isWorkingCopyProperty(propertyName)) {
                SVNProperties wcProps = context.getDb().getBaseDavCache(path);
                if (wcProps == null && propertyValue != null) {
                    wcProps = new SVNProperties();
                }
                if (wcProps != null) {
                    if (propertyValue != null) {
                        wcProps.put(propertyName, propertyValue);
                    } else {
                        wcProps.remove(propertyName);
                    }
                }
                context.getDb().setBaseDavCache(path, wcProps);
                return;
            }
        }
        SVNNodeKind kind = context.readKind(path, true);
        File dirPath;
        if (kind == SVNNodeKind.DIR) {
            dirPath = path;
        } else {
            dirPath = SVNFileUtil.getParentFile(path);
        }
        context.writeCheck(dirPath);
        if (depth == SVNDepth.EMPTY) {
            if (!context.matchesChangelist(path, changelists)) {
                return;
            }
            setProperty(context, path, kind, propertyName, propertyValue, pvProvider, skipChecks, eventHandler, receiver);
        } else {
            context.nodeWalkChildren(path, new SVNWCContext.ISVNWCNodeHandler() {
                public void nodeFound(File localAbspath, SVNWCDbKind kind) throws SVNException {
                    try {
                        setProperty(context, localAbspath, kind.toNodeKind(), propertyName, propertyValue, pvProvider, skipChecks, eventHandler, receiver);
                    } catch (SVNException e) {
                        if (e.getErrorMessage().getErrorCode() == SVNErrorCode.ILLEGAL_TARGET ||
                                e.getErrorMessage().getErrorCode() == SVNErrorCode.WC_INVALID_SCHEDULE) {
                            return;
                        }
                        throw e;
                    }
                }
            }, false, depth, changelists);
        }
    }

    public static void setProperty(final SVNWCContext context, final File path, SVNNodeKind kind, String propertyName, SVNPropertyValue value, 
            ISvnPropertyValueProvider pvProvider, boolean skipChecks, ISVNEventHandler eventHandler, ISvnObjectReceiver<SVNPropertyData> receiver) throws SVNException {
        Structure<NodeInfo> nodeInfo = context.getDb().readInfo(path, NodeInfo.status);
        ISVNWCDb.SVNWCDbStatus status = nodeInfo.get(NodeInfo.status);
        if (status != SVNWCDbStatus.Normal && status != SVNWCDbStatus.Added && status != SVNWCDbStatus.Incomplete) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_INVALID_SCHEDULE,
                    "Can''t set properties on ''{0}'': invalid status for updating properties.",
                    path);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        
        if (pvProvider == null && value != null && SVNProperty.isSVNProperty(propertyName)) {            
            final ISVNFileContentFetcher fetcher = new ISVNFileContentFetcher() {                
                public SVNPropertyValue getProperty(String propertyName) throws SVNException {
                    return context.getPropertyValue(path, propertyName);
                }                
                public boolean fileIsBinary() throws SVNException {
                    SVNPropertyValue mimeType = context.getPropertyValue(path, SVNProperty.MIME_TYPE);
                    return mimeType != null && SVNProperty.isBinaryMimeType(mimeType.getString());
                }                
                public void fetchFileContent(OutputStream os) throws SVNException {
                    InputStream is = null;
                    try {
                        is = SVNFileUtil.openFileForReading(path);
                        SVNTranslator.copy(is, os);
                    } catch (IOExceptionWrapper ioew) {
                        throw ioew.getOriginalException();
                    } catch (IOException e) {
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e);
                        SVNErrorManager.error(err, SVNLogType.WC);
                    } finally {
                        SVNFileUtil.closeFile(is);
                    }
                }
            };
            SVNPropertyValue pv = SVNPropertiesManager.validatePropertyValue(path, kind, propertyName, value, skipChecks, context.getOptions(), fetcher);
            value = pv;
        }
        SVNSkel workItems = null;
        if (pvProvider == null && kind == SVNNodeKind.FILE && (SVNProperty.EXECUTABLE.equals(propertyName) || SVNProperty.NEEDS_LOCK.equals(propertyName))) {
            workItems = context.wqBuildSyncFileFlags(path);
        }        
        
        SVNProperties properties = new SVNProperties(); 
        try {
            properties = context.getDb().readProperties(path);
        } catch (SVNException e) {
            SVNErrorMessage err = e.getErrorMessage().wrap("Failed to load current properties");
            SVNErrorManager.error(err, e, SVNLogType.WC);
        }
        if (pvProvider != null) {
            
            SVNPropertyValue oldKeywords = properties.getSVNPropertyValue(SVNProperty.KEYWORDS);
            SVNPropertyValue oldEolStyle = properties.getSVNPropertyValue(SVNProperty.EOL_STYLE);
            boolean wasExecutable = properties.containsName(SVNProperty.EXECUTABLE);
            boolean wasNeedsLock = properties.containsName(SVNProperty.NEEDS_LOCK);
            
            properties = pvProvider.providePropertyValues(path, properties);
            
            if (properties == null) {
                properties = new SVNProperties();
            }
            
            SVNPropertyValue newKeywords = properties.getSVNPropertyValue(SVNProperty.KEYWORDS);
            SVNPropertyValue newEolStyle = properties.getSVNPropertyValue(SVNProperty.EOL_STYLE);
            boolean isExecutable = properties.containsName(SVNProperty.EXECUTABLE);
            boolean isNeedsLock = properties.containsName(SVNProperty.NEEDS_LOCK);

            if (isExecutable != wasExecutable || isNeedsLock != wasNeedsLock) {
                workItems = context.wqBuildSyncFileFlags(path);
            }
            
            boolean clearRecordedInfo = !equals(oldKeywords, newKeywords) || !equals(oldEolStyle, newEolStyle);
            
            context.getDb().opSetProps(path, properties, null, clearRecordedInfo, workItems);
            if (workItems != null) {
                context.wqRun(path);
            }
            return;
        }

        boolean clearRecordedInfo = false;
        if (kind == SVNNodeKind.FILE && SVNProperty.KEYWORDS.equals(propertyName)) {
            String oldValue = properties.getStringValue(SVNProperty.KEYWORDS);
            Map<String, byte[]> keywords = oldValue != null ? context.getKeyWords(path, oldValue) : new HashMap<String, byte[]>();
            Map<String, byte[]> newKeywords = value != null ? context.getKeyWords(path, value.getString()) : new HashMap<String, byte[]>();
            if (!keywords.equals(newKeywords)) {
                clearRecordedInfo = true;
            }
        } else if (kind == SVNNodeKind.FILE && SVNProperty.EOL_STYLE.equals(propertyName)) {
            String oldValue = properties.getStringValue(SVNProperty.EOL_STYLE);
            if (oldValue == null || value == null) {
                clearRecordedInfo = (oldValue != null && value == null) || (oldValue == null && value != null);
            } else {
                clearRecordedInfo = !SVNPropertyValue.create(oldValue).equals(value);
            }
        }

        SVNEventAction action;
        if (!properties.containsName(propertyName)) {
            action = value == null ? SVNEventAction.PROPERTY_DELETE_NONEXISTENT : SVNEventAction.PROPERTY_ADD; 
        } else {
            action = value == null ? SVNEventAction.PROPERTY_DELETE : SVNEventAction.PROPERTY_MODIFY; 
        }            
        if (value != null) {
            properties.put(propertyName, value);
        } else {
            properties.remove(propertyName);
        }
        context.getDb().opSetProps(path, properties, null, clearRecordedInfo, workItems);
        if (workItems != null) {
            context.wqRun(path);
        }
        if (receiver != null) {
            receiver.receive(SvnTarget.fromFile(path), new SVNPropertyData(propertyName, value, context.getOptions()));
        }
        if (eventHandler != null) {
            SVNEvent event = SVNEventFactory.createSVNEvent(path, SVNNodeKind.NONE, 
                    null, -1, action, action, null, null, 1, 1, null, propertyName);
            eventHandler.handleEvent(event, -1);
        }
    }

    public static void setAutoProperties(final SVNWCContext context, final File path, final SVNProperties properties,
            final ISvnAddParameters addParameters, Runnable onValidationError) throws SVNException {
        final ISVNFileContentFetcher fetcher = new ISVNFileContentFetcher() {                
            public SVNPropertyValue getProperty(String propertyName) throws SVNException {
                return properties.getSVNPropertyValue(propertyName);
            }                
            public boolean fileIsBinary() throws SVNException {
                SVNPropertyValue mimeType = getProperty(SVNProperty.MIME_TYPE);
                return mimeType != null && SVNProperty.isBinaryMimeType(mimeType.getString());
            }                
            public void fetchFileContent(OutputStream os) throws SVNException {
                InputStream is = null;
                try {
                    is = SVNFileUtil.openFileForReading(path);
                    SVNTranslator.copy(is, os);
                } catch (IOExceptionWrapper ioew) {
                    throw ioew.getOriginalException();
                } catch (IOException e) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e);
                    SVNErrorManager.error(err, SVNLogType.WC);
                } finally {
                    SVNFileUtil.closeFile(is);
                }
            }
        };
        
        final SVNProperties validation = new SVNProperties(properties);
        String detectedMimeType = null;
        for(String propertyName : validation.nameSet()) {
            try {
                final SVNPropertyValue pv = SVNPropertiesManager.validatePropertyValue(path, 
                        SVNNodeKind.FILE, propertyName, properties.getSVNPropertyValue(propertyName), false, context.getOptions(), fetcher);
                properties.put(propertyName, pv);
            } catch (SVNException e) {
                if (SVNProperty.EOL_STYLE.equals(propertyName) &&
                        e.getErrorMessage().getErrorCode() == SVNErrorCode.ILLEGAL_TARGET &&
                        e.getErrorMessage().getMessage().indexOf("newlines") >= 0) {
                    ISvnAddParameters.Action action = addParameters.onInconsistentEOLs(path);
                    if (action == ISvnAddParameters.Action.REPORT_ERROR) {
                        try {
                            onValidationError.run();
                        } catch (Throwable th) {
                            SVNDebugLog.getDefaultLog().log(SVNLogType.WC, th, Level.INFO);
                        }
                            
                        throw e;
                    } else if (action == ISvnAddParameters.Action.ADD_AS_IS) {
                        properties.remove(propertyName);
                    } else if (action == ISvnAddParameters.Action.ADD_AS_BINARY) {
                        properties.remove(propertyName);
                        detectedMimeType = SVNFileUtil.BINARY_MIME_TYPE;
                    }
                } else {
                    try {
                        onValidationError.run();
                    } catch (Throwable th) {
                        SVNDebugLog.getDefaultLog().log(SVNLogType.WC, th, Level.INFO);
                    }
                    throw e;
                }
            }
        }
        if (detectedMimeType != null) {
            properties.put(SVNProperty.MIME_TYPE, detectedMimeType);
        }
        SVNSkel workItems = null;
        if (properties.containsName(SVNProperty.EXECUTABLE) || properties.containsName(SVNProperty.NEEDS_LOCK)) {
            workItems = context.wqBuildSyncFileFlags(path);
        }        

        final boolean clearRecordedInfo = properties.containsName(SVNProperty.KEYWORDS) || properties.containsName(SVNProperty.EOL_STYLE);
        context.getDb().opSetProps(path, properties, null, clearRecordedInfo, workItems);
        if (workItems != null) {
            context.wqRun(path);
        }
    }

    private static boolean equals(SVNPropertyValue oldValue, SVNPropertyValue newValue) {
        if (oldValue == null || newValue == null) {
            return oldValue == newValue;
        }
        return oldValue.equals(newValue);
    }

    public static void checkPropertyName(String propertyName, SVNPropertyValue propertyValue) throws SVNException {
        if (SVNRevisionProperty.isRevisionProperty(propertyName)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_PROPERTY_NAME, "Revision property ''{0}'' not allowed in this context", propertyName);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (propertyValue != null && !SVNPropertiesManager.isValidPropertyName(propertyName)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_PROPERTY_NAME, "Bad property name: ''{0}''", propertyName);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        if (SVNProperty.isWorkingCopyProperty(propertyName)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_PROPERTY_NAME, "''{0}'' is a wcprop, thus not accessible to clients", propertyName);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
    }
    
    public static void categorizeProperties(SVNProperties props, SVNProperties regular, SVNProperties entry, SVNProperties working) {
        for (String name : props.nameSet()) {
            SVNPropertyValue pv = props.getSVNPropertyValue(name);
            if (SVNProperty.isEntryProperty(name) && entry != null) {
                entry.put(name, pv);
            } else if (SVNProperty.isRegularProperty(name) && regular != null) {
                regular.put(name, pv);
            } else if (SVNProperty.isWorkingCopyProperty(name) && working != null) {
                working.put(name, pv);
            }
        }
    }

    public static void splitAndAppend(final List<String> patterns, final String ignores) {
        if (ignores == null) {
            return;
        }
        for (StringTokenizer tokens = new StringTokenizer(ignores, "\r\n"); tokens.hasMoreTokens();) {
            String token = tokens.nextToken().trim();
            if (token.length() > 0) {
                patterns.add(token);
            }
        }
    }
}