package org.tmatesoft.svn.core.internal.wc2.ng;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

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.SVNMergeInfoInheritance;
import org.tmatesoft.svn.core.SVNMergeRangeList;
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.SVNURL;
import org.tmatesoft.svn.core.internal.wc.SVNExternalsUtil;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNURLUtil;
import org.tmatesoft.svn.core.internal.wc.*;
import org.tmatesoft.svn.core.internal.wc17.SVNCommitter17;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.SVNWCUtils;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb;
import org.tmatesoft.svn.core.internal.wc17.db.Structure;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.NodeOriginInfo;
import org.tmatesoft.svn.core.internal.wc2.SvnWcGeneration;
import org.tmatesoft.svn.core.internal.wc2.ng.SvnNgCommitUtil.ISvnUrlKindCallback;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNEventAction;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.*;
import org.tmatesoft.svn.core.wc2.hooks.ISvnCommitHandler;
import org.tmatesoft.svn.util.SVNLogType;

public class SvnNgWcToReposCopy extends SvnNgOperationRunner<SVNCommitInfo, SvnRemoteCopy> implements ISvnUrlKindCallback {

    @Override
    public boolean isApplicable(SvnRemoteCopy operation, SvnWcGeneration wcGeneration) throws SVNException {
        return areAllSourcesLocal(operation) && !operation.getFirstTarget().isLocal();
    }
    
    private boolean areAllSourcesLocal(SvnRemoteCopy operation) {
        // need all sources to be wc files at WORKING.
        // BASE revision meas repos_to_repos copy
        for(SvnCopySource source : operation.getSources()) {
            if (source.getSource().isFile() && 
                    (source.getRevision() == SVNRevision.WORKING || source.getRevision() == SVNRevision.UNDEFINED)) {
                continue;
            }
            return false;
        }
        return true;
    }
    
    @Override
    protected SVNCommitInfo run(SVNWCContext context) throws SVNException {
        SVNCommitInfo info = null;
        try {
            info = doRun(context, getOperation().getFirstTarget().getURL());
        } catch (SVNException e) {
            SVNErrorCode code = e.getErrorMessage().getErrorCode();
            if (!getOperation().isFailWhenDstExists()
                    && getOperation().getSources().size() == 1 
                    && (code == SVNErrorCode.ENTRY_EXISTS || code == SVNErrorCode.FS_ALREADY_EXISTS)) {
                
                SvnCopySource source = getOperation().getSources().iterator().next();
                SVNURL target = getOperation().getFirstTarget().getURL();
                target = target.appendPath(source.getSource().getFile().getName(), false);

                info = doRun(context, target);
            } else {
                throw e;
            }
        }
        if (info != null) {
            getOperation().receive(getOperation().getFirstTarget(), info);
        }
        return info;
    }
    
    protected SVNCommitInfo doRun(SVNWCContext context, SVNURL target) throws SVNException {
        if (getOperation().isMove()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                    "Moves between the working copy and the repository are not supported");
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        
        Collection<SvnCopySource> sources = getOperation().getSources();
        Collection<SvnCopyPair> copyPairs = new ArrayList<SvnNgWcToReposCopy.SvnCopyPair>();

        if (sources.size() > 1) {
            for (SvnCopySource copySource : sources) {
                SvnCopyPair copyPair = new SvnCopyPair();
                String baseName;
                copyPair.source = copySource.getSource().getFile();
                baseName = copyPair.source.getName();
                copyPair.dst = target;
                copyPair.dst = copyPair.dst.appendPath(baseName, false);
                copyPairs.add(copyPair);
            }
        } else if (sources.size() == 1) {
            SvnCopyPair copyPair = new SvnCopyPair();
            SvnCopySource source = sources.iterator().next(); 
            copyPair.source= source.getSource().getFile();
            copyPair.dst = target;            
            copyPairs.add(copyPair);
        }

        return copy(copyPairs, getOperation().isMakeParents(), getOperation().getRevisionProperties(), getOperation().getCommitMessage(), 
                getOperation().getCommitHandler());
    }

    private SVNCommitInfo copy(Collection<SvnCopyPair> copyPairs, boolean makeParents, SVNProperties revisionProperties, String commitMessage, ISvnCommitHandler commitHandler) throws SVNException {
        SvnCopyPair firstPair = copyPairs.iterator().next();
        SVNURL topDstUrl = firstPair.dst.removePathTail();
        for (SvnCopyPair pair : copyPairs) {
            topDstUrl = SVNURLUtil.getCommonURLAncestor(topDstUrl, pair.dst);
        }
        File topSrcPath = getCommonCopyAncestor(copyPairs);
        SVNRepository repository = getRepositoryAccess().createRepository(topDstUrl, topSrcPath);
        topDstUrl = repository.getLocation();
        
        Collection<SVNURL> parents = null;
        if (makeParents) {
            parents = findMissingParents(topDstUrl, repository);
        }
        for (SvnCopyPair pair : copyPairs) {
            String path = SVNURLUtil.getRelativeURL(repository.getLocation(), pair.dst, false);
            if (repository.checkPath(path, -1) != SVNNodeKind.NONE) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_ALREADY_EXISTS,
                        "Path ''{0}'' already exists", pair.dst);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }
        SvnCommitItem[] items = new SvnCommitItem[(parents != null ? parents.size() : 0) + copyPairs.size()];
        int index = 0;
        if (makeParents && parents != null) {
            for (SVNURL parent : parents) {
                SvnCommitItem parentItem = new SvnCommitItem();
                parentItem.setUrl(parent);
                parentItem.setFlags(SvnCommitItem.ADD);
                parentItem.setKind(SVNNodeKind.DIR);
                items[index++] = parentItem;
            }
        }
        for (SvnCopyPair svnCopyPair : copyPairs) {
            SvnCommitItem item = new SvnCommitItem();
            item.setUrl(svnCopyPair.dst);
            item.setPath(svnCopyPair.source);
            item.setFlags(SvnCommitItem.ADD);
            item.setKind(SVNNodeKind.DIR);
            items[index++] = item;
        }
        commitMessage = getOperation().getCommitHandler().getCommitMessage(commitMessage, items);
        if (commitMessage == null) {
            return SVNCommitInfo.NULL;
        }
        commitMessage = SVNCommitUtil.validateCommitMessage(commitMessage);

        revisionProperties = getOperation().getCommitHandler().getRevisionProperties(commitMessage, items, revisionProperties);
        if (revisionProperties == null) {
            return SVNCommitInfo.NULL;
        }
        SvnCommitPacket packet = new SvnCommitPacket();
        SVNURL repositoryRoot = repository.getRepositoryRoot(true);
        if (parents != null) {
            for (SVNURL parent : parents) {
                String parentPath = SVNURLUtil.getRelativeURL(repositoryRoot, parent, false);
                packet.addItem(null, SVNNodeKind.DIR, repositoryRoot, parentPath, -1, null, -1, null, SvnCommitItem.ADD);
            }
        }
        for (SvnCopyPair svnCopyPair : copyPairs) {
            Map<File, String> externals = getOperation().getExternalsHandler() != null ? new HashMap<File, String>() : null;
            SvnNgCommitUtil.harvestCopyCommitables(getWcContext(), svnCopyPair.source, svnCopyPair.dst, packet, this, getOperation().getCommitParameters(), externals);
            
            SvnCommitItem item = packet.getItem(svnCopyPair.source);
            if (item == null) {
                continue;
            }
            Map<String, SVNMergeRangeList> mergeInfo = calculateTargetMergeInfo(svnCopyPair.source, -1, repository);
            String mergeInfoProperty = getWcContext().getProperty(svnCopyPair.source, SVNProperty.MERGE_INFO);
            Map<String, SVNMergeRangeList> wcMergeInfo = 
                    mergeInfoProperty != null ? SVNMergeInfoUtil.parseMergeInfo(new StringBuffer(mergeInfoProperty), null) : null;
            if (wcMergeInfo != null && mergeInfo != null) {
                mergeInfo = SVNMergeInfoUtil.mergeMergeInfos(mergeInfo, wcMergeInfo);
            } else if (mergeInfo == null) {
                mergeInfo = wcMergeInfo;
            }
            String extendedMergeInfoValue = null;
            if (mergeInfo != null) {
                extendedMergeInfoValue = SVNMergeInfoUtil.formatMergeInfoToString(wcMergeInfo, null);
                item.addOutgoingProperty(SVNProperty.MERGE_INFO, SVNPropertyValue.create(extendedMergeInfoValue));
            }
            // append externals changes
            if (externals != null && !externals.isEmpty()) {
                includeExternalsChanges(repository, packet, externals, svnCopyPair);
            }
            if (getOperation().isPinExternals()) {
                final Map<String, SVNPropertyValue> pinnedExternals = SVNExternalsUtil.resolvePinnedExternals(getWcContext(), getRepositoryAccess(), getOperation().getExternalsToPin(), SvnTarget.fromFile(svnCopyPair.source), SvnTarget.fromURL(svnCopyPair.dst), -1, repository, repositoryRoot);
                for (Map.Entry<String, SVNPropertyValue> entry : pinnedExternals.entrySet()) {
                    final String dstRelPath = entry.getKey();
                    final SVNPropertyValue externalsPropertyValue = entry.getValue();

                    SVNURL dstUrl;

                    boolean dstIsUrl = true;//TODO: can we really copy path-to-path as wc to repos?
                    if (dstIsUrl) {
                        dstUrl = svnCopyPair.dst;
                    } else {
                        File dstAbsPath = null;//TODO
                        dstUrl = getWcContext().getNodeUrl(dstAbsPath);
                    }
                    final SVNURL commitUrl = dstUrl.appendPath(dstRelPath, false);
                    final File srcAbsPath = SVNFileUtil.createFilePath(svnCopyPair.source, dstRelPath);

                    queuePropChangeCommitItems(srcAbsPath, commitUrl, packet, SVNProperty.EXTERNALS, externalsPropertyValue, repositoryRoot);
                }
            }
        }

        if (getOperation().isDisableLocalModifications()) {
            SvnCommitPacket oldPacket = packet;
            packet = filterLocalModifications(packet);
            if (packet.isEmpty()) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, buildErrorMessageWithDebugInformation(oldPacket));
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }
        }

        Map<String, SvnCommitItem> committables = new TreeMap<String, SvnCommitItem>();
        SVNURL url = SvnNgCommitUtil.translateCommitables(packet.getItems(packet.getRepositoryRoots().iterator().next()), committables);
        repository.setLocation(url, false);
        ISVNEditor commitEditor = repository.getCommitEditor(commitMessage, null, false, revisionProperties, null);
        SVNCommitter17 committer = new SVNCommitter17(getWcContext(), committables, repositoryRoot, null, null, null);
        SVNCommitUtil.driveCommitEditor(committer, committables.keySet(), commitEditor, -1);
        committer.sendTextDeltas(commitEditor);
        SVNEvent event = SVNEventFactory.createSVNEvent(null, SVNNodeKind.UNKNOWN, null, SVNRepository.INVALID_REVISION, SVNEventAction.COMMIT_FINALIZING, SVNEventAction.COMMIT_FINALIZING, null, null);
        event.setURL(url);
        handleEvent(event);
        SVNCommitInfo info = commitEditor.closeEdit();
        deleteDeleteFiles(committer, getOperation().getCommitParameters());
        return info;
    }

    private void queuePropChangeCommitItems(File localAbsPath, SVNURL commitUrl, SvnCommitPacket packet, String propName, SVNPropertyValue externalsPropertyValue, SVNURL repositoryRoot) {
        SvnCommitItem item = null;

        Collection<SvnCommitItem> items = packet.getItems(repositoryRoot);

        for (SvnCommitItem existingItem : items) {
            if (existingItem.getUrl().equals(commitUrl)) {
                item = existingItem;
                break;
            }
        }
        if (item == null) {
            item = new SvnCommitItem();
            item.setPath(localAbsPath);
            item.setUrl(commitUrl);
            item.setKind(SVNNodeKind.DIR);
            item.setFlags(SvnCommitItem.PROPS_MODIFIED);
            packet.addItem(item, repositoryRoot);
        } else {
            item.setFlags(item.getFlags() | SvnCommitItem.PROPS_MODIFIED);
        }
        item.addOutgoingProperty(propName, externalsPropertyValue);
    }

    private String buildErrorMessageWithDebugInformation(SvnCommitPacket oldPacket) {
        StringBuilder stringBuilder = new StringBuilder("Unable to perform wc to remote copy without local modifications:").append('\n');
        stringBuilder.append("Commit packet was:").append('\n');

        final Collection<SVNURL> repositoryRoots = oldPacket.getRepositoryRoots();
        for (SVNURL oldRoot : repositoryRoots) {
            stringBuilder.append(oldRoot).append("  :").append('\n');
            Collection<SvnCommitItem> oldItems = oldPacket.getItems(oldRoot);
            if (oldItems != null) {
                for (SvnCommitItem oldItem : oldItems) {
                    stringBuilder.append("path=").append(oldItem.getPath()).append('\n');
                    stringBuilder.append("kind=").append(oldItem.getKind()).append('\n');
                    stringBuilder.append("url=").append(oldItem.getUrl()).append('\n');
                    stringBuilder.append("revision=").append(oldItem.getRevision()).append('\n');
                    stringBuilder.append("copyUrl=").append(oldItem.getCopyFromUrl()).append('\n');
                    stringBuilder.append("copyRevision=").append(oldItem.getCopyFromRevision()).append('\n');
                    stringBuilder.append("flags=").append(oldItem.getFlags()).append('\n');
                }
            }
        }

        return stringBuilder.toString();
    }

    private SvnCommitPacket filterLocalModifications(SvnCommitPacket packet) throws SVNException {
        final SvnCommitPacket filteredPacket = new SvnCommitPacket();
        filteredPacket.setLockTokens(packet.getLockTokens());
        filteredPacket.setLockingContext(packet.getRunner(), packet.getLockingContext());

        final Collection<SVNURL> repositoryRoots = packet.getRepositoryRoots();
        for (SVNURL repositoryRoot : repositoryRoots) {
            final Collection<SvnCommitItem> items = packet.getItems(repositoryRoot);
            for (SvnCommitItem item : items) {

                if (item.hasFlag(SvnCommitItem.DELETE)) {
                    continue;
                }

                if (item.hasFlag(SvnCommitItem.ADD)) {
                    if (!item.hasFlag(SvnCommitItem.COPY)) {
                        continue;
                    }

                    final SVNURL copyFromUrl = item.getCopyFromUrl();
                    if (copyFromUrl == null) {
                        continue;
                    }

                    final ISVNWCDb.WCDbBaseInfo baseInfo;
                    try {
                        baseInfo = getWcContext().getDb().getBaseInfo(item.getPath(), ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRootUrl, ISVNWCDb.WCDbBaseInfo.BaseInfoField.reposRelPath);
                    } catch (SVNException e) {
                        if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                            throw e;
                        } else {
                            continue;
                        }
                    }
                    final SVNURL url = baseInfo.reposRootUrl.appendPath(SVNFileUtil.getFilePath(baseInfo.reposRelPath), false);

                    if (!copyFromUrl.equals(url)) {
                        continue;
                    }
                }

                item.setFlags(item.getFlags() & (~SvnCommitItem.TEXT_MODIFIED) & (~SvnCommitItem.PROPS_MODIFIED));

                filteredPacket.addItem(item, repositoryRoot);
            }
        }

        return filteredPacket;
    }
    
    private void includeExternalsChanges(SVNRepository repos, SvnCommitPacket packet, Map<File, String> externalsStorage, SvnCopyPair svnCopyPair) throws SVNException {
        for (File externalHolder : externalsStorage.keySet()) {
            String externalsPropString = (String) externalsStorage.get(externalHolder);
            SVNExternal[] externals = SVNExternal.parseExternals(externalHolder.getAbsolutePath(), externalsPropString);
            boolean introduceVirtualExternalChange = false;
            List<String> newExternals = new ArrayList<String>();
            SVNURL ownerURL = getWcContext().getNodeUrl(externalHolder);
            if (ownerURL == null) {
                continue;
            }
            long ownerRev = getWcContext().getNodeBaseRev(externalHolder);

            File ownerReposRelPath = getWcContext().getNodeReposRelPath(externalHolder);
            File sourceReposRelPath = getWcContext().getNodeReposRelPath(svnCopyPair.source);
            String relativePath = SVNWCUtils.getPathAsChild(sourceReposRelPath, ownerReposRelPath);
            SVNURL targetURL = svnCopyPair.dst.appendPath(relativePath, false);
            
            for (int k = 0; k < externals.length; k++) {
                File externalWC = new File(externalHolder, externals[k].getPath());
                SVNRevision externalsWCRevision = SVNRevision.UNDEFINED;
                
                try {
                    long rev = getWcContext().getNodeBaseRev(externalWC);
                    if (rev >= 0) {
                        externalsWCRevision = SVNRevision.create(rev);
                    }
                } catch (SVNException e) {
                    // smthing went wrong.                    
                }
                
                SVNURL resolvedURL = externals[k].resolveURL(repos.getRepositoryRoot(true), ownerURL);
                String unresolvedURL = externals[k].getUnresolvedUrl();
                if (unresolvedURL != null && !SVNPathUtil.isURL(unresolvedURL) && unresolvedURL.startsWith("../"))  {
                    unresolvedURL = SVNURLUtil.getRelativeURL(repos.getRepositoryRoot(true), resolvedURL, true);
                    if (unresolvedURL.startsWith("/")) {
                        unresolvedURL = "^" + unresolvedURL;
                    } else {
                        unresolvedURL = "^/" + unresolvedURL;
                    }
                }

                SVNRevision[] revs = getOperation().getExternalsHandler().handleExternal(
                        externalWC,
                        resolvedURL,
                        externals[k].getRevision(),
                        externals[k].getPegRevision(),
                        externals[k].getRawValue(),
                        externalsWCRevision);

                if (revs != null && revs.length == 2 && !revs[0].equals(externals[k].getRevision())) {
                    SVNExternal newExternal = new SVNExternal(externals[k].getPath(),
                            unresolvedURL,
                            revs[1],
                            revs[0], 
                            true, 
                            externals[k].isPegRevisionExplicit(),
                            externals[k].isNewFormat());
                    newExternals.add(newExternal.toString());

                    if (!introduceVirtualExternalChange) {
                        introduceVirtualExternalChange = true;
                    }
                } else if (revs != null) {
                    newExternals.add(externals[k].getRawValue());
                }
            } 
            if (introduceVirtualExternalChange) {
                String newExternalsProp = "";
                for (String external : newExternals) {
                    newExternalsProp += external + '\n';
                }

                SvnCommitItem itemWithExternalsChanges = packet.getItem(externalHolder);
                if (itemWithExternalsChanges == null) {
                    itemWithExternalsChanges = packet.addItem(externalHolder, repos.getRepositoryRoot(true), SVNNodeKind.DIR, targetURL, -1, ownerURL, ownerRev, 
                            SvnCommitItem.PROPS_MODIFIED);
                } 
                itemWithExternalsChanges.addOutgoingProperty(SVNProperty.EXTERNALS, SVNPropertyValue.create(newExternalsProp));
            }
        }
    }
    
    private Collection<SVNURL> findMissingParents(SVNURL targetURL, SVNRepository repository) throws SVNException {
        SVNNodeKind kind = repository.checkPath("", -1);
        Collection<SVNURL> parents = new ArrayList<SVNURL>();
        while (kind == SVNNodeKind.NONE) {
            parents.add(targetURL);
            targetURL = targetURL.removePathTail();
            repository.setLocation(targetURL, false);
            kind = repository.checkPath("", -1);
        }
        if (kind != SVNNodeKind.DIR) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_ALREADY_EXISTS,
                    "Path ''{0}'' already exists, but it is not a directory", targetURL);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return parents;
    }

    private File getCommonCopyAncestor(Collection<SvnCopyPair> copyPairs) {
        File ancestor = null;
        for (SvnCopyPair svnCopyPair : copyPairs) {
            if (ancestor == null) {
                ancestor = svnCopyPair.source;
                continue;
            }
            String ancestorPath = ancestor.getAbsolutePath().replace(File.separatorChar, '/');
            String sourcePath = svnCopyPair.source.getAbsolutePath().replace(File.separatorChar, '/');
            ancestorPath = SVNPathUtil.getCommonPathAncestor(ancestorPath, sourcePath);
            ancestor = new File(ancestorPath);
        }
        return ancestor;
    }

    private Map<String, SVNMergeRangeList> calculateTargetMergeInfo(File srcFile, long srcRevision, SVNRepository repository) throws SVNException {
        SVNURL url = null;
        SVNURL oldLocation = null;
        
        Structure<NodeOriginInfo> nodeOrigin = getWcContext().getNodeOrigin(srcFile, false, NodeOriginInfo.revision, NodeOriginInfo.reposRelpath, NodeOriginInfo.reposRootUrl);
        if (nodeOrigin != null && nodeOrigin.get(NodeOriginInfo.reposRelpath) != null) {
            url = nodeOrigin.get(NodeOriginInfo.reposRootUrl);
            url = SVNWCUtils.join(url, nodeOrigin.<File>get(NodeOriginInfo.reposRelpath));
            srcRevision = nodeOrigin.lng(NodeOriginInfo.revision);
        }
        if (url != null) {
            Map<String, SVNMergeRangeList> targetMergeInfo = null;
            String mergeInfoPath;
            SVNRepository repos = repository;

            try {
                mergeInfoPath = getRepositoryAccess().getPathRelativeToSession(url, null, repos);
                if (mergeInfoPath == null) {
                    oldLocation = repos.getLocation();
                    repos.setLocation(url, false);
                    mergeInfoPath = "";
                }
                targetMergeInfo = getRepositoryAccess().getReposMergeInfo(repos, mergeInfoPath, srcRevision, SVNMergeInfoInheritance.INHERITED, true);
            } finally {
                if (repository == null) {
                    repos.closeSession();
                } else if (oldLocation != null) {
                    repos.setLocation(oldLocation, false);
                }
            }
            return targetMergeInfo;
        }
        return null;
    }

    private static class SvnCopyPair {
        File source;
        SVNURL dst;
    }

    public SVNNodeKind getUrlKind(SVNURL url, long revision) throws SVNException {
        return getRepositoryAccess().createRepository(url, null).checkPath("", revision);
    }
}
