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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

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.SVNProperties;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.io.dav.handlers.DAVMergeHandler;
import org.tmatesoft.svn.core.internal.io.dav.handlers.DAVProppatchHandler;
import org.tmatesoft.svn.core.internal.io.dav.http.HTTPBodyInputStream;
import org.tmatesoft.svn.core.internal.io.dav.http.HTTPHeader;
import org.tmatesoft.svn.core.internal.io.dav.http.HTTPStatus;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.ISVNWorkspaceMediator;
import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
class DAVCommitEditor implements ISVNEditor {
    
    private DAVConnection myConnection;
    private SVNURL myLocation;
	private DAVRepository myRepository;
    private Runnable myCloseCallback;
    private String myActivity;

    private Stack myDirsStack;
    private ISVNWorkspaceMediator myCommitMediator;
    private Map myPathsMap;
    private Map myFilesMap;
    private String myBaseChecksum;
    private SVNProperties myRevProps;
    private String myActivityLocation;

    public DAVCommitEditor(DAVRepository repository, DAVConnection connection, String message, ISVNWorkspaceMediator mediator, Runnable closeCallback) {
        this(repository, connection, (SVNProperties) null, mediator, closeCallback);
        myRevProps = new SVNProperties();
        if (message != null) {
            myRevProps.put(SVNRevisionProperty.LOG, message);
        }
    }

    public DAVCommitEditor(DAVRepository repository, DAVConnection connection, SVNProperties revProps, ISVNWorkspaceMediator mediator, Runnable closeCallback) {
        myConnection = connection;
        myLocation = repository.getLocation();
        myRepository = repository;
        myCloseCallback = closeCallback;
        myCommitMediator = mediator;
        myDirsStack = new Stack();
        myPathsMap = new SVNHashMap();
        myFilesMap = new SVNHashMap();
        myRevProps = revProps != null ? revProps : new SVNProperties();
    }

    /* do nothing */
    public void targetRevision(long revision) throws SVNException {
    }
    public void absentDir(String path) throws SVNException {
    }
    public void absentFile(String path) throws SVNException {
    }

    public void openRoot(long revision) throws SVNException {
        // make activity
        final String[] activityUrls = createActivity();
        myActivity = activityUrls[0];
        myActivityLocation = activityUrls[1];
        DAVResource root = new DAVResource(myCommitMediator, myConnection, "", revision);
        root.fetchVersionURL(null, false);
        myDirsStack.push(root);
        myPathsMap.put(root.getURL(), root.getPath());
    }

    public void deleteEntry(String path, long revision) throws SVNException {
        path = SVNEncodingUtil.uriEncode(path);
        // get parent's working copy. (checkout? or use checked out?)
        DAVResource parentResource = (DAVResource) myDirsStack.peek();
        checkoutResource(parentResource, true);
        String wPath = parentResource.getWorkingURL();
		// get root wURL and delete from it!

        // append name part of the path to the checked out location
		// should we append full name here?
        String url;
		if (myDirsStack.size() == 1) {
			wPath = SVNPathUtil.append(parentResource.getWorkingURL(), path);
            url = SVNPathUtil.append(parentResource.getURL(), path);
		} else {
			// we are inside openDir()...
			wPath = SVNPathUtil.append(wPath, SVNPathUtil.tail(path));
            url = SVNPathUtil.append(parentResource.getURL(), SVNPathUtil.tail(path));
		}
        // call DELETE for the composed path
        myConnection.doDelete(url, wPath, revision);
		if (myDirsStack.size() == 1) {
			myPathsMap.put(SVNPathUtil.append(parentResource.getURL(), path), path);
		} else {
			myPathsMap.put(SVNPathUtil.append(parentResource.getURL(), SVNPathUtil.tail(path)), path);
		}
    }


    public void addDir(String path, String copyPath, long copyRevision) throws SVNException {
        path = SVNEncodingUtil.uriEncode(path);

        DAVResource parentResource = (DAVResource) myDirsStack.peek();
        checkoutResource(parentResource, true);
        String wPath = parentResource.getWorkingURL();

        DAVResource newDir = new DAVResource(myCommitMediator, myConnection, path, -1, copyPath != null);
        newDir.setWorkingURL(SVNPathUtil.append(wPath, SVNPathUtil.tail(path)));
        newDir.setAdded(true);

        myDirsStack.push(newDir);
        myPathsMap.put(newDir.getURL(), path);
        if (copyPath != null) {
            // convert to full path?
            copyPath = myRepository.doGetFullPath(copyPath);
            copyPath = SVNEncodingUtil.uriEncode(copyPath);
            DAVBaselineInfo info = DAVUtil.getBaselineInfo(myConnection, myRepository, copyPath, copyRevision, false, false, null);
            copyPath = SVNPathUtil.append(info.baselineBase, info.baselinePath);

            // full url.
            wPath = myLocation.setPath(newDir.getWorkingURL(), true).toString();
            myConnection.doCopy(copyPath, wPath, 1);
        } else {
            try {
                myConnection.doMakeCollection(newDir.getWorkingURL());
            } catch (SVNException e) {
                // check if dir already exists.
                if (!e.getErrorMessage().getErrorCode().isAuthentication() && 
                        e.getErrorMessage().getErrorCode() != SVNErrorCode.CANCELLED) {
                    SVNErrorMessage err = null;
                    try {
                        DAVBaselineInfo info = DAVUtil.getBaselineInfo(myConnection, myRepository, newDir.getURL(), -1, false, false, null);
                        if (info != null) {
                            err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_ALREADY_EXISTS, "Path ''{0}'' already exists", newDir.getURL());
                        }
                    } catch (SVNException inner) {
                    }
                    if (err != null) {
                        SVNErrorManager.error(err, SVNLogType.NETWORK);
                    }
                }
                throw e;
            }
        }
    }

    public void openDir(String path, long revision) throws SVNException {
        path = SVNEncodingUtil.uriEncode(path);
        // do nothing,
        DAVResource parent = myDirsStack.peek() != null ? (DAVResource) myDirsStack.peek() : null;
        DAVResource directory = new DAVResource(myCommitMediator, myConnection, path, revision, parent != null && parent.isCopy());
        if (parent != null && parent.getVersionURL() == null) {
            // part of copied structure -> derive wurl
            directory.setWorkingURL(SVNPathUtil.append(parent.getWorkingURL(), SVNPathUtil.tail(path)));
        } else {
            directory.fetchVersionURL(parent, false);
        }
        myDirsStack.push(directory);
        myPathsMap.put(directory.getURL(), directory.getPath());
    }

    public void changeDirProperty(String name, SVNPropertyValue value) throws SVNException {
        DAVResource directory = (DAVResource) myDirsStack.peek();
        checkoutResource(directory, true);
        directory.putProperty(name, value);
        myPathsMap.put(directory.getURL(), directory.getPath());
    }

    public void closeDir() throws SVNException {
        DAVResource resource = (DAVResource) myDirsStack.pop();
        // do proppatch if there were property changes.
        if (resource.getProperties() != null) {
            StringBuffer request = DAVProppatchHandler.generatePropertyRequest(null, resource.getProperties());
            myConnection.doProppatch(resource.getURL(), resource.getWorkingURL(), request, null, null);
        }
        resource.dispose();
    }

    public void addFile(String path, String copyPath, long copyRevision) throws SVNException {
        String originalPath = path;
        path = SVNEncodingUtil.uriEncode(path);
        // checkout parent collection.
        DAVResource parentResource = (DAVResource) myDirsStack.peek();
        checkoutResource(parentResource, true);
        String wPath = parentResource.getWorkingURL();
        // create child resource.
        DAVResource newFile = new DAVResource(myCommitMediator, myConnection, path, -1, copyPath != null);
        newFile.setWorkingURL(SVNPathUtil.append(wPath, SVNPathUtil.tail(path)));

        if (!parentResource.isAdded() && !myPathsMap.containsKey(newFile.getURL())) {
        	String filePath = SVNPathUtil.append(parentResource.getURL(), SVNPathUtil.tail(path));
            SVNErrorMessage err1 = null;
            SVNErrorMessage err2 = null;
            try {
                DAVUtil.getResourceProperties(myConnection, filePath, null, DAVElement.STARTING_PROPERTIES);
            } catch (SVNException e) {
                if (e.getErrorMessage() == null) {
                    throw e;
                }
                err1 = e.getErrorMessage();
            }
            try {
                DAVUtil.getResourceProperties(myConnection, newFile.getWorkingURL(), null, DAVElement.STARTING_PROPERTIES);
            } catch (SVNException e) {
                if (e.getErrorMessage() == null) {
                    throw e;
                }
                err2 = e.getErrorMessage();
            }
            if (err1 == null && err2 == null) {
                err1 = SVNErrorMessage.create(SVNErrorCode.RA_DAV_ALREADY_EXISTS, "File ''{0}'' already exists", filePath);
                SVNErrorManager.error(err1, SVNLogType.NETWORK);
            } else if ((err1 != null && err1.getErrorCode() == SVNErrorCode.FS_NOT_FOUND) ||
                    (err2 != null && err2.getErrorCode() == SVNErrorCode.FS_NOT_FOUND)) {
                // skip
            }  else {
                SVNErrorManager.error(err1, err2, SVNLogType.NETWORK);
            }
        }
        // put to have working URL to make PUT or PROPPATCH later (in closeFile())
        myPathsMap.put(newFile.getURL(), newFile.getPath());
        myFilesMap.put(originalPath, newFile);

        newFile.setAdded(true);
        if (copyPath != null) {
            copyPath = myRepository.doGetFullPath(copyPath);
            copyPath = SVNEncodingUtil.uriEncode(copyPath);
            DAVBaselineInfo info = DAVUtil.getBaselineInfo(myConnection, myRepository, copyPath, copyRevision, false, false, null);
            copyPath = SVNPathUtil.append(info.baselineBase, info.baselinePath);

            // do "COPY" copyPath to parents working url ?
            wPath = myLocation.setPath(newFile.getWorkingURL(), true).toString();
            myConnection.doCopy(copyPath, wPath, 0);
        } 
    }

    public void openFile(String path, long revision) throws SVNException {
        String originalPath = path;
        path = SVNEncodingUtil.uriEncode(path);
        DAVResource file = new DAVResource(myCommitMediator, myConnection, path, revision);
        DAVResource parent = (DAVResource) myDirsStack.peek();
        if (parent.getVersionURL() == null) {
            // part of copied structure -> derive wurl
            file.setWorkingURL(SVNPathUtil.append(parent.getWorkingURL(), SVNPathUtil.tail(path)));
        } else {
            file.fetchVersionURL(parent, false);
        }
        checkoutResource(file, true);
        myPathsMap.put(file.getURL(), file.getPath());
        myFilesMap.put(originalPath, file);
    }
    
    public void applyTextDelta(String path, String baseChecksum) throws SVNException {
        myCurrentDelta = null;
        myIsFirstWindow = true;
        myDeltaFile = null;
        myBaseChecksum = baseChecksum;
    }
    
    private OutputStream myCurrentDelta = null;
    private File myDeltaFile;
    private boolean myIsAborted;
    private boolean myIsFirstWindow;
    
    public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException {
        // save window, create temp file.
        try {
            if (myCurrentDelta == null) {
                myDeltaFile = SVNFileUtil.createTempFile("svnkit", ".tmp");
                myCurrentDelta = SVNFileUtil.openFileForWriting(myDeltaFile);
            }
            diffWindow.writeTo(myCurrentDelta, myIsFirstWindow);
            myIsFirstWindow = false;
            return SVNFileUtil.DUMMY_OUT;
        } catch (IOException e) {
            SVNFileUtil.closeFile(myCurrentDelta);
            SVNFileUtil.deleteFile(myDeltaFile);
            myDeltaFile = null;
            myCurrentDelta = null;
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
            SVNErrorManager.error(err, e, SVNLogType.NETWORK);
            return null;
        }
    }
    public void textDeltaEnd(String path) throws SVNException {
        SVNFileUtil.closeFile(myCurrentDelta);
    }

    public void changeFileProperty(String path, String name, SVNPropertyValue value)  throws SVNException {
        DAVResource currentFile = (DAVResource) myFilesMap.get(path);
        currentFile.putProperty(name, value);
    }

    public void closeFile(String path, String textChecksum) throws SVNException {
        // do PUT of delta if there was one (diff window + temp file).
        // do subsequent PUT of all diff windows...
        DAVResource currentFile = (DAVResource) myFilesMap.get(path);
        try {
            if (myDeltaFile != null) {
                InputStream combinedData = null;
                try {
                    combinedData = new HTTPBodyInputStream(myDeltaFile);
                    myConnection.doPutDiff(currentFile.getURL(), currentFile.getWorkingURL(), combinedData, myDeltaFile.length(),
                            myBaseChecksum, textChecksum);

                } catch (SVNException e) {
                    HTTPStatus httpStatus = myConnection.getLastStatus();
                    if (e.getErrorMessage().getErrorCode() == SVNErrorCode.RA_DAV_REQUEST_FAILED && httpStatus != null) {
                        switch (httpStatus.getCode()) {
                            case 423:
                                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.RA_NOT_LOCKED,
                                        "No lock on path ''{0}'' (Status {0} on PUT Request)",
                                        new Object[]{currentFile.getWorkingURL(), httpStatus.getCode()});
                                SVNErrorManager.error(errorMessage, e, SVNLogType.CLIENT);
                            default:
                                throw e;
                        }

                    } else {
                        throw e;
                    }
                } finally {
                    SVNFileUtil.closeFile(combinedData);
                    SVNFileUtil.deleteFile(myDeltaFile);
                    myDeltaFile = null;
                }
            }
            // do proppatch if there were property changes.
            if (currentFile.getProperties() != null) {
                StringBuffer request = DAVProppatchHandler.generatePropertyRequest(null, currentFile.getProperties());
                myConnection.doProppatch(currentFile.getURL(), currentFile.getWorkingURL(), request, null, null);
            }
        } finally {
            currentFile.dispose();
            myCurrentDelta = null;
            myBaseChecksum = null;
            myFilesMap.remove(path);
        }
    }
    
    public SVNCommitInfo closeEdit() throws SVNException {
	    try {
		    if (!myDirsStack.isEmpty()) {
		        DAVResource resource = (DAVResource) myDirsStack.pop();
		        // do proppatch if there were property changes.
		        if (resource.getProperties() != null) {
		            StringBuffer request = DAVProppatchHandler.generatePropertyRequest(null, resource.getProperties());
		            myConnection.doProppatch(resource.getURL(), resource.getWorkingURL(), request, null, null);
		        }
		        resource.dispose();
		    }
		    DAVMergeHandler handler = new DAVMergeHandler(myCommitMediator, myPathsMap);
            patchResourceProperties(myActivityLocation, myRevProps);

		    HTTPStatus status = myConnection.doMerge(myActivity, true, handler);
		    if (status.getError() != null) {
                // DELETE shouldn't be called anymore if there is an error or MERGE.
                // calling abortEdit will do nothing on closeEdit failure now.
                myIsAborted = true;
		        SVNErrorManager.error(status.getError(), SVNLogType.NETWORK);
		    }
		    return handler.getCommitInfo();
	    }
	    finally {
            // we should run abort edit if exception is thrown
            // abort edit will not be run if there was an error (from server side) on MERGE.
            try {
                abortEdit();
            } catch (SVNException e) {
                SVNDebugLog.getDefaultLog().logError(SVNLogType.DEFAULT, e);
            }

            // always run close callback to 'unlock' SVNRepository.
            runCloseCallback();            
	    }
    }
    
    public void abortEdit() throws SVNException {
        if (myIsAborted) {
            return;
        }
        myIsAborted = true;
	    try {
		    try {
			    // DELETE activity
			    if (myActivity != null) {
			        myConnection.doDelete(myActivity);
			    }
		    }
		    finally {
			    // dispose all resources!
			    if (myFilesMap != null) {
			        for (Iterator files = myFilesMap.values().iterator(); files.hasNext();) {
			            DAVResource file = (DAVResource) files.next();
			            file.dispose();
			        }
			        myFilesMap = null;
			    }
			    for(Iterator files = myDirsStack.iterator(); files.hasNext();) {
			        DAVResource resource = (DAVResource) files.next();
			        resource.dispose();
			    }
			    myDirsStack = null;
		    }
	    }
	    finally {
            runCloseCallback();
	    }
    }
    
    private void runCloseCallback() {
        if (myCloseCallback != null) {
            myCloseCallback.run();
            myCloseCallback = null;
        }
    }
    
    private String[] createActivity() throws SVNException {
        String activity = myConnection.doMakeActivity(myCommitMediator);
        // checkout head...
        String path = SVNEncodingUtil.uriEncode(myLocation.getPath());
        String vcc = DAVUtil.getPropertyValue(myConnection, path, null, DAVElement.VERSION_CONTROLLED_CONFIGURATION);
        
        HTTPStatus status = null;
        for (int i = 0; i < 5; i++) {
            String head = DAVUtil.getPropertyValue(myConnection, vcc, null, DAVElement.CHECKED_IN);
            try {
                status = myConnection.doCheckout(activity, null, head, false);
                break;
            } catch (SVNException svne) {
                if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.APMOD_BAD_BASELINE || i == 4) {
                    throw svne;
                }
            }
        }
        final String location = status.getHeader().getFirstHeaderValue(HTTPHeader.LOCATION_HEADER);
        if (location == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, "The CHECKOUT response did not contain a 'Location:' header");
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        if (myRevProps != null) {
            final SVNPropertyValue authorRevisionProperty = myRevProps.remove(SVNRevisionProperty.AUTHOR);
            patchResourceProperties(location, myRevProps);
            if (authorRevisionProperty != null) {
                myRevProps = new SVNProperties();
                myRevProps.put(SVNRevisionProperty.AUTHOR, authorRevisionProperty);
            } else {
                myRevProps = null;
            }
        }
        return new String[] {activity, location};
    }

    private void patchResourceProperties(String path, SVNProperties properties) throws SVNException {
        if (properties != null && properties.size() > 0) {
            final StringBuffer request = DAVProppatchHandler.generatePropertyRequest(null, properties);
            try {
                myConnection.doProppatch(null, path, request, null, null);
            } catch (SVNException e) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, "applying log message to {0}", path);
                SVNErrorManager.error(err, SVNLogType.NETWORK);
            }
        }
    }

    private void checkoutResource(DAVResource resource, boolean allow404) throws SVNException {
        if (resource.getWorkingURL() != null) {
            return;
        }
        HTTPStatus status = null;
        try {
            status = myConnection.doCheckout(myActivity, resource.getURL(), resource.getVersionURL(), allow404);
            if (allow404 && status.getCode() == 404) {
                resource.fetchVersionURL(null, true);
                status = myConnection.doCheckout(myActivity, resource.getURL(), resource.getVersionURL(), false);
            }
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() == SVNErrorCode.FS_CONFLICT) {
                String path = resource.getPath();
                if ("".equals(path)) {
                    path = resource.getURL();
                }
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CONFLICT, "File or directory ''{0}'' is out of date; try updating", path);
                SVNErrorManager.error(err, e.getErrorMessage(), SVNLogType.NETWORK);
            }
            throw e;
        }
        String location = status != null ? status.getHeader().getFirstHeaderValue(HTTPHeader.LOCATION_HEADER) : null;
        if (location == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, "The CHECKOUT response did not contain a 'Location:' header");
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        resource.setWorkingURL(location);
    }
    
}
