/*
 * ====================================================================
 * 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.util.Iterator;
import java.util.Map;

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.delta.SVNDeltaCombiner;
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.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNAdminDeltifier {

    private FSFS myFSFS;
    private SVNDepth myDepth;
    private boolean myIsIncludeEntryProperties;
    private boolean myIsIgnoreAncestry;
    private boolean myIsSendTextDeltas;
    private ISVNEditor myEditor;
    private SVNDeltaCombiner myDeltaCombiner;
    private SVNDeltaGenerator myDeltaGenerator;
    
    public SVNAdminDeltifier(FSFS fsfs, SVNDepth depth, boolean includeEntryProperties, 
            boolean ignoreAncestry, boolean sendTextDeltas, ISVNEditor editor) {
        myFSFS = fsfs;
        myDepth = depth;
        myIsIncludeEntryProperties = includeEntryProperties;
        myIsIgnoreAncestry = ignoreAncestry;
        myIsSendTextDeltas = sendTextDeltas;
        myEditor = editor;
        myDeltaCombiner = new SVNDeltaCombiner();
        myDeltaGenerator = new SVNDeltaGenerator();
    }
    
    public void setEditor(ISVNEditor editor) {
        myEditor = editor;
    }
    
    public void deltifyDir(FSRevisionRoot srcRoot, String srcParentDir, String srcEntry, 
            FSRevisionRoot tgtRoot, String tgtFullPath) 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) {
            myEditor.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);
        }

        myEditor.targetRevision(tgtRoot.getRevision());
        long rootRevision = srcRoot.getRevision();
        if (tgtKind == SVNNodeKind.NONE) {
            myEditor.openRoot(rootRevision);
            myEditor.deleteEntry(srcEntry, -1);
            myEditor.closeDir();
            myEditor.closeEdit();
            return;
        }

        if (srcKind == SVNNodeKind.NONE) {
            myEditor.openRoot(rootRevision);
            addFileOrDir(srcRoot, tgtRoot, tgtFullPath, srcEntry, tgtKind);
            myEditor.closeDir();
            myEditor.closeEdit();
            return;
        }

        FSRevisionNode srcNode = srcRoot.getRevisionNode(srcFullPath);
        FSRevisionNode tgtNode = tgtRoot.getRevisionNode(tgtFullPath);
        int distance = srcNode.getId().compareTo(tgtNode.getId());

        if (distance == 0) {
            myEditor.closeEdit();
        } else if (srcEntry != null) {
            if (srcKind != tgtKind || distance == -1) {
                myEditor.openRoot(rootRevision);
                myEditor.deleteEntry(srcEntry, -1);
                addFileOrDir(srcRoot, tgtRoot, tgtFullPath, srcEntry, tgtKind);
            } else {
                myEditor.openRoot(rootRevision);
                replaceFileOrDir(srcRoot, tgtRoot, srcFullPath, tgtFullPath, srcEntry, tgtKind);
            }

            myEditor.closeDir();
            myEditor.closeEdit();
        } else {
            myEditor.openRoot(rootRevision);
            deltifyDirs(srcRoot, tgtRoot, srcFullPath, tgtFullPath, "");
            myEditor.closeDir();
            myEditor.closeEdit();
        }
    }

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

    private void deltifyDirs(FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, 
            String tgtPath, String editPath) throws SVNException {
        deltifyProperties(srcRoot, tgtRoot, srcPath, tgtPath, editPath, true);

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

        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();
                
                if (myDepth == SVNDepth.INFINITY || srcKind != SVNNodeKind.DIR) {
                    int distance = srcEntry.getId().compareTo(tgtEntry.getId());
                    if (srcKind != tgtKind || (distance == -1 && !myIsIgnoreAncestry)) {
                        myEditor.deleteEntry(editFullPath, -1);
                        addFileOrDir(srcRoot, tgtRoot, targetFullPath, editFullPath, tgtKind);
                    } else if (distance != 0) {
                        replaceFileOrDir(srcRoot, tgtRoot, sourceFullPath, targetFullPath, 
                                editFullPath, tgtKind);
                    }
                    
                }
                sourceEntries.remove(name);
            } else {
                if (myDepth == SVNDepth.INFINITY || tgtKind != SVNNodeKind.DIR) {
                    addFileOrDir(srcRoot, tgtRoot, targetFullPath, editFullPath, tgtKind);
                }
            }
        }
        
        if (sourceEntries != null) {
            for (Iterator srcEntries = sourceEntries.keySet().iterator(); srcEntries.hasNext();) {
                String name = (String) srcEntries.next();
                FSEntry srcEntry = (FSEntry) sourceEntries.get(name);
                String editFullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(editPath, srcEntry.getName()));
                if (myDepth == SVNDepth.INFINITY || srcEntry.getType() != SVNNodeKind.DIR) {
                    myEditor.deleteEntry(editFullPath, -1);
                }
            }
        }
    }

    private void replaceFileOrDir(FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, 
            String tgtPath, String editPath, SVNNodeKind tgtKind) throws SVNException {
        long baseRevision = srcRoot.getRevision();
        if (tgtKind == SVNNodeKind.DIR) {
            myEditor.openDir(editPath, baseRevision);
            deltifyDirs(srcRoot, tgtRoot, srcPath, tgtPath, editPath);
            myEditor.closeDir();
        } else {
            myEditor.openFile(editPath, baseRevision);
            deltifyFiles(srcRoot, tgtRoot, srcPath, tgtPath, editPath);
            FSRevisionNode tgtNode = tgtRoot.getRevisionNode(tgtPath);
            myEditor.closeFile(editPath, tgtNode.getFileMD5Checksum());
        }
    }
    
    private void deltifyFiles(FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, 
            String tgtPath, String editPath) throws SVNException {
        deltifyProperties(srcRoot, tgtRoot, srcPath, tgtPath, editPath, false);
        
        boolean changed = false;
        if (srcPath != null) {
            if (myIsIgnoreAncestry) {
                changed = FSRepositoryUtil.checkFilesDifferent(srcRoot, srcPath, tgtRoot, tgtPath, 
                        myDeltaCombiner);
            } else {
                changed = FSRepositoryUtil.areFileContentsChanged(srcRoot, srcPath, tgtRoot, tgtPath);    
            }
        }
        
        if (changed) {
            String srcHexDigest = null;
            if (srcPath != null) {
                FSRevisionNode srcNode = srcRoot.getRevisionNode(srcPath);
                srcHexDigest = srcNode.getFileMD5Checksum();
            }
            
            FSRepositoryUtil.sendTextDelta(myEditor, editPath, srcPath, srcHexDigest, srcRoot, 
                    tgtPath, tgtRoot, myIsSendTextDeltas, myDeltaCombiner, myDeltaGenerator, myFSFS);
        }
    }

    private void deltifyProperties(FSRevisionRoot srcRoot, FSRevisionRoot tgtRoot, String srcPath, 
            String tgtPath, String editPath, boolean isDir) throws SVNException {
        if (myIsIncludeEntryProperties) {
            
            FSRevisionNode node = tgtRoot.getRevisionNode(tgtPath);
            long committedRevision = node.getCreatedRevision();
            if (SVNRevision.isValidRevisionNumber(committedRevision)) {
                if (isDir) {
                    myEditor.changeDirProperty(SVNProperty.COMMITTED_REVISION, SVNPropertyValue.create(String.valueOf(committedRevision)));
                } else {
                    myEditor.changeFileProperty(editPath, SVNProperty.COMMITTED_REVISION, SVNPropertyValue.create(String.valueOf(committedRevision)));
                }
                
                SVNProperties revisionProps = myFSFS.getRevisionProperties(committedRevision);
                String committedDateStr = revisionProps.getStringValue(SVNRevisionProperty.DATE);
                if (committedDateStr != null || srcPath != null) {
                    if (isDir) {
                        myEditor.changeDirProperty(SVNProperty.COMMITTED_DATE, SVNPropertyValue.create(committedDateStr));
                    } else {
                        myEditor.changeFileProperty(editPath,SVNProperty.COMMITTED_DATE, SVNPropertyValue.create(committedDateStr));
                    }
                }
                String lastAuthor = revisionProps.getStringValue(SVNRevisionProperty.AUTHOR);
                if (lastAuthor != null || srcPath != null) {
                    if (isDir) {
                        myEditor.changeDirProperty(SVNProperty.LAST_AUTHOR, SVNPropertyValue.create(lastAuthor));
                    } else {
                        myEditor.changeFileProperty(editPath,SVNProperty.LAST_AUTHOR, SVNPropertyValue.create(lastAuthor));
                    }
                }

                String uuid = myFSFS.getUUID();
                if (isDir) {
                    myEditor.changeDirProperty(SVNProperty.UUID, SVNPropertyValue.create(uuid));
                } else {
                    myEditor.changeFileProperty(editPath, SVNProperty.UUID, SVNPropertyValue.create(uuid));
                }
            }
        }
        
        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(myFSFS);
        } else {
            sourceProps = new SVNProperties();
        }

        SVNProperties targetProps = targetNode.getProperties(myFSFS);
        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) {
                myEditor.changeDirProperty(propName, propValue);
            } else {
                myEditor.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);
    }

}

