package com.atlassian.bitbucket.scm;

import com.atlassian.bitbucket.hook.HookHandler;
import com.atlassian.bitbucket.repository.Branch;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.Tag;
import com.atlassian.bitbucket.scm.bulk.PluginBulkContentCommandFactory;
import com.atlassian.bitbucket.scm.compare.PluginCompareCommandFactory;
import com.atlassian.bitbucket.scm.event.ScmStatusChangedEvent;
import com.atlassian.bitbucket.scm.hook.PluginHookHandlerFactory;
import com.atlassian.bitbucket.scm.mirror.PluginMirrorCommandFactory;
import com.atlassian.bitbucket.scm.pull.PluginPullRequestCommandFactory;
import com.atlassian.bitbucket.scm.ref.PluginRefCommandFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import java.util.Set;
import java.util.function.Function;

/**
 * Describes a <i>pluggable</i> extension point for implementing custom SCMs.
 * <p>
 * In order for an SCM to be usable by the system, it <i>must</i> implement:
 * <ul>
 *     <li>{@link #getCommandFactory()}</li>
 *     <li>{@link #getFeatures() getFeatures()}</li>
 *     <li>{@link #getId()}</li>
 *     <li>{@link #getName()}</li>
 *     <li>{@link #getSize(Repository)}</li>
 *     <li>{@link #getStatus()}</li>
 *     <li>{@link #isEmpty(Repository)}</li>
 * </ul>
 * It <i>may also</i> implement the following to enable additional system functionality:
 * <ul>
 *     <li>{@link #getBulkContentCommandFactory()}</li>
 *     <li>{@link #getCompareCommandFactory()}</li>
 *     <li>{@link #getExtendedCommandFactory()}</li>
 *     <li>{@link #getHookHandlerFactory()}</li>
 *     <li>{@link #getMirrorCommandFactory()}</li>
 *     <li>{@link #getPullRequestCommandFactory()}</li>
 *     <li>{@link #getRefCommandFactory()}</li>
 * </ul>
 * It <i>may also</i> implement the following to provide extra functionality for other plugin developers:
 * <ul>
 *     <li>{@link #getCommandBuilderFactory()}</li>
 * </ul>
 * Further details about each of these, and how to implement them correctly, is divided between method documentation
 * and documentation on the interfaces being returned. SCM plugins are complicated to implement, so it is encouraged
 * that developers read the documentation provided <i>thoroughly</i> to ensure they do not violate invariants that
 * allow the system to use its SCMs correctly.
 * <p>
 * SCMs which support additional functionality, or have custom factory types, are encouraged to provide a sub-
 * interface extending from this one exposing that functionality. For example:
 * <code><pre>
 *     //Overrides the builder(Repository) method using a covariant return type to return a builder supporting both
 *     //type-safe and free-form commands, instead of {@link ScmCommandBuilder}
 *     public interface MyScmCommandBuilderFactory extends PluginCommandBuilderFactory {
 *         //See the documentation for {@link PluginCommandBuilderFactory}
 *     }
 *
 *     //Overrides methods using covariant return types to return an enhanced CancelableCommands instead of
 *     //{@link Command Commands}
 *     public interface MyScmCommandFactory extends PluginCommandFactory {
 *         //See the documentation for {@link PluginCommandFactory}
 *     }
 *
 *     //Overrides methods using covariant return types to return custom versions of the {@link PluginCommandFactory},
 *     //{@link PluginCommandBuilderFactory} and {@link PluginPullRequestCommandFactory} interfaces
 *     public interface MyScm extends Scm {
 *         //Supports both free-form and type-safe builders
 *         &#064;Nonnull
 *         &#064;Override
 *         MyScmCommandBuilderFactory getCommandBuilderFactory();
 *
 *         //Uses enhanced CancelableCommands
 *         &#064;Nonnull
 *         &#064;Override
 *         MyScmCommandFactory getCommandFactory();
 *
 *         &#064;Nonnull
 *         &#064;Override
 *         MyScmPullRequestCommandFactory getPullRequestCommandFactory();
 *     }
 * </pre></code>
 * Notice that the {@link #getCommandBuilderFactory()} and {@link #getPullRequestCommandFactory()} have had their
 * annotations changed to {@code &#064;Nonnull}, indicating that {@code MyScm} <i>definitely</i> supports both
 * builders and pull requests. This allows other plugin developers to know the features supported by the custom SCM
 * so they can leverage them fully.
 * <p>
 * SCM plugins are encouraged to define a separate {@code component} for their {@code Scm} implementations, but they
 * are <i>required</i> to define an {@code scm} entry or the SCM will not be detected by the system. The following
 * example illustrates <i>both</i> directives:
 * <code><pre>
 *     &lt;!-- Note: These should be in the SCM <u>provider's</u> atlassian-plugin.xml --&gt;
 *     &lt;component key="myScm" class="com.example.DefaultMyScm" public="true"&gt;
 *         &lt;interface&gt;com.example.MyScm&lt;/interface&gt;
 *     &lt;/component&gt;
 *
 *     &lt;scm key="scm" class="bean:myScm"/&gt;
 * </pre></code>
 * The {@code public="true"} allows other plugins to import the component. This approach allows other plugin developers
 * to import the SCM directly and leverage its enhanced functionality with a {@code component-import} directive:
 * <code><pre>
 *     &lt;!-- Note: This should be in the SCM <u>consumer's</u> atlassian-plugin.xml --&gt;
 *     &lt;component-import key="myScm" interface="com.example.MyScm"/&gt;
 * </pre></code>
 * <p>
 * For SCMs which define a {@code component} for their {@code Scm}, it is <i>strongly</i> encouraged that they reference
 * that component for their {@code scm} directive, as shown above. This ensures a single instance of the {@code Scm} is
 * constructed for use by other plugins <i>and</i> by the system.
 * <p>
 * <b>Note</b>: Implementors are <i>strongly</i> encouraged to extend from {@link AbstractScm}. This interface
 * <i>will</i> change, over time, and any class implementing it directly will be broken by such changes. Extending
 * from the abstract class will help prevent such breakages.
 * <p>
 * <b>Note</b>: Implementations of this interface are required to be thread-safe. Implementations of related interfaces
 * such as {@link PluginCommandBuilderFactory}, {@link PluginCommandFactory} and {@link PluginPullRequestCommandFactory}
 * are not required to be thread-safe if distinct instances are returned for each call.
 * <p>
 * <b>Warning</b>: If a {@code component} directive is defined but the {@code scm} directive does not reference it and
 * instead defines {@code class="com.example.MyScm"}, <i>two</i> instances will be created. The one created with the
 * {@code component} directive may be imported by plugins (assuming it is {@code public}; otherwise, it may never be
 * used anywhere!) and the one created with the {@code scm} directive will be used by the system.
 *
 * @see AbstractScm
 */
@ThreadSafe
public interface Scm {

    /**
     * Retrieves a {@link PluginBulkContentCommandFactory}, used to create {@link Command commands} tailored for getting
     * content from a repository in bulk.
     * <p>
     * <b>Implementation Note</b>: This method is <i>optional</i>. SCMs which do not support bulk content retrieval may
     * return {@code null}.
     *
     * @return a command factory which will create commands operating on the specified repository, or {@code null} if the
     * SCM does not support bulk content retrieval
     * @see PluginBulkContentCommandFactory
     * @see ScmFeature#BULK_CONTENT
     * @since 4.2
     */
    @Nullable
    default PluginBulkContentCommandFactory getBulkContentCommandFactory() {
        return null;
    }

    /**
     * Retrieves a {@link PluginCommandBuilderFactory}, used to create {@link ScmCommandBuilder command builders} for
     * custom SCM commands.
     * <p>
     * <b>Implementation Note</b>: This method is <i>optional</i>. SCMs which do not support a {@link ScmCommandBuilder}
     * may return {@code null}.
     *
     * @return a command builder factory, or {@code null} if the SCM does not support command builders
     * @see PluginCommandBuilderFactory
     * @see ScmFeature#COMMAND_BUILDERS
     */
    @Nullable
    default PluginCommandBuilderFactory getCommandBuilderFactory() {
        return null;
    }

    /**
     * Retrieves a {@link PluginCommandFactory}, used to create {@link Command commands} for performing standard SCM
     * operations such as retrieving commits and viewing diffs. The commands created by the returned factory are used
     * to support normal system functionality; as a result, SCMs are <i>required</i> to provide an implementation.
     * <p>
     * <b>Implementation Note</b>: This method is <i>required</i> and may not return {@code null}.
     *
     * @return a command factory, providing access to required SCM functionality
     * @see PluginCommandFactory
     */
    @Nonnull
    PluginCommandFactory getCommandFactory();

    /**
     * Retrieves a {@link PluginCompareCommandFactory}, used to create {@link Command commands} tailored for comparing
     * refs.
     * <p>
     * <b>Implementation Note</b>: This method is <i>optional</i>. SCMs which do not support comparing refs may return
     * {@code null}.
     *
     * @return a compare command factory, or {@code null} if the SCM does not support it
     * @see PluginCompareCommandFactory
     * @see ScmFeature#COMPARE
     */
    @Nullable
    default PluginCompareCommandFactory getCompareCommandFactory() {
        return null;
    }

    /**
     * Retrieves an {@link PluginExtendedCommandFactory optional command factory}, which enables additional system
     * features for SCMs which implement its commands. Each optional command is associated with an {@link ScmFeature
     * SCM feature}, and SCMs are free to implement any subset of the factory's methods. If the SCM does not implement
     * <i>any</i> optional features, the returned factory may be {@code null}. However, SCM implementors are encouraged
     * to implement as many optional commands as they can.
     *
     * @return a command factory providing optional, extended functionality for this SCM, or {@code null} if the SCM
     *         does not support any optional commands
     * @since 4.6
     */
    @Nullable
    @SuppressWarnings("deprecation")
    default PluginExtendedCommandFactory getExtendedCommandFactory() {
        return new PluginExtendedCommandFactory() {

            @Override
            public Command<Void> fork(@Nonnull Repository repository, @Nonnull ForkCommandParameters parameters) {
                return getIfSupported(ScmFeature.FORK,
                        commandFactory -> commandFactory.fork(parameters.getFork(), repository));
            }

            @Override
            public Command<Branch> merge(@Nonnull Repository repository, @Nonnull MergeCommandParameters parameters) {
                return getIfSupported(ScmFeature.MERGE, commandFactory -> commandFactory.merge(repository, parameters));
            }

            @Override
            public Command<Void> updateDefaultBranch(@Nonnull Repository repository,
                                                     @Nonnull UpdateDefaultBranchCommandParameters parameters) {
                return getIfSupported(ScmFeature.UPDATE_DEFAULT_BRANCH,
                        commandFactory -> commandFactory.updateDefaultBranch(repository, parameters.getBranchId()));
            }

            private <T> Command<T> getIfSupported(ScmFeature feature,
                                                  Function<PluginCommandFactory, Command<T>> supplier) {
                if (getFeatures().contains(feature)) {
                    try {
                        return supplier.apply(getCommandFactory());
                    } catch (UnsupportedOperationException e) {
                        //Strange; the SCM says it supports the feature, but trying to get the command still threw...
                    }
                }

                return null;
            }
        };
    }

    /**
     * Retrieves the {@link ScmFeature features} that are supported by this SCM.
     * <p>
     * Since the system relies on optional features to provide some functionality, like branch compare and pull
     * requests, it relies on this set to disable functionality that requires features the SCM doesn't provide.
     * Similarly, plugin developers can use the returned features to control which optional features they try
     * to use, rather than checking for {@code null} or handling {@code UnsupportedOperationException}s.
     *
     * @return the set of optional features this SCM supports, which may be empty but never {@code null}
     */
    @Nonnull
    Set<ScmFeature> getFeatures();

    /**
     * Retrieves a {@link PluginHookHandlerFactory}, used to create {@link HookHandler}
     *
     * @return a hook handler factory, or {@code null} if the SCM does not support hooks
     * @see PluginHookHandlerFactory
     * @see ScmFeature#HOOKS
     */
    @Nullable
    default PluginHookHandlerFactory getHookHandlerFactory() {
        return null;
    }

    /**
     * Retrieves a unique identifier for the SCM that is provided by the plugin.
     * <p>
     * Identifiers should be unique to the SCM's <i>type</i>. Using the name of the SCM's binary is a good approach. For
     * example, a plugin implementing support for git might return {@code "git"} as its identifier. This approach allows
     * the system to detect when multiple plugins are installed which implement git support, potentially leading to
     * unexpected behaviour from some repositories.
     * <p>
     * <b>Implementation Note</b>: This method is <i>required</i> and may not return {@code null}.
     *
     * @return a unique identifier for the SCM, preferably based on its binary
     */
    @Nonnull
    String getId();

    /**
     * Retrieves a set of {@link PluginMergeStrategy merge strategies} supported by the SCM.
     * <p>
     * If the SCM supports {@link #getPullRequestCommandFactory() pull requests} and selectable merge strategies, the
     * system will use the returned {@link PluginMergeStrategy strategies} to allow administrators to configure the
     * default and enabled strategies for the "Merge" dialog.
     * <p>
     * SCMs which support {@link ScmFeature#MERGE_STRATEGIES selectable merge strategies} are required to return
     * <i>at least</i> one {@link PluginMergeStrategy}. <i>SCMs which support multiple merge strategies must
     * support the same set of strategies for merge commands and pull requests.</i> SCMs <i>must not</i> support
     * different strategies for merge commands and pull requests.
     * <p>
     * <b>Implementation Notes</b>: This method is <i>optional</i>. SCMs which do not support selectable merge
     * strategies, or which don't support the {@link PluginExtendedCommandFactory#merge merge command} or
     * {@link #getPullRequestCommandFactory() pull requests}, may return {@code null}.
     *
     * @return the merge strategies supported by this SCM, or {@code null} if the SCM doesn't support selectable
     *         strategies
     * @see ScmFeature#MERGE_STRATEGIES
     * @since 4.9
     */
    @Nullable
    default PluginMergeStrategies getMergeStrategies() {
        return null;
    }

    /**
     * Retrieves a {@link PluginMirrorCommandFactory}, used to create {@link Command commands} tailored for mirroring
     * repositories.
     * <p>
     * <b>Implementation Note</b>: This method is <i>optional</i>. SCMs which do not support mirroring may return
     * {@code null}. {@link Repository Repositories} using such SCMs will not be able to mirror repositories.
     *
     * @return a command factory which will create commands used to mirror repositories, or {@code null} if the SCM
     *         does not support mirroring
     * @see PluginMirrorCommandFactory
     * @see ScmFeature#MIRRORS
     * @since 4.1
     */
    @Nullable
    default PluginMirrorCommandFactory getMirrorCommandFactory() {
        return null;
    }

    /**
     * Retrieves a well-known name for the SCM that is provided by the plugin.
     * <p>
     * Most SCMs use binaries which do not match the well-known name of the SCM. For example, Subversion's binary is
     * {@code svn}, and Mercurial's is {@code hg}. This method should return the SCM's well-known name, suitable for
     * display in a user interface.
     * <p>
     * <b>Implementation Note</b>: This method is <i>required</i> and may not return {@code null}.
     *
     * @return the SCM's well-known name
     */
    @Nonnull
    String getName();

    /**
     * Retrieves a {@link PluginPullRequestCommandFactory}, used to create {@link Command commands} tailored for use
     * supporting pull requests. {@link Repository Repositories} using an SCM which implements this method will have
     * the ability to create, view and merge pull requests.
     * <p>
     * <b>Implementation Note</b>: This method is <i>optional</i>. SCMs which do not support pull requests may return
     * {@code null}. {@link Repository Repositories} using such SCMs will not be able to create pull requests.
     *
     * @return a pull request command factory, or {@code null} if the SCM does not support pull requests
     * @see PluginPullRequestCommandFactory
     * @see ScmFeature#PULL_REQUESTS
     */
    @Nullable
    default PluginPullRequestCommandFactory getPullRequestCommandFactory() {
        return null;
    }

    /**
     * Retrieves a {@link PluginRefCommandFactory}, used to create {@link Command commands} tailored for creating
     * {@link Branch branches} and {@link Tag tags}.
     * <p>
     * <b>Implementation Note</b>: This method is <i>optional</i>. SCMs which do not support mutable refs may return
     * {@code null}. {@link Repository Repositories} using such SCMs will not be able create
     * {@link Branch branches} and {@link Tag tags}.
     *
     * @see ScmFeature#REFS
     */
    @Nullable
    default PluginRefCommandFactory getRefCommandFactory() {
        return null;
    }

    /**
     * Calculates the size of the specified repository in bytes.
     * <p>
     * If the provided {@link Repository} does not exist, implementors should return {@code 0} rather than throwing
     * an exception. Beyond that, SCM implementors are free to calculate size as they see fit. <i>The SCM contract
     * does</i> not <i>require implementors to calculate an exact size.</i> Some SCMs may store their files such that
     * there is a very fast way to <i>approximate</i> the repository's size while calculating an exact size would be
     * extremely expensive. For such SCMs, implementors are <i>strongly encouraged</i> to use an approximation.
     *
     * @param repository the repository whose size should be calculated
     * @return the size of the specified repository in bytes
     */
    long getSize(@Nonnull Repository repository);

    /**
     * Retrieves the current {@link ScmStatus status} for the SCM.
     * <p>
     * {@link ScmStatus#isAvailable() Available} SCMs are expected to be able to process requests. However, there may
     * be situations in which an SCM is unavailable. For example, if the plugin relies on a binary, or common library,
     * which is not installed on the host system, the SCM may be unavailable. When an SCM is unavailable, a message
     * explaining the reason <i>must</i> be provided on the returned {@link ScmStatus}.
     * <p>
     * An SCM's status may be capable of changing over time. However, where possible, it is best for SCMs to determine
     * their status up front. An {@link ScmStatusChangedEvent ScmStatusChangedEvent}
     * should be raised if an SCM's status changes at runtime, but is not required. However, the system may not detect
     * the change to the SCM's status if it does not raise an event.
     * <p>
     * <b>Implementation Note</b>: This method is <i>required</i> and may not return {@code null}.
     *
     * @return the current status of the SCM
     * @see ScmStatus
     */
    @Nonnull
    ScmStatus getStatus();

    /**
     * Retrieves a flag indicating whether the specified {@link Repository} is empty, from the perspective of the SCM
     * implementation. Potential criteria for determining whether a repository is empty include:
     * <ul>
     *     <li>Whether it has branches</li>
     *     <li>Whether it has tags</li>
     *     <li>Whether it has commits</li>
     * </ul>
     * The contract for {@code Scm.isEmpty(Repository)} only mandates that a newly-created repository <i>must</i> be
     * considered empty.
     * <p>
     * How to determine whether a repository is "empty" depends on the SCM, as most SCMs create housekeeping files in
     * their repositories to allow them to manage state. As a result, the system cannot simply test for the absence of
     * any files. Instead, it calls this method to allow the SCM to perform the test.
     * <p>
     * <b>Implementation Note</b>: This method is <i>required</i>.
     *
     * @param repository the repository to check
     * @return {@code true} if the repository is empty; otherwise, {@code false}
     */
    boolean isEmpty(@Nonnull Repository repository);
}
