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

import java.io.File;
import java.util.*;

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.util.SVNMergeInfoUtil;
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.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.SVNWCUtils;
import org.tmatesoft.svn.core.internal.wc17.db.Structure;
import org.tmatesoft.svn.core.internal.wc2.SvnRepositoryAccess;
import org.tmatesoft.svn.core.internal.wc2.SvnWcGeneration;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.ISvnObjectReceiver;
import org.tmatesoft.svn.core.wc2.SvnLog;
import org.tmatesoft.svn.core.wc2.SvnLogMergeInfo;
import org.tmatesoft.svn.core.wc2.SvnOperationFactory;
import org.tmatesoft.svn.core.wc2.SvnRevisionRange;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNLogType;

public class SvnNgLogMergeInfo extends SvnNgOperationRunner<SVNLogEntry, SvnLogMergeInfo> {

    @Override
    public boolean isApplicable(SvnLogMergeInfo operation, SvnWcGeneration wcGeneration) throws SVNException {
        boolean targetOk = operation.getFirstTarget().isURL() 
                || SvnOperationFactory.detectWcGeneration(operation.getFirstTarget().getFile(), true) == SvnWcGeneration.V17;
        boolean sourceOk = operation.getSource().isURL() 
                || SvnOperationFactory.detectWcGeneration(operation.getSource().getFile(), true) == SvnWcGeneration.V17;
        return targetOk && sourceOk;
    }

    @Override
    public SvnWcGeneration getWcGeneration() {
        return SvnWcGeneration.NOT_DETECTED;
    }

    @Override
    protected SVNLogEntry run(SVNWCContext context) throws SVNException {
        SVNURL[] root = new SVNURL[1];

        if (getOperation().getDepth() != SVNDepth.EMPTY && getOperation().getDepth() != SVNDepth.INFINITY) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Only depths 'infinity' and 'empty' are currently supported");
            SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
        }

        Collection<SvnRevisionRange> ranges = getOperation().getRanges();
        SVNRevision sourceStartRevision = (ranges == null || ranges.size() == 0) ? SVNRevision.UNDEFINED : ranges.iterator().next().getStart();
        SVNRevision sourceEndRevision = (ranges == null || ranges.size() == 0) ? SVNRevision.UNDEFINED : ranges.iterator().next().getEnd();

        if (sourceStartRevision.isLocal()) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION);
            SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
        }
        if (sourceEndRevision.isLocal()) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION);
            SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
        }
        if (sourceEndRevision != SVNRevision.UNDEFINED && sourceStartRevision == SVNRevision.UNDEFINED) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION);
            SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
        }
        if (sourceEndRevision == SVNRevision.UNDEFINED && sourceStartRevision != SVNRevision.UNDEFINED) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION);
            SVNErrorManager.error(errorMessage, SVNLogType.CLIENT);
        }


        Map<String, Map<String, SVNMergeRangeList>> targetMergeInfoCatalog = null;
        Map<String, Map<String, SVNMergeRangeList>> mergeInfoCatalog = null;

        SVNRepository sourceRepository = null;
        SVNRepository targetRepository = null;
        if (targetMergeInfoCatalog != null) {
            if (targetMergeInfoCatalog.size() == 0) {
                Structure<SvnRepositoryAccess.RepositoryInfo> repositoryInfo = getRepositoryAccess().createRepositoryFor(getOperation().getFirstTarget(), getOperation().getFirstTarget().getPegRevision(), getOperation().getFirstTarget().getPegRevision(), null);
                targetRepository = repositoryInfo.get(SvnRepositoryAccess.RepositoryInfo.repository);
                root[0] = targetRepository.getRepositoryRoot(true);
            } else {
                mergeInfoCatalog = SvnNgMergeinfoUtil.getMergeInfo(getWcContext(), getRepositoryAccess(), getOperation().getFirstTarget(), getOperation().getDepth() == SVNDepth.INFINITY, true, root);
                targetMergeInfoCatalog = mergeInfoCatalog;
            }
        } else {
            mergeInfoCatalog = SvnNgMergeinfoUtil.getMergeInfo(getWcContext(), getRepositoryAccess(), getOperation().getFirstTarget(), getOperation().getDepth() == SVNDepth.INFINITY, true, root);
        }

        File reposRelPath = null;
        SvnTarget target = getOperation().getFirstTarget();
        if (!target.isURL()) {
            reposRelPath = getWcContext().getNodeReposRelPath(getOperation().getFirstTarget().getFile());
        } else {
            reposRelPath = SVNFileUtil.createFilePath(SVNPathUtil.getRelativePath(root[0].getPath(), target.getURL().getPath()));
        }

        if (mergeInfoCatalog == null) {
            if (getOperation().isFindMerged()) {
                return getOperation().first();
            }
            mergeInfoCatalog = new TreeMap<String, Map<String,SVNMergeRangeList>>();
            mergeInfoCatalog.put(SVNFileUtil.getFilePath(reposRelPath), new TreeMap<String, SVNMergeRangeList>());
        }
        Map<String, SVNMergeRangeList> history = null;
        Map<String, SVNMergeRangeList> sourceHistory = null;
        if (!getOperation().isFindMerged()) {
            history = getRepositoryAccess().getHistoryAsMergeInfo(targetRepository, target, -1, -1);
        }
        Structure<SvnRepositoryAccess.RepositoryInfo> repositoryInfo = getRepositoryAccess().createRepositoryFor(getOperation().getSource(), getOperation().getSource().getPegRevision(), getOperation().getSource().getPegRevision(), null);
        sourceRepository = repositoryInfo.get(SvnRepositoryAccess.RepositoryInfo.repository);
        long pathRevision = repositoryInfo.lng(SvnRepositoryAccess.RepositoryInfo.revision);
        Structure<SvnRepositoryAccess.RevisionsPair> startRevisionPair = getRepositoryAccess().getRevisionNumber(sourceRepository, getOperation().getSource(), sourceStartRevision, null);
        long startRevision = startRevisionPair.lng(SvnRepositoryAccess.RevisionsPair.revNumber);
        Structure<SvnRepositoryAccess.RevisionsPair> endRevisionPair = getRepositoryAccess().getRevisionNumber(sourceRepository, getOperation().getSource(), sourceEndRevision, null);
        long endRevision = endRevisionPair.lng(SvnRepositoryAccess.RevisionsPair.revNumber);
        sourceHistory = getRepositoryAccess().getHistoryAsMergeInfo(null, getOperation().getSource(), Math.max(endRevision, startRevision), Math.min(endRevision, startRevision));

        boolean oldestRevsFirst = startRevision <= endRevision;

        String reposRelPathStr = SVNFileUtil.getFilePath(reposRelPath);
        SVNMergeRangeList masterNonInheritableRangeList = new SVNMergeRangeList((SVNMergeRange[]) null);
        SVNMergeRangeList masterInheritableRangeList = new SVNMergeRangeList((SVNMergeRange[]) null);
        Map<String, SVNMergeRangeList> inheritableSubtreeMerges = new TreeMap<String, SVNMergeRangeList>();
        
        for (String subtreePath : mergeInfoCatalog.keySet()) {
            Map<String, SVNMergeRangeList> subtreeMergeInfo = mergeInfoCatalog.get(subtreePath);
            
            Map<String, SVNMergeRangeList> subtreeHistory = null;
            Map<String, SVNMergeRangeList> subtreeSourceHistory;
            Map<String, SVNMergeRangeList> subtreeInheritableMergeInfo;
            Map<String, SVNMergeRangeList> subtreeNonInheritableMergeInfo;
            Map<String, SVNMergeRangeList> mergedNonInheritableMergeInfo;
            Map<String, SVNMergeRangeList> mergedMergeInfo;

            boolean isSubtree = !subtreePath.equals(reposRelPathStr);
            
            if (isSubtree) {
                String subtreeRelPath = subtreePath.substring(reposRelPathStr.length() + 1);
                subtreeSourceHistory = SVNMergeInfoUtil.appendSuffix(sourceHistory, subtreeRelPath);
                if (!getOperation().isFindMerged()) {
                    subtreeHistory = SVNMergeInfoUtil.appendSuffix(history, subtreeRelPath);
                }
            } else {
                subtreeSourceHistory = sourceHistory;
                if (!getOperation().isFindMerged()) {
                    subtreeHistory = history;
                }
            }
            if (!getOperation().isFindMerged()) {
                Map<String, SVNMergeRangeList> mergedViaHistory = SVNMergeInfoUtil.intersectMergeInfo(subtreeHistory, subtreeSourceHistory, true);
                subtreeMergeInfo = SVNMergeInfoUtil.mergeMergeInfos(subtreeMergeInfo, mergedViaHistory);
            }
            subtreeInheritableMergeInfo = SVNMergeInfoUtil.getInheritableMergeInfo(subtreeMergeInfo, null, -1, -1, true);
            subtreeNonInheritableMergeInfo = SVNMergeInfoUtil.getInheritableMergeInfo(subtreeMergeInfo, null, -1, -1, false);
            mergedNonInheritableMergeInfo = SVNMergeInfoUtil.intersectMergeInfo(subtreeNonInheritableMergeInfo, subtreeSourceHistory, false);
            
            if (!mergedNonInheritableMergeInfo.isEmpty()) {
                for (SVNMergeRangeList rl : mergedNonInheritableMergeInfo.values()) {
                    rl.setInheritable(false);
                    masterNonInheritableRangeList = masterNonInheritableRangeList.merge(rl.dup());
                }
            }
            mergedMergeInfo = SVNMergeInfoUtil.intersectMergeInfo(subtreeInheritableMergeInfo, subtreeSourceHistory, false);

            SVNMergeRangeList subtreeMergeRangeList = new SVNMergeRangeList((SVNMergeRange[]) null);
            if (!mergedMergeInfo.isEmpty()) {
                for (SVNMergeRangeList rl : mergedMergeInfo.values()) {
                    masterInheritableRangeList = masterInheritableRangeList.merge(rl.dup());
                    subtreeMergeRangeList = subtreeMergeRangeList.merge(rl.dup());
                }
            }
            inheritableSubtreeMerges.put(subtreePath, subtreeMergeRangeList);
        }
        
        if (!masterInheritableRangeList.isEmpty()) {
            for (String path : inheritableSubtreeMerges.keySet()) {
                SVNMergeRangeList subtreeMergedRangeList = inheritableSubtreeMerges.get(path);
                // present in master, but not in subtree.
                SVNMergeRangeList deletedRanges = masterInheritableRangeList.diff(subtreeMergedRangeList, true);
                if (!deletedRanges.isEmpty()) {
                    deletedRanges.setInheritable(false);
                    masterNonInheritableRangeList = masterNonInheritableRangeList.merge(deletedRanges);
                    masterInheritableRangeList = masterInheritableRangeList.remove(deletedRanges, false);
                }
            }
        }

        if (getOperation().isFindMerged()) {
            masterInheritableRangeList = masterInheritableRangeList.merge(masterNonInheritableRangeList);
        } else {
            SVNMergeRangeList sourceMasterRangeList = new SVNMergeRangeList((SVNMergeRange[]) null);
            for(SVNMergeRangeList rl : sourceHistory.values()) {
                sourceMasterRangeList = sourceMasterRangeList.merge(rl);
            }
            sourceMasterRangeList = sourceMasterRangeList.remove(masterNonInheritableRangeList, false);
            sourceMasterRangeList = sourceMasterRangeList.merge(masterNonInheritableRangeList);
            masterInheritableRangeList = sourceMasterRangeList.remove(masterInheritableRangeList, true);
        }
        
        if (masterInheritableRangeList.isEmpty()) {
            return getOperation().first();
        }
        
        List<String> mergeSourcePaths = new ArrayList<String>();
        String logTarget = null;
        SVNMergeRange youngestRange = masterInheritableRangeList.getRanges()[masterInheritableRangeList.getSize() - 1].dup();
        SVNMergeRangeList youngestRangeList = new SVNMergeRangeList(youngestRange.getEndRevision() - 1, youngestRange.getEndRevision(), youngestRange.isInheritable());
        for (String key : sourceHistory.keySet()) {
            SVNMergeRangeList subtreeMergedList = sourceHistory.get(key);
            SVNMergeRangeList intersection = youngestRangeList.intersect(subtreeMergedList, false);
            mergeSourcePaths.add(key);
            if (!intersection.isEmpty()) {
                logTarget = key;
            }
        }
        if (logTarget != null && logTarget.startsWith("/")) {
            logTarget = logTarget.substring(1);
        }
        
        SVNURL logTargetURL = logTarget != null ? SVNWCUtils.join(root[0], SVNFileUtil.createFilePath(logTarget)) : root[0];
        
        logForMergeInfoRangeList(logTargetURL, 
                mergeSourcePaths, 
                getOperation().isFindMerged(), 
                masterInheritableRangeList,
                oldestRevsFirst,
                mergeInfoCatalog, 
                "/" + reposRelPathStr, 
                getOperation().isDiscoverChangedPaths(), 
                getOperation().getRevisionProperties(), 
                getOperation());
        
        return getOperation().first();
    }
    
    @SuppressWarnings("unchecked")
    private void logForMergeInfoRangeList(SVNURL sourceURL, List<String> mergeSourcePaths, boolean filteringMerged, 
            SVNMergeRangeList rangelist, boolean oldestRevsFirst, Map<String, Map<String, SVNMergeRangeList>> targetCatalog, String absReposTargetPath, boolean discoverChangedPaths,
            String[] revprops, ISvnObjectReceiver<SVNLogEntry> receiver) throws SVNException {
        if (rangelist.isEmpty()) {
            return;
        }
        if (targetCatalog == null) {
            targetCatalog = new TreeMap<String, Map<String,SVNMergeRangeList>>();
        }
        Map<String, Map<String, SVNMergeRangeList>> adjustedCatalog = new TreeMap<String, Map<String,SVNMergeRangeList>>();
        for (String relativePath : targetCatalog.keySet()) {
            Map<String, SVNMergeRangeList> mi = targetCatalog.get(relativePath);
            if (!relativePath.startsWith("/")) {
                relativePath = "/" + relativePath;
            }
            adjustedCatalog.put(relativePath, mi);
        }
        List<SVNMergeRange> ranges = rangelist.getRangesAsList();
        Collections.sort(ranges);
        SVNMergeRange youngestRange = ranges.get(ranges.size() - 1);
        SVNMergeRange oldestRange = ranges.get(0);
        
        long youngestRev = youngestRange.getEndRevision();
        long oldestRev = oldestRange.getStartRevision();
        
        LogEntryReceiver filteringReceiver = new LogEntryReceiver();
        filteringReceiver.receiver = receiver;
        filteringReceiver.rangelist = rangelist;
        filteringReceiver.isFilteringMerged = filteringMerged;
        filteringReceiver.targetCatalog = adjustedCatalog;
        filteringReceiver.mergeSourcePaths = mergeSourcePaths;
        filteringReceiver.reposTargertAbsPath = absReposTargetPath;
        
        SvnLog log = getOperation().getOperationFactory().createLog();
        
        log.setSingleTarget(SvnTarget.fromURL(sourceURL, SVNRevision.create(youngestRev)));
        log.setDiscoverChangedPaths(true /*getOperation().isDiscoverChangedPaths()*/);
        log.setRevisionProperties(getOperation().getRevisionProperties());
        log.setLimit(-1);
        log.setStopOnCopy(false);
        log.setUseMergeHistory(false);
        log.addRange(oldestRevsFirst ? SvnRevisionRange.create(SVNRevision.create(oldestRev), SVNRevision.create(youngestRev)) : SvnRevisionRange.create(SVNRevision.create(youngestRev), SVNRevision.create(oldestRev)));
        log.setReceiver(filteringReceiver);
        
        log.run();
    }

    private static class LogEntryReceiver implements ISvnObjectReceiver<SVNLogEntry> {

        private boolean isFilteringMerged;
        private List<String> mergeSourcePaths;
        private String reposTargertAbsPath;
        private Map<String, Map<String, SVNMergeRangeList>> targetCatalog;
        private SVNMergeRangeList rangelist;
        private ISvnObjectReceiver<SVNLogEntry> receiver;
        
        public LogEntryReceiver() {
        }

        public void receive(SvnTarget target, SVNLogEntry logEntry) throws SVNException {
            if (logEntry.getRevision() == 0) {
                return;
            }
            SVNMergeRangeList thisRangeList = new SVNMergeRangeList(logEntry.getRevision() - 1, logEntry.getRevision(), true);
            SVNMergeRangeList intersection = this.rangelist.intersect(thisRangeList, false);
            
            if (intersection == null || intersection.isEmpty()) {
                return;
            }
            intersection = thisRangeList.intersect(rangelist, true);
            logEntry.setNonInheriable(intersection.isEmpty());
            
            if ((logEntry.isNonInheritable() || !isFilteringMerged) && logEntry.getChangedPaths() != null) {
                boolean allSubtreesHaveThisRev = true;
                SVNMergeRangeList thisRevRangeList = new SVNMergeRangeList(logEntry.getRevision() - 1, logEntry.getRevision(), true);
                for (String changedPath : logEntry.getChangedPaths().keySet()) {
                    String mergeSourceRelTarget = null;
                    boolean interrupted = false;
                    String mSourcePath = null;
                    for (String mergeSourcePath : this.mergeSourcePaths) {
                        mSourcePath = mergeSourcePath;
                        mergeSourceRelTarget = mergeSourcePath.equals(changedPath) ? "" : SVNPathUtil.getPathAsChild(mergeSourcePath, changedPath);
                        if (mergeSourceRelTarget != null) {
                            interrupted = true;
                            if ("".equals(mergeSourceRelTarget) && logEntry.getChangedPaths().get(changedPath).getType() != 'M') {                                
                                interrupted = false;                                
                            }
                            break;
                        }
                    }
                    if (!interrupted) {
                        continue;
                    }
                    String targetPathAffected = SVNPathUtil.append(reposTargertAbsPath, mergeSourceRelTarget);
                    if (!targetPathAffected.startsWith("/")) {
                        targetPathAffected = "/" + targetPathAffected;
                    }
                    Map<String, SVNMergeRangeList> nearestAncestorMergeInfo = null;
                    boolean ancestorIsSelf = false;
                    for(String path : targetCatalog.keySet()) {
                        if (SVNPathUtil.isAncestor(path, targetPathAffected)) {
                            nearestAncestorMergeInfo = targetCatalog.get(path);
                            ancestorIsSelf = path.equals(targetPathAffected);
                            if (ancestorIsSelf) {
                                break;
                            }
                        }
                    }
                    
                    if (nearestAncestorMergeInfo != null && ancestorIsSelf && logEntry.getChangedPaths().get(changedPath).getType() != 'M') {
                        //
                        SVNMergeRangeList rlist = nearestAncestorMergeInfo.get(changedPath);
                        if (rlist != null) {
                            SVNMergeRange youngestRange = rlist.getRanges()[rlist.getSize() - 1];
                            if (youngestRange.getEndRevision() > logEntry.getRevision()) {
                                continue;
                            }
                        }
                    }
                    boolean foundThisRevision = false;
                    if (nearestAncestorMergeInfo != null) {
                        for(String path : nearestAncestorMergeInfo.keySet()) {
                            SVNMergeRangeList rlist = nearestAncestorMergeInfo.get(path);
                            if (SVNPathUtil.isAncestor(mSourcePath, path)) {
                                SVNMergeRangeList inter = rlist.intersect(thisRevRangeList, false);
                                if (!inter.isEmpty()) {
                                    if (ancestorIsSelf) {
                                        foundThisRevision = true;
                                        break;
                                    } else {
                                        inter = rlist.intersect(thisRevRangeList, true);
                                        if (!inter.isEmpty()) {
                                            foundThisRevision = true;
                                            break;
                                        }
                                    }
                                }
                                
                            }
                        }
                    }
                    if (!foundThisRevision) {
                        allSubtreesHaveThisRev = false;
                        break;
                    }
                }
                
                if (allSubtreesHaveThisRev) {
                    if (isFilteringMerged) {
                        logEntry.setNonInheriable(false);
                    } else {
                        return;
                    }
                }
            }
            
            receiver.receive(target, logEntry);
            
        }
    }

}
