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

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.*;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
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.internal.wc2.compat.SvnCodec;
import org.tmatesoft.svn.core.io.*;
import org.tmatesoft.svn.core.wc.*;
import org.tmatesoft.svn.core.wc2.ISvnObjectReceiver;
import org.tmatesoft.svn.core.wc2.SvnDiffStatus;
import org.tmatesoft.svn.core.wc2.SvnDiffSummarize;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.ISVNDebugLog;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

import java.io.File;

public class SvnNgDiffSummarize extends SvnNgOperationRunner<SvnDiffStatus, SvnDiffSummarize> {

    private SvnNgRepositoryAccess repositoryAccess;

    @Override
    public boolean isApplicable(SvnDiffSummarize operation, SvnWcGeneration wcGeneration) throws SVNException {
        if (operation.getSource() != null) {
            if (operation.getSource().isFile() && wcGeneration != SvnWcGeneration.V17) {
                return false;
            }

            return true;
        } else {
            if (operation.getFirstSource().isFile() && wcGeneration != SvnWcGeneration.V17) {
                return false;
            }

            if (operation.getSecondSource().isFile() && wcGeneration != SvnWcGeneration.V17) {
                return false;
            }

            return true;
        }
    }

    @Override
    protected SvnDiffStatus run(SVNWCContext context) throws SVNException {
        final SvnTarget source = getOperation().getSource();
        final SvnTarget firstSource = getOperation().getFirstSource();
        final SvnTarget secondSource = getOperation().getSecondSource();

        final ISVNDiffStatusHandler handler = createHandlerForReceiver(getOperation());
        final SVNDepth depth = getOperation().getDepth();
        final boolean useAncestry = !getOperation().isIgnoreAncestry();

        if (source != null) {
            doDiff(source, getOperation().getStartRevision(), source, getOperation().getEndRevision(), source.getPegRevision(), depth, useAncestry, getOperation().isRecurseIntoDeletedDirectories(), handler);
        } else {
            doDiff(firstSource, firstSource.getResolvedPegRevision(), secondSource, secondSource.getResolvedPegRevision(), SVNRevision.UNDEFINED, depth, useAncestry, getOperation().isRecurseIntoDeletedDirectories(), handler);
        }
        return null;
    }


    private void doDiff(SvnTarget target1, SVNRevision revision1, SvnTarget target2, SVNRevision revision2, SVNRevision pegRevision, SVNDepth depth, boolean useAncestry, boolean recurseIntoDeletedDirectories, ISVNDiffStatusHandler handler) throws SVNException {
        if ((revision1 == SVNRevision.UNDEFINED) || (revision2 == SVNRevision.UNDEFINED)) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Not all required revisions are specified");
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }

        boolean isLocalRev1 = (revision1 == SVNRevision.BASE) || (revision1 == SVNRevision.WORKING);
        boolean isLocalRev2 = (revision2 == SVNRevision.BASE) || (revision2 == SVNRevision.WORKING);

        if (pegRevision != SVNRevision.UNDEFINED && isLocalRev1 && isLocalRev2) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "At least one revision must be something other than BASE or WORKING when diffing a URL");
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }

        boolean isRepos1 = !isLocalRev1 || target1.isURL();
        boolean isRepos2 = !isLocalRev2 || target2.isURL();

        if (isRepos1) {
            if (isRepos2) {
                doDiffReposRepos(target1.getURL(), target1.getFile(), revision1,
                        target2.getURL(), target2.getFile(), revision2,
                        pegRevision, depth, useAncestry, recurseIntoDeletedDirectories, handler);
            } else {
                doDiffReposWC(target1, revision1, target2, revision2, pegRevision, false, depth, useAncestry, recurseIntoDeletedDirectories, handler);
            }
        } else {
            if (isRepos2) {
                doDiffReposWC(target2, revision2, target1, revision1, pegRevision, true, depth, useAncestry, recurseIntoDeletedDirectories, handler);
            } else {
                if (revision1 == SVNRevision.WORKING && revision2 == SVNRevision.WORKING) {
                    File path1 = target1.getFile();
                    File path2 = target2.getFile();

                    SVNNodeKind kind = SVNFileType.getNodeKind(SVNFileType.getType(path1));
                    String target;
                    SvnTarget baseTarget;
                    if (kind == SVNNodeKind.DIR) {
                        target = "";
                        baseTarget = target1;
                    } else {
                        target = SVNFileUtil.getFileName(path1);
                        baseTarget = SvnTarget.fromFile(SVNFileUtil.getParentFile(target1.getFile()), target1.getPegRevision());
                    }
                    SvnNgDiffUtil.doArbitraryNodesDiff(target1, target2, depth, getWcContext(), new SvnDiffSummarizeCallback(kind == SVNNodeKind.DIR ? path1 : SVNFileUtil.getParentFile(path1), false, getRepositoryAccess().getTargetURL(baseTarget), baseTarget.getFile(), handler), getOperation().getEventHandler());
                } else {
                    doDiffWCWC(target1, revision1, target2, revision2, depth, useAncestry, handler);
                }
            }
        }
    }

    private void doDiffURL(SVNURL url, File path, SVNRevision startRevision, SVNRevision endRevision, SVNRevision pegRevision,
                           SVNDepth depth, boolean useAncestry, boolean recurseIntoDeletedDirectories, ISVNDiffStatusHandler handler) throws SVNException {
        if (handler == null) {
            return;
        }
        doDiffReposRepos(url, path, startRevision,
                url, path, endRevision,
                pegRevision, depth, useAncestry, recurseIntoDeletedDirectories, handler);
    }

    private void doDiffReposRepos(SVNURL url1, File path1, SVNRevision revision1,
                                  SVNURL url2, File path2, SVNRevision revision2,
                                  SVNRevision pegRevision, SVNDepth depth, boolean useAncestry,
                                  boolean recurseIntoDeletedDirectories,
                                  ISVNDiffStatusHandler handler) throws SVNException {

        if (revision1 == SVNRevision.UNDEFINED || revision2 == SVNRevision.UNDEFINED) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Not all revisions are specified");
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        boolean isLocalRev1 = revision1 == SVNRevision.BASE || revision1 == SVNRevision.WORKING;
        boolean isLocalRev2 = revision2 == SVNRevision.BASE || revision2 == SVNRevision.WORKING;
        boolean isRepos1;
        boolean isRepos2;

        if (pegRevision != SVNRevision.UNDEFINED) {
            if (isLocalRev1 && isLocalRev2) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "At least one revision must be non-local for a pegged diff");
                SVNErrorManager.error(err, SVNLogType.WC);
            }

            isRepos1 = !isLocalRev1;
            isRepos2 = !isLocalRev2;
        } else {
            isRepos1 = !isLocalRev1 || url1 != null;
            isRepos2 = !isLocalRev2 || url2 != null;
        }

        if (!isRepos1 || !isRepos2) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE, "Summarizing diff can only compare repository to repository");
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        File basePath = null;
        if (path1 != null) {
            basePath = path1;
        }
        if (path2 != null) {
            basePath = path2;
        }
        if (pegRevision.isValid()) {
            url2 = resolvePeggedDiffTargetUrl(url2, path2, pegRevision, revision2);
            url1 = resolvePeggedDiffTargetUrl(url1, path1, pegRevision, revision1);

            if (url2 != null && url1 == null) {
                url1 = url2;
            }
            if (url1 != null && url2 == null) {
                url2 = url1;
            }

        } else {
            url1 = url1 == null ? getURL(path1) : url1;
            url2 = url2 == null ? getURL(path2) : url2;
        }
        SVNRepository repository1 = createRepository(url1, null, true);
        SVNRepository repository2 = createRepository(url2, null, false);
        long rev1 = getRevisionNumber(revision1, repository1, url1);
        long rev2 = -1;
        SVNNodeKind kind1 = null;
        SVNNodeKind kind2 = null;

        SVNURL anchor1 = url1;
        SVNURL anchor2 = url2;
        String target1 = "";
        String target2 = "";

        try {
            rev2 = getRevisionNumber(revision2, repository2, url2);
            kind1 = repository1.checkPath("", rev1);
            kind2 = repository2.checkPath("", rev2);

            if (kind1 == SVNNodeKind.NONE && kind2 == SVNNodeKind.NONE) {
                if (url1.equals(url2)) {
                    SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "Diff target ''{0}'' was not found in the repository at revisions ''{1}'' and ''{2}''", url1, rev1, rev2);
                    SVNErrorManager.error(errorMessage, SVNLogType.WC);
                } else {
                    SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "Diff targets ''{0}'' and ''{1}'' were not found in the repository at revisions ''{2}'' and ''{3}''");
                    SVNErrorManager.error(errorMessage, SVNLogType.WC);
                }
            } else if (kind1 == SVNNodeKind.NONE) {
                checkDiffTargetExists(url1, rev2, rev1, repository1);
            } else if (kind2 == SVNNodeKind.NONE) {
                checkDiffTargetExists(url2, rev1, rev2, repository2);
            }

            SVNURL repositoryRoot = repository1.getRepositoryRoot(true);
            if (!url1.equals(repositoryRoot) && !url2.equals(repositoryRoot)) {
                anchor1 = url1.removePathTail();
                anchor2 = url2.removePathTail();

                target1 = SVNPathUtil.tail(url1.toDecodedString());
                target2 = SVNPathUtil.tail(url2.toDecodedString());
            }
        } finally {
            repository2.closeSession();
        }

        boolean nonDir = kind1 != SVNNodeKind.DIR || kind2 != SVNNodeKind.DIR;

        if (basePath == null) {
            basePath = nonDir ? new File(SVNPathUtil.tail(url1.toDecodedString())).getAbsoluteFile() : new File("").getAbsoluteFile();
        }

        ISvnDiffCallback oldCallback = new SvnDiffSummarizeCallback(path1 != null ? SVNFileUtil.createFilePath(path1.getParentFile(), target1) : SVNFileUtil.createFilePath(new File("").getAbsolutePath(), target1), false, anchor1, nonDir ? basePath.getParentFile() : basePath, handler);
        ISvnDiffCallback2 callback = new SvnDiffCallbackWrapper(oldCallback, recurseIntoDeletedDirectories, nonDir ? basePath.getParentFile() : basePath);

        if (kind2 == SVNNodeKind.NONE) {
            SVNURL tmpUrl;

            tmpUrl = url1;
            url1 = url2;
            url2 = tmpUrl;

            long tmpRev;
            tmpRev = rev1;
            rev1 = rev2;
            rev2 = tmpRev;

            tmpUrl = anchor1;
            anchor1 = anchor2;
            anchor2 = tmpUrl;

            String tmpTarget;
            tmpTarget = target1;
            target1 = target2;
            target2 = tmpTarget;

            callback = new SvnReverseOrderDiffCallback(callback, null);
        }

        repository1.setLocation(anchor1, true);
        repository2.setLocation(anchor1, true);
        SvnNgRemoteDiffEditor2 editor = null;
        try {
            editor = new SvnNgRemoteDiffEditor2(rev1, false, repository2, callback);
            final long finalRev1 = rev1;
            ISVNReporterBaton reporter = new ISVNReporterBaton() {

                public void report(ISVNReporter reporter) throws SVNException {
                    reporter.setPath("", null, finalRev1, SVNDepth.INFINITY, false);
                    reporter.finishReport();
                }
            };
            repository1.diff(url2, rev2, rev1, target1, !useAncestry, depth, false, reporter, SVNCancellableEditor.newInstance(editor, this, getDebugLog()));
        } finally {
            repository2.closeSession();
            if (editor != null) {
                editor.cleanup();
            }
        }
    }

    private void doDiffReposWC(SvnTarget target1, SVNRevision revision1,
                               SvnTarget target2, SVNRevision revision2,
                               SVNRevision pegRevision, boolean reverse, SVNDepth depth, boolean useAncestry,
                               boolean recurseIntoDeletedDirectories,
                               ISVNDiffStatusHandler handler) throws SVNException {//TODO: changelists?

        SvnNgDiffUtil.doDiffSummarizeReposWC(target1, revision1, pegRevision, target2, revision2, reverse, getRepositoryAccess(), getWcContext(), false, depth, useAncestry, recurseIntoDeletedDirectories, getOperation().getApplicableChangelists(), false, null, handler, this);
    }

    private void doDiffWCWC(SvnTarget target1, SVNRevision revision1,
                            SvnTarget target2, SVNRevision revision2,
                            SVNDepth depth, boolean useAncestry, ISVNDiffStatusHandler handler) throws SVNException {
        assert !target1.isURL();
        assert !target2.isURL();

        File path1 = target1.getFile();
        File path2 = target2.getFile();

        if (!path1.equals(path2) || (!(revision1 == SVNRevision.BASE && revision2 == SVNRevision.WORKING))) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.INCORRECT_PARAMS, "Summarized diffs are only supported between a path's text-base and its working files at this time");
            SVNErrorManager.error(errorMessage, SVNLogType.WC);
        }

        SVNNodeKind kind = SVNFileType.getNodeKind(SVNFileType.getType(path1));

        String targetString1 = (kind == SVNNodeKind.DIR) ? "" : SVNFileUtil.getFileName(path1);
        File basePath = (kind == SVNNodeKind.DIR) ? path1 : SVNFileUtil.getParentFile(path1);

        SVNURL baseUrl = kind == SVNNodeKind.DIR ? getRepositoryAccess().getTargetURL(target1) : getRepositoryAccess().getTargetURL(SvnTarget.fromFile(SVNFileUtil.getParentFile(target1.getFile())));
        ISvnDiffCallback callback = new SvnDiffSummarizeCallback(path1, false, baseUrl, basePath, handler);

        SvnNgDiffUtil.doDiffWCWC(path1, getRepositoryAccess(), getWcContext(), depth, useAncestry, getOperation().isRecurseIntoDeletedDirectories(), getOperation().getApplicableChangelists(), false, false, null, callback, getOperation().getEventHandler());
    }

    private SVNURL resolvePeggedDiffTargetUrl(SVNURL url, File path, SVNRevision pegRevision, SVNRevision revision) throws SVNException {
        try {
            final Structure<SvnRepositoryAccess.LocationsInfo> locationsInfo = getRepositoryAccess().getLocations(null,
                    url == null ? SvnTarget.fromFile(path) : SvnTarget.fromURL(url),
                    pegRevision, revision, SVNRevision.UNDEFINED);
            return locationsInfo.get(SvnRepositoryAccess.LocationsInfo.startUrl);
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() == SVNErrorCode.CLIENT_UNRELATED_RESOURCES ||
                    e.getErrorMessage().getErrorCode() == SVNErrorCode.FS_NOT_FOUND) {
                return null;
            }
            throw e;
        }
    }

    private void checkDiffTargetExists(SVNURL url, long revision, long otherRevision, SVNRepository repository) throws SVNException {
        SVNURL sessionUrl = repository.getLocation();
        boolean equal = sessionUrl.equals(url);
        if (!equal) {
            repository.setLocation(url, true);
        }
        SVNNodeKind kind = repository.checkPath("", revision);
        if (kind == SVNNodeKind.NONE) {
            if (revision == otherRevision) {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "Diff target ''{0}'' was not found in the repository at revision ''{1}''", url, revision);
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            } else {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.FS_NOT_FOUND, "Diff target ''{0}'' was not found in the repository at revision ''{1}'' or ''{2}''", url, revision, otherRevision);
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }
        }
        if (!equal) {
            repository.setLocation(url, true);
        }
    }

    private ISVNDebugLog getDebugLog() {
        return SVNDebugLog.getDefaultLog();
    }

    private long getRevisionNumber(SVNRevision revision1, SVNRepository repository1, SVNURL url1) throws SVNException {
        final Structure<SvnRepositoryAccess.RevisionsPair> revisionNumber = getRepositoryAccess().getRevisionNumber(repository1, SvnTarget.fromURL(url1, revision1), revision1, null);
        return revisionNumber.lng(SvnRepositoryAccess.RevisionsPair.revNumber);
    }

    private SVNURL getURL(File path1) throws SVNException {
        return getRepositoryAccess().getURLFromPath(SvnTarget.fromFile(path1), SVNRevision.UNDEFINED, null).<SVNURL>get(SvnRepositoryAccess.UrlInfo.url);
    }

    protected SVNRepository createRepository(SVNURL url, File path, boolean mayReuse) throws SVNException {
        return getRepositoryAccess().createRepository(url, null, mayReuse);
    }

    private static ISVNDiffStatusHandler createHandlerForReceiver(final ISvnObjectReceiver<SvnDiffStatus> receiver) {
        return new ISVNDiffStatusHandler() {
            public void handleDiffStatus(SVNDiffStatus diffStatus) throws SVNException {
                if (receiver != null) {
                    final SvnTarget target = diffStatus.getURL() != null ?
                            SvnTarget.fromURL(diffStatus.getURL()) :
                            SvnTarget.fromFile(diffStatus.getFile());
                    receiver.receive(target, SvnCodec.diffStatus(diffStatus));
                }
            }
        };
    }

    protected SvnNgRepositoryAccess getRepositoryAccess() throws SVNException {
        if (repositoryAccess == null) {
            repositoryAccess = new SvnNgRepositoryAccess(getOperation(), getWcContext());
        }
        return repositoryAccess;
    }
}
