package org.tmatesoft.svn.core.wc2;

import java.io.File;

import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * Produces a diff summary which lists the changed items between
 * <code>source</code> in its <code>pegRevision</code>, as it changed
 * between <code>startRevision</code> and <code>endRevision</code>,
 * or diff summary between <code>firstSource</code> at its <code>pegRevision</code> 
 * and <code>secondSource</code> at its <code>pegRevision</code>.
 * Changes are produced without creating text deltas.
 * 
 * <ul>
 * <li>
 * If it is diff between <code>startRevision</code> and <code>endRevision</code> of one <code>source</code>:
 * 
 * <p/>
 * <code>Source</code> can be either working copy path or URL.
 * 
 * <p/>
 * If <code>pegRevision</code> is {@link SVNRevision#isValid() invalid}, behaves identically to 
 * diff between two sources, using <code>source</code>'s path for both sources.
 * </li>
 * 
 * <li>
 * If it is diff between first <code>source</code> and second <code>source</code>:
 * 
 * <p/>
 * First and second <code>sources</code> can be either working copy path or URL.
 * 
 * <p/>
 * Both <code>sources</code> must represent the same node kind -- that is, if first <code>source</code> is a directory,
 * second <code>sources</code> must also be, and if first <code>sources</code> is a file,
 * second <code>sources</code> must also be.
 * 
 * </li>
 * </ul>
 * 
 * The operation may report false positives if <code>ignoreAncestry</code> is
 * <code>false</code>, since a file might have been
 * modified between two revisions, but still have the same contents.
 * 
 * <p/>
 * If <code>depth</code> is {@link SVNDepth#INFINITY}, diffs fully
 * recursively. Else if it is {@link SVNDepth#IMMEDIATES}, diffs the named
 * paths and their file children (if any), and diffs properties of
 * subdirectories, but does not descend further into the subdirectories.
 * Else if {@link SVNDepth#FILES}, behaves as if for
 * {@link SVNDepth#IMMEDIATES} except doesn't diff properties of
 * subdirectories. If {@link SVNDepth#EMPTY}, diffs exactly the named paths
 * but nothing underneath them.
 * 
 * <p/>
 * {@link #run()} method throws {@link SVNException} in the following cases:
 *             <ul>
 *             <li/>exception with {@link SVNErrorCode#CLIENT_BAD_REVISION}
 *             error code - if either <code>startRevision</code> or <code>endRevision</code> is
 *             {@link SVNRevision#isValid() invalid} 
 *             <li/>exception with
 *             {@link SVNErrorCode#UNSUPPORTED_FEATURE} error code - if
 *             either of <code>startRevision</code> or </code>endRevision</code> is either
 *             {@link SVNRevision#WORKING} or {@link SVNRevision#BASE}
 *             </ul>
 * 
 * @author TMate Software Ltd.
 * @version 1.7
 */
public class SvnDiffSummarize extends SvnReceivingOperation<SvnDiffStatus> {

    private SvnTarget firstSource;
    private SvnTarget secondSource;
    
    private SvnTarget source;
    private SVNRevision startRevision;
    private SVNRevision endRevision;
    
    private boolean ignoreAncestry;
    private boolean recurseIntoDeletedDirectories;

    protected SvnDiffSummarize(SvnOperationFactory factory) {
        super(factory);
        setRecurseIntoDeletedDirectories(true);
    }
    
    /**
     * Sets the diff's <code>source</code> with start and end revisions for one-source type of operation.
     * 
     * @param source source of the diff 
     * @param start start revision of the diff
     * @param end end revision of the diff
     */
    public void setSource(SvnTarget source, SVNRevision start, SVNRevision end) {
        this.source = source;
        this.startRevision = start;
        this.endRevision = end;
        if (source != null) {
            setSources(null, null);
        }
    }
    
    /**
     * Sets both diff's <code>sources</code>. 
     * 
     * @param source1 first source of the diff 
     * @param source2 second source of the diff
     */
    public void setSources(SvnTarget source1, SvnTarget source2) {
        this.firstSource = source1;
        this.secondSource = source2;
        if (firstSource != null) {
            setSource(null, null, null);
        }
    }
    
    /**
     * Gets the diff's <code>source</code> with start and end revisions for one-target type of operation.
     * 
     * @return source of the diff 
     */
    public SvnTarget getSource() {
        return source;
    }
    
    public SVNRevision getStartRevision() {
        return startRevision;
    }
    
    public SVNRevision getEndRevision() {
        return endRevision;
    }
    
    public SvnTarget getFirstSource() {
        return firstSource;
    }

    public SvnTarget getSecondSource() {
        return secondSource;
    }
    public boolean isIgnoreAncestry() {
        return ignoreAncestry;
    }
    public void setIgnoreAncestry(boolean ignoreAncestry) {
        this.ignoreAncestry = ignoreAncestry;
    }

    public boolean isRecurseIntoDeletedDirectories() {
        return recurseIntoDeletedDirectories;
    }

    public void setRecurseIntoDeletedDirectories(boolean recurseIntoDeletedDirectories) {
        this.recurseIntoDeletedDirectories = recurseIntoDeletedDirectories;
    }

    @Override
    protected File getOperationalWorkingCopy() {
        if (getSource() != null && getSource().isFile()) {
            return getSource().getFile();
        } else if (getFirstSource() != null && getFirstSource().isFile()) {
            return getFirstSource().getFile();
        } else if (getSecondSource() != null && getSecondSource().isFile()) {
            return getSecondSource().getFile();
        }
        
        return null;
    }

    @Override
    protected void ensureArgumentsAreValid() throws SVNException {
        if (getSource() == null || getSource().getPegRevision() == null || getSource().getPegRevision() == SVNRevision.UNDEFINED) {
            final SvnTarget firstSource = getFirstSource();
            final SvnTarget secondSource = getSecondSource();
            ensureArgumentsAreValid(firstSource.getURL(), firstSource.getFile(), firstSource.getPegRevision(),
                    secondSource.getURL(), secondSource.getFile(), secondSource.getPegRevision(),
                    null);
        } else {
            final SvnTarget source = getSource();
            ensureArgumentsAreValid(source.getURL(), source.getFile(), getStartRevision(),
                    source.getURL(), source.getFile(), getEndRevision(),
                    source.getPegRevision());
        }
    }

    private void ensureArgumentsAreValid(SVNURL url1, File path1, SVNRevision revision1,
                                         SVNURL url2, File path2, SVNRevision revision2,
                                         SVNRevision pegRevision) throws SVNException {
        if (pegRevision == null) {
            pegRevision = SVNRevision.UNDEFINED;
        }
        ensureRevisionIsValid(revision1);
        ensureRevisionIsValid(revision2);

        final boolean isPath1Local = startRevision == SVNRevision.WORKING || startRevision == SVNRevision.BASE;
        final boolean isPath2Local = endRevision == SVNRevision.WORKING || endRevision == SVNRevision.BASE;

        final boolean isRepos1;
        final boolean isRepos2;

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

            isRepos1 = !isPath1Local;
            isRepos2 = !isPath2Local;
        } else {
            isRepos1 = !isPath1Local || (url1 != null);
            isRepos2 = !isPath2Local || (url2 != null);
        }

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

    private void ensureRevisionIsValid(SVNRevision revision) throws SVNException {
        final boolean revisionIsValid = revision != null && revision.isValid();
        if (!revisionIsValid) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_BAD_REVISION, "Not all required revisions are specified");
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
    }

    /**
     * Gets whether the operation changes working copy
     * @return <code>true</code> if the operation changes the working copy, otherwise <code>false</code>
     */
    @Override
    public boolean isChangesWorkingCopy() {
        return false;
    }
}
