package org.tmatesoft.svn.core.internal.wc16;

import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.tmatesoft.svn.core.SVNCommitInfo;
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.SVNProperties;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.wc.SVNCopyDriver;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNExternal;
import org.tmatesoft.svn.core.internal.wc.SVNPath;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.DefaultSVNCommitHandler;
import org.tmatesoft.svn.core.wc.DefaultSVNCommitParameters;
import org.tmatesoft.svn.core.wc.ISVNCommitHandler;
import org.tmatesoft.svn.core.wc.ISVNCommitParameters;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNExternalsHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNRepositoryPool;
import org.tmatesoft.svn.core.wc.SVNCommitItem;
import org.tmatesoft.svn.core.wc.SVNCopySource;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import org.tmatesoft.svn.core.wc2.SvnTarget;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * The <b>SVNCopyClient</b> provides methods to perform any kinds of copying and
 * moving that SVN supports - operating on both Working Copies (WC) and URLs.
 * <p>
 * Copy operations allow a user to copy versioned files and directories with all
 * their previous history in several ways.
 * <p>
 * Supported copy operations are:
 * <ul>
 * <li>Working Copy to Working Copy (WC-to-WC) copying - this operation copies
 * the source Working Copy item to the destination one and schedules the source
 * copy for addition with history.
 * <li>Working Copy to URL (WC-to-URL) copying - this operation commits to the
 * repository (exactly to that repository location that is specified by URL) a
 * copy of the Working Copy item.
 * <li>URL to Working Copy (URL-to-WC) copying - this operation will copy the
 * source item from the repository to the Working Copy item and schedule the
 * source copy for addition with history.
 * <li>URL to URL (URL-to-URL) copying - this is a fully repository-side
 * operation, it commits a copy of the source item to a specified repository
 * location (within the same repository, of course).
 * </ul>
 * <p>
 * Besides just copying <b>SVNCopyClient</b> also is able to move a versioned
 * item - that is first making a copy of the source item and then scheduling the
 * source item for deletion when operating on a Working Copy, or right
 * committing the deletion of the source item when operating immediately on the
 * repository.
 * <p>
 * Supported move operations are:
 * <ul>
 * <li>Working Copy to Working Copy (WC-to-WC) moving - this operation copies
 * the source Working Copy item to the destination one and schedules the source
 * item for deletion.
 * <li>URL to URL (URL-to-URL) moving - this is a fully repository-side
 * operation, it commits a copy of the source item to a specified repository
 * location and deletes the source item.
 * </ul>
 * <p>
 * Overloaded <b>doCopy()</b> methods of <b>SVNCopyClient</b> are similar to <code>'svn copy'</code>
 * and <code>'svn move'</code> commands of the SVN command line client.
 * 
 * @version 1.3
 * @author TMate Software Ltd.
 * @since 1.2
 * @see <a target="_top" href="http://svnkit.com/kb/examples/">Examples</a>
 */
public class SVNCopyClient16 extends SVNCopyDriver {

    protected ISVNCommitHandler myCommitHandler;
    protected ISVNCommitParameters myCommitParameters;
    protected ISVNExternalsHandler myExternalsHandler;

    /**
     * Constructs and initializes an <b>SVNCopyClient</b> object with the
     * specified run-time configuration and authentication drivers.
     * <p>
     * If <code>options</code> is <span class="javakeyword">null</span>, then
     * this <b>SVNCopyClient</b> will be using a default run-time configuration
     * driver which takes client-side settings from the default SVN's run-time
     * configuration area but is not able to change those settings (read more on
     * {@link ISVNOptions} and {@link SVNWCUtil}).
     * <p>
     * If <code>authManager</code> is <span class="javakeyword">null</span>,
     * then this <b>SVNCopyClient</b> will be using a default authentication and
     * network layers driver (see
     * {@link SVNWCUtil#createDefaultAuthenticationManager()}) which uses
     * server-side settings and auth storage from the default SVN's run-time
     * configuration area (or system properties if that area is not found).
     * 
     * @param authManager
     *            an authentication and network layers driver
     * @param options
     *            a run-time configuration options driver
     */
    public SVNCopyClient16(ISVNAuthenticationManager authManager, ISVNOptions options) {
        super(authManager, options);
    }

    /**
     * Constructs and initializes an <b>SVNCopyClient</b> object with the
     * specified run-time configuration and repository pool object.
     * <p/>
     * If <code>options</code> is <span class="javakeyword">null</span>, then
     * this <b>SVNCopyClient</b> will be using a default run-time configuration
     * driver which takes client-side settings from the default SVN's run-time
     * configuration area but is not able to change those settings (read more on
     * {@link ISVNOptions} and {@link SVNWCUtil}).
     * <p/>
     * If <code>repositoryPool</code> is <span class="javakeyword">null</span>,
     * then {@link org.tmatesoft.svn.core.io.SVNRepositoryFactory} will be used
     * to create {@link SVNRepository repository access objects}.
     * 
     * @param repositoryPool
     *            a repository pool object
     * @param options
     *            a run-time configuration options driver
     */
    public SVNCopyClient16(ISVNRepositoryPool repositoryPool, ISVNOptions options) {
        super(repositoryPool, options);
    }

    /**
     * Sets an implementation of <b>ISVNCommitHandler</b> to the commit handler
     * that will be used during commit operations to handle commit log messages.
     * The handler will receive a clien's log message and items (represented as
     * <b>SVNCommitItem</b> objects) that will be committed. Depending on
     * implementor's aims the initial log message can be modified (or something
     * else) and returned back.
     * <p>
     * If using <b>SVNCopyClient</b> without specifying any commit handler then
     * a default one will be used - {@link DefaultSVNCommitHandler}.
     * 
     * @param handler
     *            an implementor's handler that will be used to handle commit
     *            log messages
     * @see #getCommitHandler()
     * @see SVNCommitItem
     */
    public void setCommitHandler(ISVNCommitHandler handler) {
        this.myCommitHandler = handler;
    }

    /**
     * Returns the specified commit handler (if set) being in use or a default
     * one (<b>DefaultSVNCommitHandler</b>) if no special implementations of
     * <b>ISVNCommitHandler</b> were previousely provided.
     * 
     * @return the commit handler being in use or a default one
     * @see #setCommitHandler(ISVNCommitHandler)
     * @see DefaultSVNCommitHandler
     */
    public ISVNCommitHandler getCommitHandler() {
        return this.myCommitHandler;
    }

    /**
     * Sets commit parameters to use.
     * <p>
     * When no parameters are set {@link DefaultSVNCommitParameters default}ones
     * are used.
     * 
     * @param parameters
     *            commit parameters
     * @see #getCommitParameters()
     */
    public void setCommitParameters(ISVNCommitParameters parameters) {
        this.myCommitParameters = parameters;
    }

    /**
     * Returns commit parameters.
     * <p>
     * If no user parameters were previously specified, once creates and returns
     * {@link DefaultSVNCommitParameters default} ones.
     * 
     * @return commit parameters
     * @see #setCommitParameters(ISVNCommitParameters)
     */
    public ISVNCommitParameters getCommitParameters() {
        return this.myCommitParameters;
    }

    /**
     * Sets an externals handler to be used by this client object.
     * 
     * @param externalsHandler
     *            user's implementation of {@link ISVNExternalsHandler}
     * @see #getExternalsHandler()
     * @since 1.2
     */
    public void setExternalsHandler(ISVNExternalsHandler externalsHandler) {
        this.myExternalsHandler = externalsHandler;
    }

    /**
     * Returns an externals handler used by this update client.
     * <p/>
     * If no user's handler is provided then
     * {@link ISVNExternalsHandler#DEFAULT} is returned and used by this client
     * object by default.
     * <p/>
     * For more information what externals handlers are for, please, refer to
     * {@link ISVNExternalsHandler} and
     * {@link #doCopy(SVNCopySource[],SVNURL,boolean,boolean,boolean,String,SVNProperties)}.
     * 
     * @return externals handler being in use
     * @see #setExternalsHandler(ISVNExternalsHandler)
     * @since 1.2
     */
    public ISVNExternalsHandler getExternalsHandler() {
        if (myExternalsHandler == null) {
            return ISVNExternalsHandler.DEFAULT;
        }
        return this.myExternalsHandler;
    }

    /**
     * Copies each source in <code>sources</code> to <code>dst</code>.
     * <p/>
     * If multiple <code>sources</code> are given, <code>dst</code> must be a
     * directory, and <code>sources</code> will be copied as children of
     * <code>dst</code>.
     * <p/>
     * Each <code>src</code> in <code>sources</code> must be files or
     * directories under version control, or URLs of a versioned item in the
     * repository. If <code>sources</code> has multiple items, they must be all
     * repository URLs or all working copy paths.
     * <p/>
     * The parent of <code>dst</code> must already exist.
     * <p/>
     * If <code>sources</code> has only one item, attempts to copy it to
     * <code>dst</code>. If <code>failWhenDstExists</code> is <span
     * class="javakeyword">false</span> and <code>dst</code> already exists,
     * attempts to copy the item as a child of <code>dst</code>. If
     * <code>failWhenDstExists</code> is <span class="javakeyword">true</span>
     * and <code>dst</code> already exists, throws an {@link SVNException} with
     * the {@link SVNErrorCode#ENTRY_EXISTS} error code.
     * <p/>
     * If <code>sources</code> has multiple items, and
     * <code>failWhenDstExists</code> is <span class="javakeyword">false</span>,
     * all <code>sources</code> are copied as children of <code>dst</code>. If
     * any child of <code>dst</code> already exists with the same name any item
     * in <code>sources</code>, throws an {@link SVNException} with the
     * {@link SVNErrorCode#ENTRY_EXISTS} error code.
     * <p/>
     * If <code>sources</code> has multiple items, and
     * <code>failWhenDstExists</code> is <span class="javakeyword">true</span>,
     * throws an {@link SVNException} with the
     * {@link SVNErrorCode#CLIENT_MULTIPLE_SOURCES_DISALLOWED}.
     * <p/>
     * This method is just a variant of a local add operation, where
     * <code>sources</code> are scheduled for addition as copies. No changes
     * will happen to the repository until a commit occurs. This scheduling can
     * be removed with {@link SVNWCClient#doRevert(File[],SVNDepth,Collection)}.
     * <p/>
     * If
     * <code>makeParents is <span class="javakeyword">true</span>, creates any non-existent parent directories
     * also.
     * <p/>
     * If the caller's {@link ISVNEventHandler} is non-<span class="javakeyword">null</span>, invokes it  
	 * for each item added at the new location.
	 * <p/>
	 * Note: this routine requires repository access only when sources are urls.
     * 
     * @param sources
     *            array of copy sources
     * @param dst
     *            destination working copy path
     * @param isMove
     *            if <span class="javakeyword">true</span>, then it will be a
     *            move operation (delete, then add with history)
     * @param makeParents
     *            if <span class="javakeyword">true</span>, creates non-existent
     *            parent directories as well
     * @param failWhenDstExists
     *            controls whether to fail or not if <code>dst</code> already
     *            exists
     * @throws SVNException
     * @since 1.2, SVN 1.5
     */
    public void doCopy(SVNCopySource[] sources, File dst, boolean isMove, boolean makeParents, boolean failWhenDstExists) throws SVNException {
        doCopy(sources, dst, isMove, makeParents, failWhenDstExists, false, null);
    }

    private void doCopy(SVNCopySource[] sources, File dst, boolean isMove, boolean makeParents, boolean failWhenDstExists, boolean pinExternals, Map<SvnTarget, List<SVNExternal>> externalsToPin) throws SVNException {
        if (sources.length > 1 && failWhenDstExists) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_MULTIPLE_SOURCES_DISALLOWED);
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        sources = expandCopySources(sources);
        if (sources.length == 0) {
            return;
        }
        try {
            setupCopy(sources, new SVNPath(dst.getAbsolutePath(), false, false), isMove, makeParents, pinExternals, externalsToPin, null, null, getCommitHandler(), getCommitParameters(), getExternalsHandler());
        } catch (SVNException e) {
            SVNErrorCode err = e.getErrorMessage().getErrorCode();
            if (!failWhenDstExists && sources.length == 1 && (err == SVNErrorCode.ENTRY_EXISTS || err == SVNErrorCode.FS_ALREADY_EXISTS)) {
                SVNCopySource source = sources[0];
                String baseName = source.getName();
                if (source.isURL()) {
                    baseName = SVNEncodingUtil.uriDecode(baseName);
                }
                try {
                    setupCopy(sources, new SVNPath(new File(dst, baseName).getAbsolutePath(), false, false), isMove, makeParents, pinExternals, externalsToPin, null, null, getCommitHandler(), getCommitParameters(), getExternalsHandler());
                } catch (SVNException second) {
                    throw second;
                }
                return;
            }
            throw e;
        }
    }

    /**
     * Copies each source in <code>sources</code> to <code>dst</code>.
     * <p/>
     * If multiple <code>sources</code> are given, <code>dst</code> must be a
     * directory, and <code>sources</code> will be copied as children of
     * <code>dst</code>.
     * <p/>
     * Each <code>src</code> in <code>sources</code> must be files or
     * directories under version control, or URLs of a versioned item in the
     * repository. If <code>sources</code> has multiple items, they must be all
     * repository URLs or all working copy paths.
     * <p/>
     * The parent of <code>dst</code> must already exist.
     * <p/>
     * If <code>sources</code> has only one item, attempts to copy it to
     * <code>dst</code>. If <code>failWhenDstExists</code> is <span
     * class="javakeyword">false</span> and <code>dst</code> already exists,
     * attempts to copy the item as a child of <code>dst</code>. If
     * <code>failWhenDstExists</code> is <span class="javakeyword">true</span>
     * and <code>dst</code> already exists, throws an {@link SVNException} with
     * the {@link SVNErrorCode#FS_ALREADY_EXISTS} error code.
     * <p/>
     * If <code>sources</code> has multiple items, and
     * <code>failWhenDstExists</code> is <span class="javakeyword">false</span>,
     * all <code>sources</code> are copied as children of <code>dst</code>. If
     * any child of <code>dst</code> already exists with the same name any item
     * in <code>sources</code>, throws an {@link SVNException} with the
     * {@link SVNErrorCode#FS_ALREADY_EXISTS} error code.
     * <p/>
     * If <code>sources</code> has multiple items, and
     * <code>failWhenDstExists</code> is <span class="javakeyword">true</span>,
     * throws an {@link SVNException} with the
     * {@link SVNErrorCode#CLIENT_MULTIPLE_SOURCES_DISALLOWED}.
     * <p/>
     * {@link ISVNAuthenticationManager Authentication manager} (whether
     * provided directly through the appropriate constructor or in an
     * {@link ISVNRepositoryPool} instance) and {@link #getCommitHandler()
     * commit handler} are used to immediately attempt to commit the copy action
     * in the repository.
     * <p/>
     * If
     * <code>makeParents is <span class="javakeyword">true</span>, creates any non-existent parent directories
     * also.
     * <p/>
     * If non-<span class="javakeyword">null</span>, <code>revisionProperties</code>
     * is an object holding additional, custom revision properties (
     * <code>String</code> to {@link SVNPropertyValue} mappings) to be set on
     * the new revision. This table cannot contain any standard Subversion
     * properties.
     * <p/>
     * If the caller's {@link ISVNEventHandler} is non-<span
     * class="javakeyword">null</span>, invokes it for each item added at the
     * new location.
     * <p/>
     * When performing a wc-to-url copy (tagging|branching from a working copy)
     * it's possible to fix revisions of external working copies (if any) which
     * are located within the working copy being copied. For example, imagine
     * you have a working copy and on one of its subdirecotries you set an <span
     * class="javastring">"svn:externals"</span> property which does not contain
     * a revision number. Suppose you have made a tag from your working copy and
     * in some period of time a user checks out that tag. It could have happened
     * that the external project has evolved since the tag creation moment and
     * the tag version is nomore compatible with it. So, the user has a broken
     * project since it will not compile because of the API incompatibility
     * between the two versions of the external project: the HEAD one and the
     * one existed in the moment of the tag creation. That is why it appears
     * useful to fix externals revisions during a wc-to-url copy. To enable
     * externals revision fixing a user should implement
     * {@link ISVNExternalsHandler}. The user's implementation
     * {@link ISVNExternalsHandler#handleExternal(File,SVNURL,SVNRevision,SVNRevision,String,SVNRevision)}
     * method will be called on every external that will be met in the working
     * copy. If the user's implementation returns non-<span
     * class="javakeyword">null</span> external revision, it's compared with the
     * revisions fetched from the external definition. If they are different,
     * the user's revision will be written in the external definition of the
     * tag. Otherwise if the returned revision is equal to the revision from the
     * external definition or if the user's implementation returns <span
     * class="javakeyword">null</span> for that external, it will be skipped
     * (i.e. left as is, unprocessed).
     * <p/>
     * Note: this routine requires repository access.
     * 
     * @param sources
     *            array of copy sources
     * @param dst
     *            destination url
     * @param isMove
     *            if <span class="javakeyword">true</span>, then it will be a
     *            move operation (delete, then add with history)
     * @param makeParents
     *            if <span class="javakeyword">true</span>, creates non-existent
     *            parent directories as well
     * @param failWhenDstExists
     *            controls whether to fail or not if <code>dst</code> already
     *            exists
     * @param commitMessage
     *            commit log message
     * @param revisionProperties
     *            custom revision properties
     * @return information about the new committed revision
     * @throws SVNException
     * @since 1.2, SVN 1.5
     */
    public SVNCommitInfo doCopy(SVNCopySource[] sources, SVNURL dst, boolean isMove, boolean makeParents, boolean failWhenDstExists, String commitMessage, SVNProperties revisionProperties)
            throws SVNException {
        return doCopy(sources, dst, isMove, makeParents, failWhenDstExists, false, null, commitMessage, revisionProperties);
    }

    public SVNCommitInfo doCopy(SVNCopySource[] sources, SVNURL dst, boolean isMove, boolean makeParents, boolean failWhenDstExists, boolean pinExternals, Map<SvnTarget, List<SVNExternal>> externalsToPin, String commitMessage, SVNProperties revisionProperties)
            throws SVNException {
        if (sources.length > 1 && failWhenDstExists) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_MULTIPLE_SOURCES_DISALLOWED);
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
        sources = expandCopySources(sources);
        if (sources.length == 0) {
            return SVNCommitInfo.NULL;
        }
        try {
            return setupCopy(sources, new SVNPath(dst.toString(), false, false), isMove, makeParents, pinExternals, externalsToPin, commitMessage, revisionProperties, getCommitHandler(), getCommitParameters(), getExternalsHandler());
        } catch (SVNException e) {
            SVNErrorCode err = e.getErrorMessage().getErrorCode();
            if (!failWhenDstExists && sources.length == 1 && (err == SVNErrorCode.ENTRY_EXISTS || err == SVNErrorCode.FS_ALREADY_EXISTS)) {
                SVNCopySource source = sources[0];
                String baseName = source.getName();
                if (!source.isURL()) {
                    baseName = SVNEncodingUtil.uriEncode(baseName);
                }
                try {
                    return setupCopy(sources, new SVNPath(dst.appendPath(baseName, true).toString(), false, false), isMove, makeParents, pinExternals, externalsToPin, commitMessage, revisionProperties, getCommitHandler(),
                            getCommitParameters(), getExternalsHandler());
                } catch (SVNException second) {
                    throw second;
                }
            }
            throw e;
        }
    }

    /**
     * Converts a disjoint working copy to a copied one.
     * <p/>
     * Note: this routine does not require repository access. However if it's
     * performed on an old format working copy where repository root urls were
     * not written, the routine will connect to the repository to fetch the
     * repository root url.
     * 
     * @param nestedWC
     *            the root of the working copy located in another working copy
     *            (disjoint wc)
     * @throws SVNException
     *             in the following cases:
     *             <ul>
     *             <li/>exception with {@link SVNErrorCode#UNSUPPORTED_FEATURE}
     *             error code - if <code>nestedWC</code> is either not a
     *             directory, or has no parent at all; if the current local
     *             filesystem parent of <code>nestedWC</code> is actually a
     *             child of it in the repository <li/>exception with
     *             {@link SVNErrorCode#ENTRY_EXISTS} error code - if <code>
     *             nestedWC</code> is not a disjoint working copy, i.e. there is
     *             already a versioned item under the parent path of <code>
     *             nestedWC</code>; if <code>nestedWC</code> is not in the
     *             repository yet (has got a schedule for addition flag) <li/>
     *             exception with {@link SVNErrorCode#WC_INVALID_SCHEDULE} error
     *             code - if <code>nestedWC</code> is not from the same
     *             repository as the parent directory; if the parent of <code>
     *             nestedWC</code> is scheduled for deletion; if <code>nestedWC
     *             </code> is scheduled for deletion <li/>
     *             </ul>
     * @since 1.2.0
     */
    public void doCopy(File nestedWC) throws SVNException {
        copyDisjointWCToWC(nestedWC);
    }
}
