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

import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.List;
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.SVNPropertyValue;
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.diff.SVNDiffWindow;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNLogType;

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

    private SVNConnection myConnection;
    private SVNRepositoryImpl myRepository;
    private ISVNCommitCallback myCloseCallback;
    private Stack myDirsStack;
    private int myNextToken;
    private Map myFilesToTokens;

    public interface ISVNCommitCallback {
        public void run(SVNException error);
    }

    public SVNCommitEditor(SVNRepositoryImpl location, SVNConnection connection, ISVNCommitCallback closeCallback) {
        myRepository = location;
        myConnection = connection;
        myCloseCallback = closeCallback;
        myDirsStack = new Stack();
        myNextToken = 0;
    }

    /* 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 {
        DirBaton rootBaton = new DirBaton(myNextToken++);
        myConnection.write("(w((n)s))", new Object[]{"open-root",
                getRevisionObject(revision), rootBaton.getToken()});
        myDirsStack.push(rootBaton);
    }

    public void deleteEntry(String path, long revision) throws SVNException {
        DirBaton parentBaton = (DirBaton) myDirsStack.peek();
        myConnection.write("(w(s(n)s))", new Object[]{"delete-entry", path,
                getRevisionObject(revision), parentBaton.getToken()});
    }

    public void addDir(String path, String copyFromPath, long copyFromRevision)
            throws SVNException {
        DirBaton parentBaton = (DirBaton) myDirsStack.peek();
        DirBaton dirBaton = new DirBaton(myNextToken++);

        if (copyFromPath != null) {
            if (!SVNRevision.isValidRevisionNumber(copyFromRevision)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_SVN_MALFORMED_DATA, "Got source path but no source revision for ''{0}''", path);
                SVNErrorManager.error(err, SVNLogType.NETWORK);
            }
            String rootURL = myRepository.getRepositoryRoot(false).toString();
            copyFromPath = SVNPathUtil.append(rootURL, SVNEncodingUtil.uriEncode(myRepository.getRepositoryPath(copyFromPath)));
            myConnection.write("(w(sss(sn)))", new Object[]{"add-dir", path,
                    parentBaton.getToken(), dirBaton.getToken(), copyFromPath,
                    getRevisionObject(copyFromRevision)});
        } else {
            myConnection.write("(w(sss()))", new Object[]{"add-dir", path,
                    parentBaton.getToken(), dirBaton.getToken()});
        }
        myDirsStack.push(dirBaton);
    }

    public void openDir(String path, long revision) throws SVNException {
        DirBaton parentBaton = (DirBaton) myDirsStack.peek();
        DirBaton dirBaton = new DirBaton(myNextToken++);

        myConnection.write("(w(sss(n)))", new Object[]{"open-dir", path,
                parentBaton.getToken(), dirBaton.getToken(), getRevisionObject(revision)});

        myDirsStack.push(dirBaton);
    }

    public void changeDirProperty(String name, SVNPropertyValue value)
            throws SVNException {
        DirBaton dirBaton = (DirBaton) myDirsStack.peek();
        byte[] bytes = SVNPropertyValue.getPropertyAsBytes(value);
        myConnection.write("(w(ss(b)))", new Object[]{"change-dir-prop",
                dirBaton.getToken(), name, bytes});
    }

    public void closeDir() throws SVNException {
        DirBaton dirBaton = (DirBaton) myDirsStack.pop();

        myConnection.write("(w(s))",
                new Object[]{"close-dir", dirBaton.getToken()});
    }

    public void addFile(String path, String copyFromPath, long copyFromRevision) throws SVNException {
        DirBaton parentBaton = (DirBaton) myDirsStack.peek();
        String fileToken = "c" + myNextToken++;

        if (copyFromPath != null) {
            String host = myRepository.getRepositoryRoot(false).toString();
            copyFromPath = SVNPathUtil.append(host, SVNEncodingUtil.uriEncode(myRepository.getRepositoryPath(copyFromPath)));
            myConnection.write("(w(sss(sn)))", new Object[]{"add-file", path,
                    parentBaton.getToken(), fileToken, copyFromPath,
                    getRevisionObject(copyFromRevision)});
        } else {
            myConnection.write("(w(sss()))", new Object[]{"add-file", path,
                    parentBaton.getToken(), fileToken});
        }
        if (myFilesToTokens == null) {
            myFilesToTokens = new SVNHashMap();
        }
        myFilesToTokens.put(path, fileToken);
    }

    public void openFile(String path, long revision) throws SVNException {
        DirBaton parentBaton = (DirBaton) myDirsStack.peek();
        String fileToken = "c" + myNextToken++;

        myConnection.write("(w(sss(n)))", new Object[]{"open-file", path,
                parentBaton.getToken(), fileToken, getRevisionObject(revision)});
        if (myFilesToTokens == null) {
            myFilesToTokens = new SVNHashMap();
        }
        myFilesToTokens.put(path, fileToken);
    }

    public void applyTextDelta(String path, String baseChecksum) throws SVNException {
        String fileToken = (String) myFilesToTokens.get(path);
        myDiffWindowCount = 0;
        myConnection.write("(w(s(s)))", new Object[]{"apply-textdelta", fileToken, baseChecksum});
    }

    private int myDiffWindowCount = 0;
    private boolean myIsAborted;

    public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException {
        String fileToken = (String) myFilesToTokens.get(path);

        try {
            diffWindow.writeTo(myConnection.getDeltaStream(fileToken), myDiffWindowCount == 0, myConnection.getDeltaCompression());
            myDiffWindowCount++;
            return SVNFileUtil.DUMMY_OUT;
        } catch (IOException e) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.RA_SVN_IO_ERROR, e.getMessage()), e, SVNLogType.NETWORK);
        }
        return null;
    }

    public void textDeltaEnd(String path) throws SVNException {
        String fileToken = (String) myFilesToTokens.get(path);
        myDiffWindowCount = 0;
        myConnection.write("(w(s))", new Object[]{"textdelta-end", fileToken});
    }

    public void changeFileProperty(String path, String name, SVNPropertyValue value) throws SVNException {
        String fileToken = (String) myFilesToTokens.get(path);
        byte[] bytes = SVNPropertyValue.getPropertyAsBytes(value);
        myConnection.write("(w(ss(b)))", new Object[]{"change-file-prop", fileToken, name, bytes});
    }

    public void closeFile(String path, String textChecksum) throws SVNException {
        String fileToken = (String) myFilesToTokens.remove(path);
        myDiffWindowCount = 0;
        myConnection.write("(w(s(s)))", new Object[]{"close-file", fileToken, textChecksum});
    }

    public SVNCommitInfo closeEdit() throws SVNException {
        SVNException e = null;
        try {
            myConnection.write("(w())", new Object[]{"close-edit"});
            myConnection.read("", null, true);

            myRepository.authenticate();

            List items = myConnection.readTuple("r(?s)(?s)?(?s)", true);
            long revision = SVNReader.getLong(items, 0);
            Date date = SVNReader.getDate(items, 1);
            String author = SVNReader.getString(items, 2);
            String errorMessage = SVNReader.getString(items, 3);
            SVNErrorMessage err = errorMessage == null || errorMessage.length() == 0 ? null : SVNErrorMessage.create(SVNErrorCode.REPOS_POST_COMMIT_HOOK_FAILED, errorMessage, SVNErrorMessage.TYPE_WARNING);
            return new SVNCommitInfo(revision, author, date, err);
        } catch (SVNException exception) {
            e = exception;
            try {
                myConnection.write("(w())", new Object[]{"abort-edit"});
            } catch (SVNException e1) {
            }
            throw exception;
        } finally {
            myCloseCallback.run(e);
            myCloseCallback = null;
        }
    }

    public void abortEdit() throws SVNException {
        if (myIsAborted || myCloseCallback == null) {
            return;
        }
        myIsAborted = true;
        SVNException error = null;
        try {
            myConnection.write("(w())", new Object[]{"abort-edit"});
            myConnection.read("", null, true);
        } catch (SVNException e) {
            error = e;
            throw e;
        } finally {
            myCloseCallback.run(error);
            myCloseCallback = null;
        }
    }

    private static Long getRevisionObject(long rev) {
        return rev >= 0 ? rev : null;
    }

    private final static class DirBaton {

        private String myToken;

        public DirBaton(int token) {
            myToken = "d" + token;
        }

        public String getToken() {
            return myToken;
        }
    }
}
