/*
 * ====================================================================
 * 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.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;

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.SVNRevisionProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.internal.io.fs.FSEntry;
import org.tmatesoft.svn.core.internal.io.fs.FSFS;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryUtil;
import org.tmatesoft.svn.core.internal.io.fs.FSRevisionNode;
import org.tmatesoft.svn.core.internal.io.fs.FSRevisionRoot;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNAdminHelper {
    
    public static int writeRevisionProperties(SVNRepository toRepository, long revision, SVNProperties revProps) throws SVNException {
        int filteredCount = 0;
        for (Iterator propNamesIter = revProps.nameSet().iterator(); propNamesIter.hasNext();) {
            String propName = (String) propNamesIter.next();
            SVNPropertyValue propValue = revProps.getSVNPropertyValue(propName);
            if (propName.startsWith(SVNProperty.SVN_SYNC_PREFIX)) {
                filteredCount++;
            } else {
                toRepository.setRevisionPropertyValue(revision, propName, propValue);
            }
        }
        return filteredCount;
    }

    public static void removePropertiesNotInSource(SVNRepository repository, long revision, 
            SVNProperties sourceProps, SVNProperties targetProps) throws SVNException {
        for (Iterator propNamesIter = targetProps.nameSet().iterator(); propNamesIter.hasNext();) {
            String propName = (String) propNamesIter.next();
            if (sourceProps.getSVNPropertyValue(propName) == null) {
                repository.setRevisionPropertyValue(revision, propName, null);
            }
        }
    }

    public static FSFS openRepository(File reposRootPath, boolean openFS) throws SVNException {
        FSFS fsfs = new FSFS(reposRootPath);
        if (openFS) {
            fsfs.open();
        } else {
            fsfs.openRoot();
            fsfs.getFSType();
        }
        return fsfs;
    }

    public static void closeRepository(FSFS fsfs) {
        if (fsfs != null) {
            try {
                fsfs.close();
            } catch (SVNException e) {
                SVNDebugLog.getDefaultLog().logFine(SVNLogType.FSFS, e);
                SVNDebugLog.getDefaultLog().logFine(SVNLogType.FSFS, e.getMessage());
            }
        }
    }

    public static FSFS openRepositoryForRecovery(File reposRootPath) throws SVNException {
        FSFS fsfs = new FSFS(reposRootPath);
        fsfs.openForRecovery();
        return fsfs;
    }

    public static long getRevisionNumber(SVNRevision revision, long youngestRevision, FSFS fsfs) throws SVNException {
        long revNumber = -1;
        if (revision.getNumber() >= 0) {
            revNumber = revision.getNumber();
        } else if (revision == SVNRevision.HEAD) {
            revNumber = youngestRevision;
        } else if (revision.getDate() != null) {
            revNumber = fsfs.getDatedRevision(revision.getDate());
        } else if (revision != SVNRevision.UNDEFINED) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CL_ARG_PARSING_ERROR, "Invalid revision specifier");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        
        if (revNumber > youngestRevision) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CL_ARG_PARSING_ERROR, "Revisions must not be greater than the youngest revision ({0})", new Long(youngestRevision));
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        return revNumber;
    }
    
    public static void writeProperties(SVNProperties props, SVNProperties oldProps, OutputStream dumpStream) throws SVNException {
        LinkedList propNames = new LinkedList();
        for(Iterator names = props.nameSet().iterator(); names.hasNext();) {
            String propName = (String) names.next();
            if (SVNRevisionProperty.LOG.equals(propName)) {
                  propNames.addFirst(propName);
            } else if (SVNRevisionProperty.AUTHOR.equals(propName)) {
                if (propNames.contains(SVNRevisionProperty.LOG)) {
                    int ind = propNames.indexOf(SVNRevisionProperty.LOG);
                    propNames.add(ind + 1, propName);
                } else {
                    propNames.addFirst(propName);
                }
            } else {
                propNames.addLast(propName);
            }
        }
        
        for(Iterator names = propNames.iterator(); names.hasNext();) {
            String propName = (String) names.next();
            SVNPropertyValue propValue = props.getSVNPropertyValue(propName);
            if (oldProps != null) {
                SVNPropertyValue oldValue = oldProps.getSVNPropertyValue(propName);
                if (oldValue != null && oldValue.equals(propValue)) {
                    continue;
                }
            }
            
            SVNWCProperties.appendProperty(propName, propValue, dumpStream);
        }
        
        if (oldProps != null) {
            for(Iterator names = oldProps.nameSet().iterator(); names.hasNext();) {
                String propName = (String) names.next();
                if (props.containsName(propName)) {
                    continue;
                }
                SVNWCProperties.appendPropertyDeleted(propName, dumpStream);
            }            
        }
        
        try {
            byte[] terminator = "PROPS-END\n".getBytes("UTF-8");
            dumpStream.write(terminator);
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        }
    }
    
    public static void deltifyDir(FSFS fsfs, FSRevisionRoot srcRoot, String srcParentDir, 
                                  String srcEntry, FSRevisionRoot tgtRoot, String tgtFullPath, 
                                  ISVNEditor editor) throws SVNException {
        if (srcParentDir == null) {
            generateNotADirError("source parent", srcParentDir);
        }
        
        if (tgtFullPath == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_PATH_SYNTAX, 
                                                         "Invalid target path");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        
        String srcFullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(srcParentDir, srcEntry));
        SVNNodeKind tgtKind = tgtRoot.checkNodeKind(tgtFullPath); 
        SVNNodeKind srcKind = srcRoot.checkNodeKind(srcFullPath);
        
        if (tgtKind == SVNNodeKind.NONE && srcKind == SVNNodeKind.NONE) {
            editor.closeEdit();
            return;
        }
        
        if (srcEntry == null && (srcKind != SVNNodeKind.DIR || tgtKind != SVNNodeKind.DIR)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_PATH_SYNTAX, "Invalid editor anchoring; at least one of the input paths is not a directory and there was no source entry");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        
        editor.targetRevision(tgtRoot.getRevision());
        long rootRevision = srcRoot.getRevision();
        if (tgtKind == SVNNodeKind.NONE) {
            editor.openRoot(rootRevision);
            editor.deleteEntry(srcEntry, -1);
            editor.closeDir();
            editor.closeEdit();
            return;
        }
        if (srcKind == SVNNodeKind.NONE) {
            editor.openRoot(rootRevision);
            addFileOrDir(fsfs, editor, srcRoot, tgtRoot, tgtFullPath, srcEntry, tgtKind);
            editor.closeDir();
            editor.closeEdit();
            return;
        }

        FSRevisionNode srcNode = srcRoot.getRevisionNode(srcFullPath);
        FSRevisionNode tgtNode = tgtRoot.getRevisionNode(tgtFullPath);
        int distance = srcNode.getId().compareTo(tgtNode.getId());
        if (distance == 0) {
            editor.closeEdit();
            return;
        } else if (srcEntry != null) {
            if (srcKind != tgtKind || distance == -1) {
                editor.openRoot(rootRevision);
                editor.deleteEntry(srcEntry, -1);
                addFileOrDir(fsfs, editor, srcRoot, tgtRoot, tgtFullPath, srcEntry, tgtKind);
            } else {
                editor.openRoot(rootRevision);
                replaceFileOrDir(fsfs, editor, srcRoot, tgtRoot, srcFullPath, tgtFullPath, srcEntry, tgtKind);
            }
            editor.closeDir();
            editor.closeEdit();
        } else {
            editor.openRoot(rootRevision);
            deltifyDirs(fsfs, editor, srcRoot, tgtRoot, srcFullPath, tgtFullPath, "");
            editor.closeDir();
            editor.closeEdit();
        }
    }
    
    public static void generateIncompleteDataError() throws SVNException {
        SVNDebugLog.getDefaultLog().logFine(SVNLogType.FSFS, new Exception());
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.INCOMPLETE_DATA, 
                "Premature end of content data in dumpstream");
        SVNErrorManager.error(err, SVNLogType.FSFS);
    }

    public static void generateStreamMalformedError() throws SVNException {
        SVNDebugLog.getDefaultLog().logFine(SVNLogType.FSFS, new Exception());
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_MALFORMED_DATA, "Dumpstream data appears to be malformed");
        SVNErrorManager.error(err, SVNLogType.FSFS);
    }

    public static int readKeyOrValue(InputStream dumpStream, byte[] buffer, int len) throws SVNException, IOException {
        int read = 0;
        while(len - read > 0) {
            int r = dumpStream.read(buffer, read, len - read);
            if (r < 0) {
                break;
            }
            read += r;
        }        
        if (read != len) {
            SVNAdminHelper.generateIncompleteDataError();
        }
        if (buffer[len - 1] != '\n') {
            SVNAdminHelper.generateStreamMalformedError();
        }
        return read - 1;
    }

    private static void addFileOrDir(FSFS fsfs, ISVNEditor editor, FSRevisionRoot srcRoot, 
            FSRevisionRoot tgtRoot, String tgtPath, String editPath, SVNNodeKind tgtKind) throws SVNException {
        if (tgtKind == SVNNodeKind.DIR) {
            editor.addDir(editPath, null, -1);
            deltifyDirs(fsfs, editor, srcRoot, tgtRoot, null, tgtPath, editPath);
            editor.closeDir();
        } else {
            editor.addFile(editPath, null, -1);
            deltifyFiles(fsfs, editor, srcRoot, tgtRoot, null, tgtPath, editPath);
            FSRevisionNode tgtNode = tgtRoot.getRevisionNode(tgtPath);
            editor.closeFile(editPath, tgtNode.getFileMD5Checksum());
        }
    }

    private static void replaceFileOrDir(FSFS fsfs, ISVNEditor editor, FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, String tgtPath, String editPath, SVNNodeKind tgtKind) throws SVNException {
        long baseRevision = srcRoot.getRevision();
        if (tgtKind == SVNNodeKind.DIR) {
            editor.openDir(editPath, baseRevision);
            deltifyDirs(fsfs, editor, srcRoot, tgtRoot, srcPath, tgtPath, editPath);
            editor.closeDir();
        } else {
            editor.openFile(editPath, baseRevision);
            deltifyFiles(fsfs, editor, srcRoot, tgtRoot, srcPath, tgtPath, editPath);
            FSRevisionNode tgtNode = tgtRoot.getRevisionNode(tgtPath);
            editor.closeFile(editPath, tgtNode.getFileMD5Checksum());
        }
    }
    
    private static void deltifyFiles(FSFS fsfs, ISVNEditor editor, FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, String tgtPath, String editPath) throws SVNException {
        deltifyProperties(fsfs, editor, srcRoot, tgtRoot, srcPath, tgtPath, editPath, false);
        
        boolean changed = false;
        if (srcPath != null) {
            changed = FSRepositoryUtil.areFileContentsChanged(srcRoot, srcPath, tgtRoot, tgtPath);
        }
        
        if (changed) {
            String srcHexDigest = null;
            if (srcPath != null) {
                FSRevisionNode srcNode = srcRoot.getRevisionNode(srcPath);
                srcHexDigest = srcNode.getFileMD5Checksum();
            }
            editor.applyTextDelta(editPath, srcHexDigest);
            editor.textDeltaChunk(editPath, SVNDiffWindow.EMPTY);
        }
    }
    
    private static void deltifyDirs(FSFS fsfs, ISVNEditor editor, FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, String tgtPath, String editPath) throws SVNException {
        deltifyProperties(fsfs, editor, srcRoot, tgtRoot, srcPath, tgtPath, editPath, true);

        FSRevisionNode targetNode = tgtRoot.getRevisionNode(tgtPath);
        Map targetEntries = targetNode.getDirEntries(fsfs);
        
        Map sourceEntries = null;
        if (srcPath != null) {
            FSRevisionNode sourceNode = srcRoot.getRevisionNode(srcPath);
            sourceEntries = sourceNode.getDirEntries(fsfs);
        }

        for (Iterator tgtEntries = targetEntries.keySet().iterator(); tgtEntries.hasNext();) {
            String name = (String) tgtEntries.next();
            FSEntry tgtEntry = (FSEntry) targetEntries.get(name);
            
            SVNNodeKind tgtKind = tgtEntry.getType();
            String targetFullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(tgtPath, tgtEntry.getName()));
            String editFullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(editPath, tgtEntry.getName()));
            
            if (sourceEntries != null && sourceEntries.containsKey(name)) {
                FSEntry srcEntry = (FSEntry) sourceEntries.get(name);
                String sourceFullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(srcPath, tgtEntry.getName()));
                SVNNodeKind srcKind = srcEntry.getType();
                
                int distance = srcEntry.getId().compareTo(tgtEntry.getId());
                if (srcKind != tgtKind || distance == -1) {
                    editor.deleteEntry(editFullPath, -1);
                    addFileOrDir(fsfs, editor, srcRoot, tgtRoot, targetFullPath, editFullPath, tgtKind);
                } else if (distance != 0) {
                    replaceFileOrDir(fsfs, editor, srcRoot, tgtRoot, sourceFullPath, targetFullPath, editFullPath, tgtKind);
                }
                sourceEntries.remove(name);
            } else {
                addFileOrDir(fsfs, editor, srcRoot, tgtRoot, targetFullPath, editFullPath, tgtKind);
            }
        }
        
        if (sourceEntries != null) {
            for (Iterator srcEntries = sourceEntries.keySet().iterator(); srcEntries.hasNext();) {
                String name = (String) srcEntries.next();
                FSEntry entry = (FSEntry) sourceEntries.get(name);
                String editFullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(editPath, entry.getName()));
                editor.deleteEntry(editFullPath, -1);
            }
        }
    }

    private static void deltifyProperties(FSFS fsfs, ISVNEditor editor, FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, String tgtPath, String editPath, boolean isDir) throws SVNException {
        FSRevisionNode targetNode = tgtRoot.getRevisionNode(tgtPath);

        SVNProperties sourceProps = null;
        if (srcPath != null) {
            FSRevisionNode sourceNode = srcRoot.getRevisionNode(srcPath);
            boolean propsChanged = !FSRepositoryUtil.arePropertiesEqual(sourceNode, targetNode);
            if (!propsChanged) {
                return;
            }
            sourceProps = sourceNode.getProperties(fsfs);
        } else {
            sourceProps = new SVNProperties();
        }

        SVNProperties targetProps = targetNode.getProperties(fsfs);
        SVNProperties propsDiffs = FSRepositoryUtil.getPropsDiffs(sourceProps, targetProps);
        Object[] names = propsDiffs.nameSet().toArray();
        for (int i = 0; i < names.length; i++) {
            String propName = (String) names[i];
            SVNPropertyValue propValue = propsDiffs.getSVNPropertyValue(propName);
            if (isDir) {
                editor.changeDirProperty(propName, propValue);
            } else {
                editor.changeFileProperty(editPath, propName, propValue);
            }
        }
    }
    
    private static void generateNotADirError(String role, String path) throws SVNException {
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_DIRECTORY, "Invalid {0} directory ''{1}''", new Object[]{role, path != null ? path : "(null)"});
        SVNErrorManager.error(err, SVNLogType.FSFS);
    }
    
    public static final String DUMPFILE_MAGIC_HEADER               = "SVN-fs-dump-format-version";
    public static final String DUMPFILE_CONTENT_LENGTH             = "Content-length";
    public static final String DUMPFILE_NODE_ACTION                = "Node-action";
    public static final String DUMPFILE_NODE_COPYFROM_PATH         = "Node-copyfrom-path";
    public static final String DUMPFILE_NODE_COPYFROM_REVISION     = "Node-copyfrom-rev";
    public static final String DUMPFILE_NODE_KIND                  = "Node-kind";
    public static final String DUMPFILE_NODE_PATH                  = "Node-path";
    public static final String DUMPFILE_PROP_CONTENT_LENGTH        = "Prop-content-length";
    public static final String DUMPFILE_PROP_DELTA                 = "Prop-delta";
    public static final String DUMPFILE_REVISION_NUMBER            = "Revision-number";
    public static final String DUMPFILE_TEXT_CONTENT_LENGTH        = "Text-content-length";
    public static final String DUMPFILE_TEXT_DELTA                 = "Text-delta";
    public static final String DUMPFILE_UUID                       = "UUID";
    public static final String DUMPFILE_TEXT_CONTENT_MD5           = "Text-content-md5";
    public static final String DUMPFILE_TEXT_CONTENT_SHA1          = "Text-content-sha1";
    public static final String DUMPFILE_TEXT_COPY_SOURCE_MD5       = "Text-copy-source-md5";
    public static final String DUMPFILE_TEXT_COPY_SOURCE_SHA1      = "Text-copy-source-sha1"; 
    public static final String DUMPFILE_TEXT_DELTA_BASE_MD5        = "Text-delta-base-md5";
    public static final String DUMPFILE_TEXT_DELTA_BASE_SHA1       = "Text-delta-base-sha1";
    public static final int DUMPFILE_FORMAT_VERSION                = 3;

    public static final int NODE_ACTION_ADD     = 1;
    public static final int NODE_ACTION_CHANGE  = 0;
    public static final int NODE_ACTION_DELETE  = 2;
    public static final int NODE_ACTION_REPLACE = 3;
    public static final int NODE_ACTION_UNKNOWN = -1;

}
