package org.tmatesoft.svn.core.wc2;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.SVNDiffOptions;
import org.tmatesoft.svn.core.wc.SVNRevision;


/**
 * Represents merge operation.
 * There are three possible cases of merge operation.
 * If revision ranges (<code>ranges</code>) are provided, merges the changes 
 * between <code>source</code> in its <code>pegRevision</code>, 
 * as it changed between the <code>ranges</code> in to 
 * the working copy path defined in operation's <code>target</code>.
 * If revision ranges are not provided, merges changes from <code>firstSource</code>/its <code>pegRevision</code> to
 * <code>secondSource</code>/its <code>pegRevision</code> into the working copy path 
 * defined in operation's <code>target</code>.
 * The third case is if <code>reintegrate</code> is <code>true</code> performs 
 * a reintegration merge of <code>source</code> at its <code>pegRevision</code> 
 * into working copy <code>target</code>.
 * 
 * <ul>
 * <li>
 * <b>Merge between sources/revision ranges, no reintegration.</b>
 * 
 * <p/>
 * If <code>depth</code> is {@link SVNDepth#INFINITY}, merges fully
 * recursively. Else if {@link SVNDepth#IMMEDIATES}, merges changes at most
 * to files that are immediate children of <code>target</code> and to
 * directory properties of <code>target</code> and its immediate
 * subdirectory children. Else if {@link SVNDepth#FILES}, merges at most to
 * immediate file children of <code>target</code> and to
 * <code>target</code> itself. Else if {@link SVNDepth#EMPTY}, applies
 * changes only to <code>target</code> (i.e., directory property changes
 * only).
 * 
 * <p/>
 * If <code>depth</code> is {@link SVNDepth#UNKNOWN}, uses the depth of
 * <code>target</code>.
 * 
 * <p/>
 * Uses <code>ignoreAncestry</code> to control whether or not items being
 * diffed will be checked for relatedness first. Unrelated items are
 * typically transmitted to the editor as a deletion of one thing and the
 * addition of another, but if this flag is <code>false</code>, unrelated items will be diffed as if
 * they were related.
 * 
 * <p/>
 * If <code>force</code> is not set and the merge involves deleting locally
 * modified or unversioned items the operation will fail. If
 * <code>force</code> is set such items will be deleted.
 * 
 * <p/>
 * Merge options <code>mergeOptions</code> is a collection of {@link SVNDiffOptions},
 * they are used to pass arguments to the merge processes (internal or external).
 * 
 * <p/>
 * If the caller's {@link ISVNEventHandler} is not <code>null</code>, 
 * then it will be called once for each merged target.
 * 
 * <p/>
 * If <code>recordOnly</code> is <code>true</code>, the
 * merge isn't actually performed, but the mergeinfo for the revisions which
 * would've been merged is recorded in the working copy (and must be
 * subsequently committed back to the repository).
 * 
 * <p/>
 * If <code>dryRun</code> is <code>true</code>, the
 * merge is carried out, and full notification feedback is provided, but the
 * working copy is not modified.
 * 
 * <ul>
 * <li>
 * <b>Merge between revision ranges.</b>
 * 
 * <p/>
 * <code>Ranges</code> is a collection of {@link SvnRevisionRange}
 * ranges. These ranges may describe additive and/or subtractive merge
 * ranges, they may overlap fully or partially, and/or they may partially or
 * fully negate each other. This range list is not required to be sorted.
 * 
 * </li>
 * <li>
 * <b>Merge between two sources.</b>
 * 
 * <p/>
 * <code>FirstSource</code> and <code>secondSource</code> must both represent the same node
 * kind - that is, if <code>firstSource</code> is a directory, <code>secondSource</code>
 * must also be, and if <code>firstSource</code> is a file, <code>secondSource</code> must
 * also be.
 * 
 * </li>
 * </ul>
 * 
 * </li>
 * <li>
 * <b>Reintegration merge.</b>
 * 
 * <p/>
 * This kind of merge should be used for back merging (for example, merging
 * branches back to trunk, in which case merge is carried out by comparing
 * the latest trunk tree with the latest branch tree; i.e. the resulting
 * difference is exactly the branch changes which will go back to trunk).
 * 
 * <p/>
 * Destination <code>target</code> must be a single-revision, {@link SVNDepth#INFINITY}, 
 * pristine, unswitched working copy - in other words, it must reflect a
 * single revision tree, the "target". The mergeinfo on <code>source</code>
 * must reflect that all of the target has been merged into it.
 *  
 * <p/>
 * The depth of the merge is always {@link SVNDepth#INFINITY}.
 * 
 * <p/>
 * If <code>source</code>'s <code>pegRevision</code> is 
 * <code>null</code> or {@link SVNRevision#isValid() invalid}, then it defaults to
 * {@link SVNRevision#HEAD}.
 * 
 * </li>
 * </ul>
 * 
 * <p/>
 * Note: this operation requires repository access.
 * 
 * <p/>
 * {@link #run()} method throws org.tmatesoft.svn.core.SVNException in
 *             the following cases:
 *             <ul>
 *             <li/>exception with {@link SVNErrorCode#CLIENT_BAD_REVISION}-
 *             If any revision in the list of provided ranges is
 *             {@link SVNRevision#isValid() invalid}
 *             <li/>exception with {@link SVNErrorCode#CLIENT_BAD_REVISION}
 *             error code - if either <code>firstSource</code>'s <code>pegRevision</code> or <code>
 *             firstSource</code>'s <code>pegRevision</code> is {@link SVNRevision#isValid() invalid}
 *             </ul>
 *             </ul>
 *             
 * @author TMate Software Ltd.
 * @version 1.7
 */
public class SvnMerge extends SvnOperation<Void> {

    /*
     * Honest automatic merge is yet to be implemented (SVNKIT-432)
     * This is quick work-around
     */
    public static final boolean EMULATE_AUTOMATIC_MERGE = true;

    private SvnTarget firstSource;
    private SvnTarget secondSource;

    private boolean ignoreMergeInfo;
    private boolean ignoreAncestry;
    private boolean force;
    private boolean recordOnly;
    private boolean dryRun;
    private boolean allowMixedRevisions;
    
    private SvnTarget source;
    private boolean reintegrate;

    private Collection<SvnRevisionRange> ranges;
    private SVNDiffOptions mergeOptions;
    
    protected SvnMerge(SvnOperationFactory factory) {
        super(factory);
    }

    /**
     * Adds the revision range to collection of revision ranges for merging.
     * @param range revision range for merge
     * @see #getRevisionRanges()
     */
    public void addRevisionRange(SvnRevisionRange range) {
        if (ranges == null) {
            ranges = new ArrayList<SvnRevisionRange>();
        }
        SVNRevision start = range.getStart();
        SVNRevision end = range.getEnd();        
        if (start == SVNRevision.UNDEFINED && end == SVNRevision.UNDEFINED) {
            start = SVNRevision.create(0);
            end = getSource().getResolvedPegRevision();
            range  = SvnRevisionRange.create(start, end);
        }
        ranges.add(range);
    }
    
    /**
    * Returns the  collection of {@link SvnRevisionRange}
    * ranges. These ranges may describe additive and/or subtractive merge
    * ranges, they may overlap fully or partially, and/or they may partially or
    * fully negate each other. This range list is not required to be sorted.
    * 
    * @return revision ranges of the merge
    */
    public Collection<SvnRevisionRange> getRevisionRanges() {
        if (ranges == null && EMULATE_AUTOMATIC_MERGE) {
            SvnTarget source = getSource();
            SVNRevision startRevision = SVNRevision.create(1);
            SVNRevision endRevision = source.getPegRevision();
            if (endRevision == null || endRevision == SVNRevision.UNDEFINED) {
                endRevision = source.isURL() ? SVNRevision.HEAD : SVNRevision.WORKING;
            }
            final List<SvnRevisionRange> revisionRanges = new ArrayList<SvnRevisionRange>();
            revisionRanges.add(SvnRevisionRange.create(startRevision, endRevision));
            return revisionRanges;
        }
        return ranges;
    }
    
    /**
     * Sets source of the merge with reintegrate flag.
     * This <code>source</code> is used in merge between revisions
     * and reintegrate merge.
     *   
     * @param source of merge
     * @param reintegrate <code>true</code> if it is reintegrate merge, otherwise <code>false</code>
     * @see #isReintegrate()
     */
    public void setSource(SvnTarget source, boolean reintegrate) {
        this.source = source;
        this.reintegrate = reintegrate;
        if (source != null) {
            setSources(null, null);
        }
    }
    
    /**
     * Sets first and seconds sources of the merge.
     * Those sources are used in merge between two sources.
     * 
     * @param source1 first source
     * @param source2 second source
     */
    public void setSources(SvnTarget source1, SvnTarget source2) {
        this.firstSource = source1;
        this.secondSource = source2;
        if (firstSource != null) {
            setSource(null, false);
        }
    }
    
    /**
     * Returns source for merge between revisions and reintegrate merge.
     *
     * @return merge source 
     */
    public SvnTarget getSource() {
        return this.source;
    }
    
    /**
     * Returns first source for merge between two sources.
     * 
     * @return first source of merge
     */
    public SvnTarget getFirstSource() {
        return this.firstSource;
    }
    
    /**
     * Returns second source for merge between two sources.
     * 
     * @return first source of merge
     */
    public SvnTarget getSecondSource() {
        return this.secondSource;
    }
    
    /**
     * Returns whether it is reintegrate merge.
     * This kind of merge should be used for back merging (for example, merging
     * branches back to trunk, in which case merge is carried out by comparing
     * the latest trunk tree with the latest branch tree; i.e. the resulting
     * difference is exactly the branch changes which will go back to trunk).
     * 
     * @return <code>true</code> if it is reintegrate merge, otherwise <code>false</code>
     */
    public boolean isReintegrate() {
        return this.reintegrate;
    }

    public boolean isIgnoreMergeInfo() {
        return ignoreMergeInfo;
    }

    public void setIgnoreMergeInfo(boolean ignoreMergeInfo) {
        this.ignoreMergeInfo = ignoreMergeInfo;
    }

    /**
     * Returns whether or not items being diffed will be checked for relatedness first. Unrelated items are
	 * typically transmitted to the editor as a deletion of one thing and the
	 * addition of another, but if this flag is <code>false</code>, unrelated items will be diffed as if
	 * they were related.
	 * 
     * @return <code>true</code> if ancestry should be ignored, otherwise <code>false</code>
     */
    public boolean isIgnoreAncestry() {
        return ignoreAncestry;
    }

    /**
     * Sets whether or not items being diffed will be checked for relatedness first. Unrelated items are
	 * typically transmitted to the editor as a deletion of one thing and the
	 * addition of another, but if this flag is <code>false</code>, unrelated items will be diffed as if
	 * they were related.
	 * 
     * @param ignoreAncestry <code>true</code> if ancestry should be ignored, otherwise <code>false</code>
     */
    public void setIgnoreAncestry(boolean ignoreAncestry) {
        this.ignoreAncestry = ignoreAncestry;
    }

    /**
     * Returns whether to fail if merge involves deleting locally
	 * modified or unversioned items. If <code>force</code> is <code>true</code> such items will be deleted,
	 * otherwise operation will fail.
	 * 
     * @return <code>true</code> if operation should be forced to run, otherwise <code>false</code>
     */
    public boolean isForce() {
        return force;
    }

    /**
     * Sets whether to fail if merge involves deleting locally
	 * modified or unversioned items. If <code>force</code> is <code>true</code> such items will be deleted,
	 * otherwise operation will fail.
	 * 
     * @param force <code>true</code> if operation should be forced to run, otherwise <code>false</code>
     */
    public void setForce(boolean force) {
        this.force = force;
    }

    /**
     * Returns whether the merge isn't actually performed, but the mergeinfo for the revisions which
     * would've been merged is recorded in the working copy (and must be
     * subsequently committed back to the repository).
     * 
     * 
     * @return <code>true</code> if operation should record only the result of merge - mergeinfo data, otherwise <code>false</code>
     */
    public boolean isRecordOnly() {
        return recordOnly;
    }

    /**
     * Sets whether the merge isn't actually performed, but the mergeinfo for the revisions which
     * would've been merged is recorded in the working copy (and must be
     * subsequently committed back to the repository).
     * 
     * 
     * @param recordOnly <code>true</code> if operation should record only the result of merge - mergeinfo data, otherwise <code>false</code>
     */
    public void setRecordOnly(boolean recordOnly) {
        this.recordOnly = recordOnly;
    }

    /**
     * Returns whether the merge is carried out, and full notification feedback is provided, but the
     * working copy is not modified.
     * 
     * @return <code>true</code> if the operation should only find out if a file can be merged successfully, otherwise <code>false</code>
     */
    public boolean isDryRun() {
        return dryRun;
    }

    /**
     * Sets whether the merge is carried out, and full notification feedback is provided, but the
     * working copy is not modified.
     * 
     * @param dryRun <code>true</code> if the operation should only find out if a file can be merged successfully, otherwise <code>false</code>
     */
    public void setDryRun(boolean dryRun) {
        this.dryRun = dryRun;
    }

    /**
     * Returns whether to allow merge target to have mixed revisions.
     * If set to <code>false</code> and target has mixed revisions, {@link SVNException} is 
     * thrown with error codes {@link SVNErrorCode#CLIENT_NOT_READY_TO_MERGE} 
     * or {@link SVNErrorCode#CLIENT_MERGE_UPDATE_REQUIRED}.
     * 
     * @return <code>true</code> if operation allows merging to mixed-revision working copy, otherwise <code>false</code>
     * @since 1.7, SVN 1.7
     */
    public boolean isAllowMixedRevisions() {
        return allowMixedRevisions;
    }

    /**
     * Sets whether to allow merge target to have mixed revisions.
     * If set to <code>false</code> and target has mixed revisions, {@link SVNException} is 
     * thrown with error codes {@link SVNErrorCode#CLIENT_NOT_READY_TO_MERGE} 
     * or {@link SVNErrorCode#CLIENT_MERGE_UPDATE_REQUIRED}.
     * 
     * @param allowMixedRevisions <code>true</code> if operation allows merging to mixed-revision working copy, otherwise <code>false</code>
     * @since 1.7, SVN 1.7
     */
    public void setAllowMixedRevisions(boolean allowMixedRevisions) {
        this.allowMixedRevisions = allowMixedRevisions;
    }

    /**
     * Returns the operation's merge options controlling white-spaces and eol-styles.
     * 
     * @return merge options of the operation
     */
    public SVNDiffOptions getMergeOptions() {
        return mergeOptions;
    }

    /**
     * Sets the operation's merge options controlling white-spaces and eol-styles.
     * 
     * @param mergeOptions merge options of the operation
     */
    public void setMergeOptions(SVNDiffOptions mergeOptions) {
        this.mergeOptions = mergeOptions;
    }
    
    /**
     * 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 true;
    }
}
