package org.tmatesoft.svn.test;

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.*;

public class CommitBuilder {

    private final SVNURL url;
    private String commitMessage;
    private final Map<String, byte[]> filesToAdd;
    private final Map<String, byte[]> filesToChange;
    private final Map<String, SVNProperties> filesToProperties;
    private final Map<String, SVNProperties> directoriesToProperties;
    private final Map<String, String> filesToCopyFromPath;
    private final Map<String, Long> filesToCopyFromRevision;
    private final Map<String, String> directoriesToCopyFromPath;
    private final Map<String, Long> directoriesToCopyFromRevision;
    private final Set<String> directoriesToAdd;
    private final Set<String> entriesToDelete;
    private ISVNAuthenticationManager authenticationManager;

    public CommitBuilder(SVNURL url) {
        this.filesToAdd = new HashMap<String, byte[]>();
        this.filesToChange = new HashMap<String, byte[]>();
        this.filesToCopyFromPath = new HashMap<String, String>();
        this.filesToCopyFromRevision = new HashMap<String, Long>();
        this.directoriesToCopyFromPath = new HashMap<String, String>();
        this.directoriesToCopyFromRevision = new HashMap<String, Long>();
        this.directoriesToAdd = new HashSet<String>();
        this.entriesToDelete = new HashSet<String>();
        this.filesToProperties = new HashMap<String, SVNProperties>();
        this.directoriesToProperties = new HashMap<String, SVNProperties>();
        this.url = url;

        setCommitMessage("");
    }

    public void setFileProperty(String path, String propertyName, SVNPropertyValue propertyValue) {
        SVNProperties properties;
        if (!filesToProperties.containsKey(path)) {
            properties = new SVNProperties();
            filesToProperties.put(path, properties);
        } else {
            properties = filesToProperties.get(path);
        }

        properties.put(propertyName, propertyValue);
    }

    public void setDirectoryProperty(String path, String propertyName, SVNPropertyValue propertyValue) {
        SVNProperties properties;
        if (!directoriesToProperties.containsKey(path)) {
            properties = new SVNProperties();
            directoriesToProperties.put(path, properties);
        } else {
            properties = directoriesToProperties.get(path);
        }

        properties.put(propertyName, propertyValue);
    }

    public void setCommitMessage(String commitMessage) {
        this.commitMessage = commitMessage;
    }

    public void addDirectory(String directory) {
        directoriesToAdd.add(directory);
    }

    public CommitBuilder addFile(String path) {
        return addFile(path, new byte[0]);
    }

    public CommitBuilder addFile(String path, byte[] contents) {
        filesToAdd.put(path, contents);
        return this;
    }

    public CommitBuilder changeFile(String path, byte[] contents) {
        filesToChange.put(path, contents);
        return this;
    }

    public CommitBuilder addDirectoryByCopying(String path, String copyFromPath) {
        return addDirectoryByCopying(path, copyFromPath, -1); //the latest revision is used in this case
    }

    public void addFileByCopying(String path, String copyFromPath) {
        addFileByCopying(path, copyFromPath, -1);
    }

    public void addFileByCopying(String path, String copyFromPath, long copyFromRevision) {
        filesToCopyFromPath.put(path, copyFromPath);
        filesToCopyFromRevision.put(path, copyFromRevision);
    }

    public CommitBuilder addDirectoryByCopying(String path, String copyFromPath, long copyFromRevision) {
        directoriesToCopyFromPath.put(path, copyFromPath);
        directoriesToCopyFromRevision.put(path, copyFromRevision);
        return this;
    }

    public void replaceFileByCopying(String path, String copyFromPath) {
        delete(path);
        addFileByCopying(path, copyFromPath);
    }

    public void replaceDirectoryByCopying(String path, String copyFromPath) {
        delete(path);
        addDirectoryByCopying(path, copyFromPath);
    }

    public void replaceFileByCopying(String path, String copyFromPath, long copyFromRevision) {
        delete(path);
        addFileByCopying(path, copyFromPath, copyFromRevision);
    }

    public void replaceDirectoryByCopying(String path, String copyFromPath, long copyFromRevision) {
        delete(path);
        addDirectoryByCopying(path, copyFromPath, copyFromRevision);
    }

    public SVNCommitInfo commit() throws SVNException {
        final SortedSet<String> directoriesToVisit = getDirectoriesToVisit();
        final SVNRepository svnRepository = createSvnRepository();
        final long latestRevision = svnRepository.getLatestRevision();

        final ISVNEditor commitEditor = svnRepository.getCommitEditor(commitMessage, null);
        commitEditor.openRoot(-1);

        String currentDirectory = "";
        for (String directory : directoriesToVisit) {
            if (directory.length() == 0) {
                continue;
            }
            closeUntilCommonAncestor(commitEditor, currentDirectory, directory);
            openOrAddDir(commitEditor, directory, latestRevision);
            setDirProperties(commitEditor, directory);
            currentDirectory = directory;

            addChildrensFiles(commitEditor, directory, latestRevision);
            deleteEntries(commitEditor, directory);
        }
        closeUntilCommonAncestor(commitEditor, currentDirectory, "");
        currentDirectory = "";

        setDirProperties(commitEditor, "");
        addChildrensFiles(commitEditor, "", latestRevision);
        deleteEntries(commitEditor, "");

        commitEditor.closeDir();
        return commitEditor.closeEdit();
    }

    private void setDirProperties(ISVNEditor commitEditor, String directory) throws SVNException {
        SVNProperties properties = directoriesToProperties.get(directory);
        if (properties == null) {
            return;
        }
        for (String propertyName : properties.nameSet()) {
            final SVNPropertyValue propertyValue = properties.getSVNPropertyValue(propertyName);
            commitEditor.changeDirProperty(propertyName, propertyValue);
        }
    }

    private void setFileProperties(ISVNEditor commitEditor, String file) throws SVNException {
        SVNProperties properties = filesToProperties.get(file);
        if (properties == null) {
            return;
        }
        for (String propertyName : properties.nameSet()) {
            final SVNPropertyValue propertyValue = properties.getSVNPropertyValue(propertyName);
            commitEditor.changeFileProperty(file, propertyName, propertyValue);
        }
    }

    private void addChildrensFiles(ISVNEditor commitEditor, String directory, long latestRevision) throws SVNException {
        for (String file : filesToAdd.keySet()) {
            String parent = getParent(file);
            if (parent == null) {
                parent = "";
            }
            if (directory.equals(parent)) {
                maybeDelete(commitEditor, file);
                addFile(commitEditor, file, filesToAdd.get(file), latestRevision);
            }
        }

        for (String file : filesToCopyFromPath.keySet()) {
            String parent = getParent(file);
            if (parent == null) {
                parent = "";
            }
            if (directory.equals(parent)) {
                maybeDelete(commitEditor, file);
                addFileByCopying(commitEditor, file, filesToCopyFromPath.get(file), filesToCopyFromRevision.get(file), latestRevision);
            }
        }

        for (String file : filesToChange.keySet()) {
            String parent = getParent(file);
            if (parent == null) {
                parent = "";
            }
            if (directory.equals(parent)) {
                changeFile(commitEditor, file, filesToChange.get(file));
            }
        }

        for (String file : filesToProperties.keySet()) {
            String parent = getParent(file);
            if (parent == null) {
                parent = "";
            }
            if (directory.equals(parent)) {
                commitEditor.openFile(file, -1);
                setFileProperties(commitEditor, file);
                commitEditor.closeFile(file, null);
            }
        }
    }

    private void deleteEntries(ISVNEditor commitEditor, String directory) throws SVNException {
        for (String path : entriesToDelete) {
            String parent = getParent(path);
            if (parent == null) {
                parent = "";
            }
            if (directory.equals(parent)) {
                if (!filesToAdd.containsKey(path) && !filesToCopyFromPath.containsKey(path) && !directoriesToAdd.contains(path) && !directoriesToCopyFromPath.containsKey(path)) {
                    commitEditor.deleteEntry(path, -1);
                }
            }
        }

    }

    private void maybeDelete(ISVNEditor commitEditor, String file) throws SVNException {
        for (Iterator<String> iterator = entriesToDelete.iterator(); iterator.hasNext(); ) {
            String path = iterator.next();
            if (file.equals(path)) {
                commitEditor.deleteEntry(file, -1);
                iterator.remove();
                break;
            }
        }
    }

    private void addFileByCopying(ISVNEditor commitEditor, String file, String copySource, Long copyRevisionLong, long latestRevision) throws SVNException {
        final long copyRevisionSpecified = copyRevisionLong == null ? -1 : copyRevisionLong;
        final long copyRevision = copyRevisionSpecified == -1 ? latestRevision : copyRevisionSpecified;
        commitEditor.addFile(file, copySource, copyRevision);
        final byte[] originalContents = getOriginalContents(copySource, copyRevision);
        final String checksum = TestUtil.md5(originalContents);
        commitEditor.closeFile(file, checksum);
    }

    private void addFile(ISVNEditor commitEditor, String file, byte[] contents, long latestRevision) throws SVNException {
        final SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();

        commitEditor.addFile(file, null, -1);
        setFileProperties(commitEditor, file);
        commitEditor.applyTextDelta(file, null);
        final String checksum = deltaGenerator.sendDelta(file, new ByteArrayInputStream(contents), commitEditor, true);
        commitEditor.closeFile(file, checksum);
    }

    private void changeFile(ISVNEditor commitEditor, String file, byte[] newContents) throws SVNException {
        final SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
        final byte[] originalContents = getOriginalContents(file, -1);

        if (newContents == null) {
            newContents = originalContents;
        }

        commitEditor.openFile(file, -1);
        setFileProperties(commitEditor, file);
        commitEditor.applyTextDelta(file, TestUtil.md5(originalContents));
        final String checksum = deltaGenerator.sendDelta(file,
                new ByteArrayInputStream(originalContents), 0, new ByteArrayInputStream(newContents),
                commitEditor, true);
        commitEditor.closeFile(file, checksum);
    }

    private byte[] getOriginalContents(String file, long revision) throws SVNException {
        final SVNRepository svnRepository = createSvnRepository();
        try {
            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

            svnRepository.getFile(file, revision, null, byteArrayOutputStream);
            return byteArrayOutputStream.toByteArray();
        } finally {
            svnRepository.closeSession();
        }
    }

    private void closeUntilCommonAncestor(ISVNEditor commitEditor, String currentDirectory, String directory) throws SVNException {
        final String commonPathAncestor = getCommonPathAncestor(currentDirectory, directory);
        while (currentDirectory != null && !currentDirectory.equals(commonPathAncestor)) {
            commitEditor.closeDir();
            currentDirectory = getParent(currentDirectory);
        }
    }

    private String getCommonPathAncestor(String directory1, String directory2) {
        if (directory1 == null || directory1.length() == 0) {
            return "";
        }
        if (directory2 == null || directory2.length() == 0) {
            return "";
        }
        return SVNPathUtil.getCommonPathAncestor(directory1, directory2);
    }

    private void openOrAddDir(ISVNEditor commitEditor, String directory, long latestRevision) throws SVNException {
        boolean exists = existsDirectory(directory);

        //process replacement
        for (Iterator<String> iterator = entriesToDelete.iterator(); iterator.hasNext(); ) {
            final String path = iterator.next();
            if (directory.equals(path)) {
                commitEditor.deleteEntry(path, -1);
                iterator.remove();
                exists = false;
                break;
            }
        }

        if (exists) {
            if (!directoriesToAdd.contains(directory)) {
                commitEditor.openDir(directory, -1);
            } else {
                commitEditor.addDir(directory, null, -1);
            }
        } else {
            final String copySource = directoriesToCopyFromPath.get(directory);
            final Long copyRevisionLong = directoriesToCopyFromRevision.get(directory);
            final long copyRevisionSpecified = copyRevisionLong == null ? -1 : copyRevisionLong;
            final long copyRevision = copyRevisionSpecified == -1 ? latestRevision : copyRevisionSpecified;
            if (copySource == null) {
                commitEditor.addDir(directory, null, -1);
            } else {
                commitEditor.addDir(directory, copySource, copyRevision);
            }
        }
    }

    private boolean existsDirectory(String directory) throws SVNException {
        final SVNRepository svnRepository = createSvnRepository();
        try {
            final SVNNodeKind nodeKind = svnRepository.checkPath(directory, SVNRepository.INVALID_REVISION);
            return nodeKind == SVNNodeKind.DIR;
        } finally {
            svnRepository.closeSession();
        }
    }

    private SortedSet<String> getDirectoriesToVisit() {
        final SortedSet<String> directoriesToVisit = new TreeSet<String>(SVNPathUtil.PATH_COMPARATOR);
        for (String directory : directoriesToAdd) {
            addDirectoryToVisit(directory, directoriesToVisit);
        }
        for (String path: entriesToDelete) {
            String directory = getParent(path);
            if (directory != null) {
                addDirectoryToVisit(directory, directoriesToVisit);
            }
        }
        for (String directory : directoriesToProperties.keySet()) {
            addDirectoryToVisit(directory, directoriesToVisit);
        }
        for (String directoryToAdd : directoriesToAdd) {
            addDirectoryToVisit(directoryToAdd, directoriesToVisit);
        }

        addFilesParents(directoriesToVisit, filesToAdd.keySet());
        addFilesParents(directoriesToVisit, filesToChange.keySet());
        addFilesParents(directoriesToVisit, filesToProperties.keySet());
        addFilesParents(directoriesToVisit, filesToCopyFromPath.keySet());

        for (String directoryToCopy : directoriesToCopyFromPath.keySet()) {
            addDirectoryToVisit(directoryToCopy, directoriesToVisit);
        }

        return directoriesToVisit;
    }

    private void addFilesParents(SortedSet<String> directoriesToVisit, Set<String> files) {
        for (String file : files) {
            final String directory = getParent(file);
            if (directory != null) {
                addDirectoryToVisit(directory, directoriesToVisit);
            }
        }
    }

    private void addDirectoryToVisit(String directory, SortedSet<String> directoriesToVisit) {
        do {
            directoriesToVisit.add(directory);
            directory = getParent(directory);
        } while (directory != null);
    }

    private String getParent(String file) {
        if ("".equals(file)) {
            return null;
        }
        String parent = SVNPathUtil.removeTail(file);
        if ("".equals(parent)) {
            return null;
        }
        return parent;
    }

    private SVNRepository createSvnRepository() throws SVNException {
        final SVNRepository svnRepository = SVNRepositoryFactory.create(url);
        svnRepository.setAuthenticationManager(authenticationManager);
        return svnRepository;
    }

    public void delete(String path) {
        entriesToDelete.add(path);
    }

    public void setAuthenticationManager(ISVNAuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }
}
