/*
 * ====================================================================
 * 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.SVNCommitInfo;
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.internal.delta.SVNDeltaCombiner;
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.io.fs.FSRoot;
import org.tmatesoft.svn.core.internal.io.fs.FSTransactionRoot;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc2.ng.ISvnPropertiesDiffHandler;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNLocationEntry;
import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
import org.tmatesoft.svn.core.wc.ISVNDiffGenerator;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.admin.ISVNChangeEntryHandler;
import org.tmatesoft.svn.core.wc.admin.ISVNChangedDirectoriesHandler;
import org.tmatesoft.svn.core.wc.admin.ISVNGNUDiffGenerator;
import org.tmatesoft.svn.core.wc.admin.SVNChangeEntry;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNNodeEditor implements ISVNEditor {

    private static final char TYPE_REPLACED = 'R';
    
    private Node myCurrentNode;
    private Node myRootNode;
    private FSRoot myBaseRoot;
    private FSFS myFSFS;
    private Map myFiles;
    private ISVNEventHandler myCancelHandler;
    private File myTempDirectory;
    
    public SVNNodeEditor(FSFS fsfs, FSRoot baseRoot, ISVNEventHandler handler) {
        myBaseRoot = baseRoot;
        myFSFS = fsfs;
        myCancelHandler = handler;
        myFiles = new SVNHashMap();
    }

    public void abortEdit() throws SVNException {
    }

    public void absentDir(String path) throws SVNException {
    }

    public void absentFile(String path) throws SVNException {
    }

    public void addDir(String path, String copyFromPath, long copyFromRevision) throws SVNException {
        myCurrentNode = addOrOpen(path, SVNChangeEntry.TYPE_ADDED, SVNNodeKind.DIR, myCurrentNode, copyFromPath, copyFromRevision);
    }

    public void addFile(String path, String copyFromPath, long copyFromRevision) throws SVNException {
        Node node = addOrOpen(path, SVNChangeEntry.TYPE_ADDED, SVNNodeKind.FILE, myCurrentNode, copyFromPath, copyFromRevision);
        myFiles.put(path, node);
    }

    public void changeDirProperty(String name, SVNPropertyValue value) throws SVNException {
        myCurrentNode.myHasPropModifications = true;
    }

    public void changeFileProperty(String path, String name, SVNPropertyValue value) throws SVNException {
        Node fileNode = (Node) myFiles.get(path);
        fileNode.myHasPropModifications = true;
    }

    public void closeDir() throws SVNException {
        myCurrentNode = myCurrentNode.myParent;
    }

    public SVNCommitInfo closeEdit() throws SVNException {
        return null;
    }

    public void closeFile(String path, String textChecksum) throws SVNException {
        myFiles.remove(path);
    }

    public void deleteEntry(String path, long revision) throws SVNException {
        String name = SVNPathUtil.tail(path);
        Node node = null;

        if (myCurrentNode != null && myCurrentNode.myChildren != null) {
            for (Iterator children = myCurrentNode.myChildren.iterator(); children.hasNext();) {
                Node child = (Node) children.next();
                if (child.myName.equals(name)) {
                    node = child;
                    break;
                }
            }
        }

        if (node == null) {
            if (myCurrentNode != null) {
                node = new Node();
                node.myName = name;
                node.myParent = myCurrentNode;

                if (myCurrentNode.myChildren == null) {
                    myCurrentNode.myChildren = new LinkedList();
                }
                myCurrentNode.myChildren.add(node);
            }
        }

        node.myAction = SVNChangeEntry.TYPE_DELETED;
        SVNLocationEntry baseLocation = findRealBaseLocation(node);
        FSRoot baseRoot = null;
        if (!SVNRevision.isValidRevisionNumber(baseLocation.getRevision())) {
            baseRoot = myBaseRoot;
        } else {
            baseRoot = myFSFS.createRevisionRoot(baseLocation.getRevision()); 
        }
        
        SVNNodeKind kind = baseRoot.checkNodeKind(baseLocation.getPath());
        if (kind == SVNNodeKind.NONE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "''{0}'' not found in filesystem", path);
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        
        node.myKind = kind;
    }

    public void openDir(String path, long revision) throws SVNException {
        myCurrentNode = addOrOpen(path, TYPE_REPLACED, SVNNodeKind.DIR, myCurrentNode, null, -1);
    }

    public void openFile(String path, long revision) throws SVNException {
        Node node = addOrOpen(path, TYPE_REPLACED, SVNNodeKind.FILE, myCurrentNode, null, -1);
        myFiles.put(path, node);
    }

    public void openRoot(long revision) throws SVNException {
        myRootNode = myCurrentNode = new Node();
        myCurrentNode.myName = "";
        myCurrentNode.myParent = null;
        myCurrentNode.myKind = SVNNodeKind.DIR;
        myCurrentNode.myAction = TYPE_REPLACED;
    }

    public void targetRevision(long revision) throws SVNException {
    }

    public void applyTextDelta(String path, String baseChecksum) throws SVNException {
        Node fileNode = (Node) myFiles.get(path);
        fileNode.myHasTextModifications = true;
    }

    public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException {
        return null;
    }

    public void textDeltaEnd(String path) throws SVNException {
    }

    public void diff(FSRoot root, long baseRevision, ISVNGNUDiffGenerator generator, OutputStream os) throws SVNException {
        if (myRootNode != null) {
            FSRevisionRoot baseRoot = root.getOwner().createRevisionRoot(baseRevision);
            try {
                diffImpl(root, baseRoot, "/", "/", myRootNode, generator, os);
            } finally {
                cleanup();
            }
        }
    }
    
    public void traverseTree(boolean includeCopyInfo, ISVNChangeEntryHandler handler) throws SVNException {
        if (myRootNode != null) {
            traverseChangedTreeImpl(myRootNode, "/", includeCopyInfo, handler);
        }
    }
    
    public void traverseChangedDirs(ISVNChangedDirectoriesHandler handler) throws SVNException {
        if (myRootNode != null) {
            traverseChangedDirsImpl(myRootNode, "/", handler);
        }
    }

    private void diffImpl(FSRoot root, FSRevisionRoot baseRoot, String path, String basePath, Node node, ISVNGNUDiffGenerator generator, OutputStream os) throws SVNException {
        if (myCancelHandler != null) {
            myCancelHandler.checkCancelled();
        }
        
        DefaultSVNGNUDiffGenerator defaultGenerator = null;
        if (generator instanceof DefaultSVNGNUDiffGenerator) {
            defaultGenerator = (DefaultSVNGNUDiffGenerator) generator;
            defaultGenerator.setHeaderWritten(false);
            defaultGenerator.setDiffWritten(false);
        }
        
        boolean isCopy = false;
        boolean printedHeader = false;
        if (SVNRevision.isValidRevisionNumber(node.myCopyFromRevision) && node.myCopyFromPath != null) {
            basePath = node.myCopyFromPath;
            generator.displayHeader(ISVNGNUDiffGenerator.COPIED, path, basePath, node.myCopyFromRevision, os);
            baseRoot = myFSFS.createRevisionRoot(node.myCopyFromRevision);
            isCopy = true;
            printedHeader = true;
        }
        
        boolean doDiff = false;
        boolean isOriginalEmpty = false;
        DiffItem originalFile = null;
        DiffItem newFile = null;
        if (node.myKind == SVNNodeKind.FILE) {
            if (node.myAction == TYPE_REPLACED && node.myHasTextModifications) {
                doDiff = true;
                originalFile = prepareTmpFile(baseRoot, basePath, generator);
                newFile = prepareTmpFile(root, path, generator);
            } else if (generator.isDiffCopied() && node.myAction == SVNChangeEntry.TYPE_ADDED && isCopy) {
                if (node.myHasTextModifications) {
                    doDiff = true;
                    originalFile = prepareTmpFile(baseRoot, basePath, generator);
                    newFile = prepareTmpFile(root, path, generator);
                }
            } else if (generator.isDiffAdded() && node.myAction == SVNChangeEntry.TYPE_ADDED) {
                doDiff = true;
                isOriginalEmpty = true;
                originalFile = prepareTmpFile(null, basePath, generator);
                newFile = prepareTmpFile(root, path, generator);
            } else if (generator.isDiffDeleted() && node.myAction == SVNChangeEntry.TYPE_DELETED) {
                doDiff = true;
                originalFile = prepareTmpFile(null, basePath, generator);
                newFile = prepareTmpFile(null, path, generator);
            }
            
            if (!printedHeader && (node.myAction != TYPE_REPLACED || node.myHasTextModifications)) {
                if (node.myAction == SVNChangeEntry.TYPE_ADDED) {
                    generator.displayHeader(ISVNGNUDiffGenerator.ADDED, path, null, -1, os);
                } else if (node.myAction == SVNChangeEntry.TYPE_DELETED) {
                    generator.displayHeader(ISVNGNUDiffGenerator.DELETED, path, null, -1, os);
                } else if (node.myAction == TYPE_REPLACED) {
                    generator.displayHeader(ISVNGNUDiffGenerator.MODIFIED, path, null, -1, os);
                }
                printedHeader = true;
            }
        }

        SVNProperties baseProps = null;
        SVNProperties propsDiff = null;
        String displayPath = null;

        if (node.myHasPropModifications && node.myAction != SVNChangeEntry.TYPE_DELETED) {
            FSRevisionNode localNode = root.getRevisionNode(path);
            SVNProperties props = localNode.getProperties(root.getOwner());
            if (node.myAction != SVNChangeEntry.TYPE_ADDED) {
                FSRevisionNode baseNode = baseRoot.getRevisionNode(basePath);
                baseProps = baseNode.getProperties(baseRoot.getOwner());
            }
            propsDiff = FSRepositoryUtil.getPropsDiffs(baseProps, props);
            if (propsDiff.size() > 0) {
                displayPath = path.startsWith("/") ? path.substring(1) : path;
            }
        }
        
        if (doDiff) {
            if (defaultGenerator != null) {
                if (isOriginalEmpty) {
                    defaultGenerator.setOriginalFile(null, path);
                } else {
                    defaultGenerator.setOriginalFile(baseRoot, basePath);
                }
                defaultGenerator.setNewFile(root, path);
            }
            String rev1 = isOriginalEmpty ? "(rev 0)" : "(rev " + baseRoot.getRevision() + ")";
            String rev2 = null;
            if (root instanceof FSRevisionRoot) {
                FSRevisionRoot revRoot = (FSRevisionRoot) root;
                rev2 = "(rev " + revRoot.getRevision() + ")";
            } else {
                FSTransactionRoot txnRoot = (FSTransactionRoot) root;
                rev2 = "(txn " + txnRoot.getTxnID() + ")";
            }
            if (generator instanceof ISvnPropertiesDiffHandler) {
                ((ISvnPropertiesDiffHandler)generator).handlePropertiesDiff(baseProps, propsDiff);
            }
            generator.displayFileDiff(path, originalFile.myTmpFile, newFile.myTmpFile, rev1, rev2, originalFile.myMimeType, newFile.myMimeType, os);
            boolean hasDiff = defaultGenerator != null ? defaultGenerator.isDiffWritten() : true;
            if (!hasDiff && !node.myHasPropModifications &&
                    (node.myAction == SVNChangeEntry.TYPE_ADDED && generator.isDiffAdded()  ||
                     node.myAction == SVNChangeEntry.TYPE_DELETED && generator.isDiffDeleted())) {
                int kind = node.myAction == SVNChangeEntry.TYPE_ADDED ? ISVNGNUDiffGenerator.ADDED : ISVNGNUDiffGenerator.DELETED;
                defaultGenerator.setHeaderWritten(false);
                defaultGenerator.displayHeader(kind, path, null, -1, os);
                defaultGenerator.printHeader(os);
            }
        } else if (printedHeader) {
            generator.displayHeader(ISVNGNUDiffGenerator.NO_DIFF, path, null, -1, os);
        }

        if (propsDiff != null && displayPath != null) {
            generator.displayPropDiff(displayPath, baseProps, propsDiff, os);
        }

        if (node.myChildren == null || node.myChildren.size() == 0) {
            return;
        }
        
        for (Iterator children = node.myChildren.iterator(); children.hasNext();) {
            Node childNode = (Node) children.next();
            String childPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(path, childNode.myName));
            String childBasePath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(basePath, childNode.myName));
            diffImpl(root, baseRoot, childPath, childBasePath, childNode, generator, os);
        }
    }

    private DiffItem prepareTmpFile(FSRoot root, String path, ISVNDiffGenerator generator) throws SVNException {
        String mimeType = null; 
        if (root != null) {
            FSRevisionNode node = root.getRevisionNode(path);
            SVNProperties nodeProps = node.getProperties(root.getOwner());
            mimeType = nodeProps.getStringValue(SVNProperty.MIME_TYPE);
            if (SVNProperty.isBinaryMimeType(mimeType) && !generator.isForcedBinaryDiff()) {
                return new DiffItem(mimeType, null);
            }
        }
        
        File tmpFile = createTempFile(generator);
        if (root != null) {
            InputStream contents = null;
            OutputStream tmpOS = null;
            try {
                contents = root.getFileStreamForPath(new SVNDeltaCombiner(), path);
                tmpOS = SVNFileUtil.openFileForWriting(tmpFile);
                FSRepositoryUtil.copy(contents, tmpOS, myCancelHandler);
            } finally {
                SVNFileUtil.closeFile(contents);
                SVNFileUtil.closeFile(tmpOS);
            }
        }
        return new DiffItem(mimeType, tmpFile);
    }
    
    private File createTempFile(ISVNDiffGenerator generator) throws SVNException {
        File tmpFile = null;
        try {
            return File.createTempFile("diff.", ".tmp", getTempDirectory(generator));
        } catch (IOException e) {
            SVNFileUtil.deleteFile(tmpFile);
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        return null;
    }
    
    private File getTempDirectory(ISVNDiffGenerator generator) throws SVNException {
        if (myTempDirectory == null || !myTempDirectory.exists()) {
            myTempDirectory = generator.createTempDirectory();
        }
        return myTempDirectory;
    }

    private void cleanup() {
        if (myTempDirectory != null) {
            SVNFileUtil.deleteAll(myTempDirectory, true);
        }
    }

    private void traverseChangedDirsImpl(Node node, String path, ISVNChangedDirectoriesHandler handler) throws SVNException {
        if (myCancelHandler != null) {
            myCancelHandler.checkCancelled();
        }
        
        if (node == null || node.myKind != SVNNodeKind.DIR) {
            return;
        }

        boolean proceed = node.myHasPropModifications;
        if (!proceed && node.myChildren != null) {
            for (Iterator children = node.myChildren.iterator(); children.hasNext() && !proceed;) {
                Node child = (Node) children.next();
                if (child.myKind == SVNNodeKind.FILE || child.myHasTextModifications || child.myAction == SVNChangeEntry.TYPE_ADDED || child.myAction == SVNChangeEntry.TYPE_DELETED) {
                    proceed = true;
                }
            }
        }
        
        if (proceed && handler != null) {
            handler.handleDir(path);
        }
        
        if (node.myChildren == null || node.myChildren.size() == 0) {
            return;
        }
        
        for (Iterator children = node.myChildren.iterator(); children.hasNext();) {
            Node childNode = (Node) children.next();
            String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(path, childNode.myName));
            traverseChangedDirsImpl(childNode, fullPath, handler);
        }
    }
    
    private void traverseChangedTreeImpl(Node node, String path, boolean includeCopyInfo, ISVNChangeEntryHandler handler) throws SVNException {
        if (myCancelHandler != null) {
            myCancelHandler.checkCancelled();
        }
        
        if (node == null) {
            return;
        }
        
        SVNChangeEntry changeEntry = null;
        if (node.myAction == SVNChangeEntry.TYPE_ADDED) {
            String copyFromPath = includeCopyInfo ? node.myCopyFromPath : null;
            long copyFromRevision = includeCopyInfo ? node.myCopyFromRevision : -1;
            changeEntry = new SVNChangeEntry(path, node.myKind, node.myAction, copyFromPath, copyFromRevision, false, false);
        } else if (node.myAction == SVNChangeEntry.TYPE_DELETED) {
            changeEntry = new SVNChangeEntry(path, node.myKind, node.myAction, null, -1, false, false);
        } else if (node.myAction == TYPE_REPLACED) {
            if (node.myHasPropModifications || node.myHasTextModifications) {
                changeEntry = new SVNChangeEntry(path, node.myKind, SVNChangeEntry.TYPE_UPDATED, null, -1, node.myHasTextModifications, node.myHasPropModifications);
            }
        }
        
        if (changeEntry != null && handler != null) {
            handler.handleEntry(changeEntry);
        }
        
        if (node.myChildren == null || node.myChildren.size() == 0) {
            return;
        }
        
        for (Iterator children = node.myChildren.iterator(); children.hasNext();) {
            Node childNode = (Node) children.next();
            String fullPath = SVNPathUtil.getAbsolutePath(SVNPathUtil.append(path, childNode.myName));
            traverseChangedTreeImpl(childNode, fullPath, includeCopyInfo, handler);
        }
    }
    
    private SVNLocationEntry findRealBaseLocation(Node node) throws SVNException {
        if (node.myAction == SVNChangeEntry.TYPE_ADDED && node.myCopyFromPath != null && SVNRevision.isValidRevisionNumber(node.myCopyFromRevision)) {
            return new SVNLocationEntry(node.myCopyFromRevision, node.myCopyFromPath);
        }
        
        if (node.myParent != null) {
            SVNLocationEntry location = findRealBaseLocation(node.myParent);
            return new SVNLocationEntry(location.getRevision(), SVNPathUtil.getAbsolutePath(SVNPathUtil.append(location.getPath(), node.myName)));
        }

        return new SVNLocationEntry(-1, "/");
    }

    private Node addOrOpen(String path, char action, SVNNodeKind kind, Node parent, String copyFromPath, long copyFromRevision) {
        if (parent.myChildren == null) {
            parent.myChildren = new LinkedList();
        }

        Node node = new Node();
        node.myName = SVNPathUtil.tail(path);
        node.myAction = action;
        node.myKind = kind;
        node.myCopyFromPath = copyFromPath;
        node.myCopyFromRevision = copyFromRevision;
        node.myParent = parent;
        parent.myChildren.add(node);
        return node;
    }

    private class Node {

        SVNNodeKind myKind;
        char myAction;
        boolean myHasTextModifications;
        boolean myHasPropModifications;
        String myName;
        long myCopyFromRevision;
        String myCopyFromPath;
        Node myParent;
        LinkedList myChildren;
    }
    
    private class DiffItem {
        String myMimeType;
        File myTmpFile;
        
        public DiffItem(String mimeType, File tmpFile) {
            myMimeType = mimeType;
            myTmpFile = tmpFile; 
        }
    }
}
