package org.tmatesoft.svn.core.wc2;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

import org.tmatesoft.sqljet.core.internal.SqlJetPagerJournalMode;
import org.tmatesoft.svn.core.ISVNCanceller;
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.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.wc.ISVNEventHandler;
import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.ISVNRepositoryPool;
import org.tmatesoft.svn.core.wc.SVNEvent;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * Base class for all Svn* operations. 
 * Encapsulates mostly used parameters, operation state, different methods used by implementations.
 * 
 * <p/>
 * Those parameters includes:
 * <ul>
 * <li>operation's target(s)</li>
 * <li>operation's revision</li>
 * <li>operation's depth</li>
 * <li>operation's changelists</li>
 * <li>whether to sleep after operation fails</li>
 * </ul>
 * 
 * <p/>
 * Those methods are:
 * <ul>
 * <li>base implementation of <code>run</code> method, starts the operation execution</li>
 * <li>methods for access to the factory that created the object and its options, event handler, canceler</li>
 * <li>variety of methods for getting, setting, recognition operation's targets</li>
 * <li>cancel the operation</li>
 * <li>access to the authentication manager</li>
 * </ul>
 * 
 * @author TMate Software Ltd.
 * @version 1.7
 * @param <V> type of returning value in {@link #run()} method
 */
public class SvnOperation<V> implements ISvnOperationOptionsProvider {
    
    private SVNDepth depth;
    private Collection<SvnTarget> targets;
    private SVNRevision revision;
    private Collection<String> changelists;
    private SvnOperationFactory operationFactory;
    private boolean isSleepForTimestamp;
    
    private SqlJetPagerJournalMode sqliteJournalMode;
    
    private volatile boolean isCancelled;
    
    protected SvnOperation(SvnOperationFactory factory) {
        this.operationFactory = factory;
        initDefaults();
    }

    /**
     * Gets the event handler for the operation, provided by {@link SvnOperationFactory#getEventHandler()}. This event handler will be
     * dispatched {@link SVNEvent} objects to provide detailed information about
     * actions and progress state of version control operations performed by
     * <code>run()</code> method of <code>SVN*</code> operation classes. 
     * 
     * @return handler for events
     * @see ISVNEventHandler
     */
    public ISVNEventHandler getEventHandler() {
        return getOperationFactory().getEventHandler();
    }

    /**
     * Gets operation's options, provided by {@link SvnOperationFactory#getOptions()}.
     * 
     * @return options of the operation
     */
    public ISVNOptions getOptions() {
        return getOperationFactory().getOptions();
    }
    
    protected void initDefaults() {
        setDepth(SVNDepth.UNKNOWN);
        setSleepForTimestamp(true);
        setRevision(SVNRevision.UNDEFINED);
        this.targets = new ArrayList<SvnTarget>();
    }
    
    /**
     * Sets one target of the operation.
     *  
     * @param target target of the operation
     * @see SvnTarget
     */
    public void setSingleTarget(SvnTarget target) {
        this.targets = new ArrayList<SvnTarget>();
        if (target != null) {
            this.targets.add(target);
        }
    }

    /**
     * Adds one target to the operation's targets.
     * 
     * @param target target of the operation
     * @see SvnTarget
     */
    public void addTarget(SvnTarget target) {
        this.targets.add(target);
    }
    
    /**
     * Returns all targets of the operation.
     * 
     * @return targets of the operation
     * @see SvnTarget
     */
    public Collection<SvnTarget> getTargets() {
        return Collections.unmodifiableCollection(targets);
    }
    
    /**
     * Returns first target of the operation.
     * 
     * @return first target of the operation
     * @see SvnTarget
     */
    public SvnTarget getFirstTarget() {
        return targets != null && !targets.isEmpty() ? targets.iterator().next() : null;
    }
    
    /**
     * Sets the limit of the operation by depth.
     * 
     * @param depth depth of the operation
     */
    public void setDepth(SVNDepth depth) {
        this.depth = depth;
    }
    
    /**
     * Gets the limit of the operation by depth.
     * 
     * @return depth of the operation
     */
    public SVNDepth getDepth() {
        return depth;
    }
    
    /**
     * Sets revision of the operation. 
     * In most cases if revision equals {@link SVNRevision#UNDEFINED}, the operation's revision will be {@link SVNRevision#WORKING}
     * if target(s) are local; it will be will be {@link SVNRevision#HEAD} it targets are remote.
     * 
     * @param revision revision of the operation
     */
    public void setRevision(SVNRevision revision) {
        this.revision = revision;
    }

    /**
     * Gets revision to operate on.
     * 
     * @return revision of the operation
     * @see #setRevision(SVNRevision)
     */
    public SVNRevision getRevision() {
        return revision;
    }
    
    /**
     * Sets changelists to operate only on members of.
     * 
     * @param changelists changelists of the operation
     */
    public void setApplicalbeChangelists(Collection<String> changelists) {
        this.changelists = changelists;
    }
    
    /**
     * Gets changelists to operate only on members of.
     * 
     * @return changelists of the operation
     */
    public Collection<String> getApplicableChangelists() {
        if (this.changelists == null || this.changelists.isEmpty()) {
            return null;
        }
        return Collections.unmodifiableCollection(this.changelists);
    }
    
    /**
     * Gets the factory that created the operation.
     * 
     * @return creation factory of the operations
     */
    public SvnOperationFactory getOperationFactory() {
        return this.operationFactory;
    }
    
    /**
     * Gets whether or not the operation has local targets.
     * 
     * @return <code>true</code> if the operation has local targets, otherwise <code>false</code>
     */
    public boolean hasLocalTargets() {
        for (SvnTarget target : getTargets()) {
            if (target.isLocal()) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Gets whether or not the operation has remote targets.
     * 
     * @return <code>true</code> if the operation has remote targets, otherwise <code>false</code>
     */
    public boolean hasRemoteTargets() {
        for (SvnTarget target : getTargets()) {
            if (!target.isLocal()) {
                return true;
            }
        }
        return false;
    }
    
    protected void ensureEnoughTargets() throws SVNException {
        int targetsCount = getTargets().size();
        
        if (targetsCount < getMinimumTargetsCount()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET,
                    "Wrong number of targets has been specified ({0}), at least {1} is required.",
                    new Object[] {targetsCount, getMinimumTargetsCount()},
                    SVNErrorMessage.TYPE_ERROR);
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (targetsCount > getMaximumTargetsCount()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET,
                    "Wrong number of targets has been specified ({0}), no more that {1} may be specified.",
                    new Object[] {targetsCount,
                            getMaximumTargetsCount()},
                    SVNErrorMessage.TYPE_ERROR);
            SVNErrorManager.error(err, SVNLogType.WC);
        }
    }
    
    protected int getMinimumTargetsCount() {
        return 1;
    }

    protected int getMaximumTargetsCount() {
        return 1;
    }
    
    /**
     * Cancels the operation. Execution of operation will be stopped at the next point of checking <code>isCancelled</code> state.
     * If canceler is set, {@link ISVNCanceller#checkCancelled()} is called, 
     * otherwise {@link org.tmatesoft.svn.core.SVNCancelException} is raised at the point of checking <code>isCancelled</code> state.
     */
    public void cancel() {
        isCancelled = true;
    }
    
    /**
     * Gets whether or not the operation is cancelled.
     * 
     * @return <code>true</code> if the operation is cancelled, otherwise <code>false</code>
     */
    public boolean isCancelled() {
        return isCancelled;
    }
    
    /**
     * Executes the operation.
     * 
     * @return result depending on operation type
     * @throws SVNException
     */
    @SuppressWarnings("unchecked")
    public V run() throws SVNException {
        ensureArgumentsAreValid();
        return (V) getOperationFactory().run(this);
    }
    
    protected void ensureArgumentsAreValid() throws SVNException {
        ensureEnoughTargets();
        ensureHomohenousTargets();
    }
    
    protected boolean needsHomohenousTargets() {
        return true;
    }
    
    protected void ensureHomohenousTargets() throws SVNException {
        if (getTargets().size() <= 1) {
            return;
        }
        if (!needsHomohenousTargets()) {
            return;
        }
        if (hasLocalTargets() && hasRemoteTargets()) {
            SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.ILLEGAL_TARGET, "Cannot mix repository and working copy targets"), SVNLogType.WC);
        }
    }

    /**
     * Gets the operation's pool of repositories, provided by {@link SvnOperationFactory#getRepositoryPool()}.
     * 
     * @return pool of repositories
     */
    public ISVNRepositoryPool getRepositoryPool() {
        return getOperationFactory().getRepositoryPool();
    }

    /**
     * Gets operation's authentication manager, provided by {@link SvnOperationFactory#getAuthenticationManager()}.
     * 
     * @return authentication manager
     */
    public ISVNAuthenticationManager getAuthenticationManager() {
        return getOperationFactory().getAuthenticationManager();
    }

    /**
     * Gets the cancel handler of the operation, provided by {@link SvnOperationFactory#getCanceller() }.
     * 
     * @return cancel handler
     * @see #cancel()
     */
    public ISVNCanceller getCanceller() {
        return getOperationFactory().getCanceller();
    }

    /**
     * Gets whether or not the operation should sleep after if fails.
     * 
     * @return <code>true</code> if the operation should sleep, otherwise <code>false</code>
     * @see SvnUpdate
     * @since 1.7
     */
    public boolean isSleepForTimestamp() {
        return isSleepForTimestamp;
    }

    /**
     * Sets whether or not the operation should sleep after if fails.
     * 
     * @param isSleepForTimestamp <code>true</code> if the operation should sleep, otherwise <code>false</code>
     * @see SvnUpdate
     * @since 1.7
     */
    public void setSleepForTimestamp(boolean isSleepForTimestamp) {
        this.isSleepForTimestamp = isSleepForTimestamp;
    }

    /**
     * Analyzes the targets and returns whether or not operation has at least one file in targets.
     * 
     * @return <code>true</code> if operation has at least one file in targets, otherwise <code>false</code>
     */
    public boolean hasFileTargets() {
        for (SvnTarget target : getTargets()) {
            if (target.isFile()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Gets whether or not to use parent working copy format.
     * 
     * @return <code>true</code> if parent working copy format should be used, otherwise <code>false</code>
     */
    public boolean isUseParentWcFormat() {
        return false;
    }

    /**
     * Gets whether the operation changes working copy
     * @return <code>true</code> if the operation changes the working copy, otherwise <code>false</code>
     */
    public boolean isChangesWorkingCopy() {
        return true;
    }
    
    public SqlJetPagerJournalMode getSqliteJournalMode() {
        return sqliteJournalMode;
    }
    
    public void setSqliteJournalMode(SqlJetPagerJournalMode journalMode) {
        sqliteJournalMode = journalMode; 
    }

    protected File getOperationalWorkingCopy() {
        if (hasFileTargets()) {
            return getFirstTarget().getFile();
        }
        return null;
    }
}
