/*
 * The MIT License
 *
 * Copyright 2014 Jesse Glick.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package jenkins.model;

import hudson.Util;
import hudson.model.Action;
import hudson.model.BuildableItem;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.ParametersAction;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Queue;
import hudson.model.Run;
import hudson.model.queue.QueueTaskFuture;
import hudson.search.SearchIndexBuilder;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.AlternativeUiTextProvider;
import hudson.views.BuildButtonColumn;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.servlet.ServletException;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import jenkins.util.TimeDuration;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;

/**
 * Allows a {@link Job} to make use of {@link ParametersDefinitionProperty} and be scheduled in various ways.
 * Stateless so there is no need to keep an instance of it in a field.
 * @since 1.556
 */
@SuppressWarnings("unchecked") // AbstractItem.getParent does not correctly override; scheduleBuild2 inherently untypable
public abstract class ParameterizedJobMixIn<JobT extends Job<JobT, RunT> & ParameterizedJobMixIn.ParameterizedJob & Queue.Task, RunT extends Run<JobT, RunT> & Queue.Executable> {
    
    protected abstract JobT asJob();

    /** @see BuildableItem#scheduleBuild() */
    @SuppressWarnings("deprecation")
    public final boolean scheduleBuild() {
        return scheduleBuild(asJob().getQuietPeriod(), new Cause.LegacyCodeCause());
    }

    /** @see BuildableItem#scheduleBuild(Cause) */
    public final boolean scheduleBuild(Cause c) {
        return scheduleBuild(asJob().getQuietPeriod(), c);
    }

    /** @see BuildableItem#scheduleBuild(int) */
    @SuppressWarnings("deprecation")
    public final boolean scheduleBuild(int quietPeriod) {
        return scheduleBuild(quietPeriod, new Cause.LegacyCodeCause());
    }

    /** @see BuildableItem#scheduleBuild(int, Cause) */
    public final boolean scheduleBuild(int quietPeriod, Cause c) {
        return scheduleBuild2(quietPeriod, c != null ? Collections.<Action>singletonList(new CauseAction(c)) : Collections.<Action>emptyList()) != null;
    }

    /**
     * Provides a standard implementation of an optional method of the same name in a {@link Job} type to schedule a build with the ability to wait for its result.
     * That job method is often used during functional tests ({@code JenkinsRule.assertBuildStatusSuccess}).
     * @param quietPeriod seconds to wait before starting (normally 0)
     * @param actions various actions to associate with the scheduling, such as {@link ParametersAction} or {@link CauseAction}
     * @return a handle by which you may wait for the build to complete (or just start); or null if the build was not actually scheduled for some reason
     */
    public final @CheckForNull QueueTaskFuture<RunT> scheduleBuild2(int quietPeriod, Action... actions) {
        Queue.Item i = scheduleBuild2(quietPeriod, Arrays.asList(actions));
        return i != null ? (QueueTaskFuture) i.getFuture() : null;
    }

    /**
     * Convenience method to schedule a build.
     * Useful for {@link Trigger} implementations, for example.
     * If you need to wait for the build to start (or finish), use {@link Queue.Item#getFuture}.
     * @param job a job which might be schedulable
     * @param quietPeriod seconds to wait before starting; use {@code -1} to use the job’s default settings
     * @param actions various actions to associate with the scheduling, such as {@link ParametersAction} or {@link CauseAction}
     * @return a newly created, or reused, queue item if the job could be scheduled; null if it was refused for some reason (e.g., some {@link Queue.QueueDecisionHandler} rejected it), or if {@code job} is not a {@link ParameterizedJob} or it is not {@link Job#isBuildable})
     * @since 1.621
     */
    public static @CheckForNull Queue.Item scheduleBuild2(final Job<?,?> job, int quietPeriod, Action... actions) {
        if (!(job instanceof ParameterizedJob)) {
            return null;
        }
        return new ParameterizedJobMixIn() {
            @Override protected Job asJob() {
                return job;
            }
        }.scheduleBuild2(quietPeriod == -1 ? ((ParameterizedJob) job).getQuietPeriod() : quietPeriod, Arrays.asList(actions));
    }

    @CheckForNull Queue.Item scheduleBuild2(int quietPeriod, List<Action> actions) {
        if (!asJob().isBuildable())
            return null;

        List<Action> queueActions = new ArrayList<Action>(actions);
        if (isParameterized() && Util.filter(queueActions, ParametersAction.class).isEmpty()) {
            queueActions.add(new ParametersAction(getDefaultParametersValues()));
        }
        return Jenkins.getInstance().getQueue().schedule2(asJob(), quietPeriod, queueActions).getItem();
    }

    private List<ParameterValue> getDefaultParametersValues() {
        ParametersDefinitionProperty paramDefProp = asJob().getProperty(ParametersDefinitionProperty.class);
        ArrayList<ParameterValue> defValues = new ArrayList<ParameterValue>();

        /*
         * This check is made ONLY if someone will call this method even if isParametrized() is false.
         */
        if(paramDefProp == null)
            return defValues;

        /* Scan for all parameter with an associated default values */
        for(ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions())
        {
           ParameterValue defaultValue  = paramDefinition.getDefaultParameterValue();

            if(defaultValue != null)
                defValues.add(defaultValue);
        }

        return defValues;
    }

    /**
     * A job should define a method of the same signature for use from {@link BuildButtonColumn}.
     */
    public final boolean isParameterized() {
        return asJob().getProperty(ParametersDefinitionProperty.class) != null;
    }

    /**
     * Schedules a new build command.
     * Create a method on your job with the same signature and delegate to this.
     */
    @SuppressWarnings("deprecation")
    public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
        if (delay == null) {
            delay = new TimeDuration(asJob().getQuietPeriod());
        }

        if (!asJob().isBuildable()) {
            throw HttpResponses.error(SC_CONFLICT, new IOException(asJob().getFullName() + " is not buildable"));
        }

        // if a build is parameterized, let that take over
        ParametersDefinitionProperty pp = asJob().getProperty(ParametersDefinitionProperty.class);
        if (pp != null && !req.getMethod().equals("POST")) {
            // show the parameter entry form.
            req.getView(pp, "index.jelly").forward(req, rsp);
            return;
        }

        hudson.model.BuildAuthorizationToken.checkPermission(asJob(), asJob().getAuthToken(), req, rsp);

        if (pp != null) {
            pp._doBuild(req, rsp, delay);
            return;
        }


        Queue.Item item = Jenkins.getInstance().getQueue().schedule2(asJob(), delay.getTime(), getBuildCause(asJob(), req)).getItem();
        if (item != null) {
            rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl());
        } else {
            rsp.sendRedirect(".");
        }
    }

    /**
     * Supports build trigger with parameters via an HTTP GET or POST.
     * Currently only String parameters are supported.
     * Create a method on your job with the same signature and delegate to this.
     */
    @SuppressWarnings("deprecation")
    public final void doBuildWithParameters(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
        hudson.model.BuildAuthorizationToken.checkPermission(asJob(), asJob().getAuthToken(), req, rsp);

        ParametersDefinitionProperty pp = asJob().getProperty(ParametersDefinitionProperty.class);
        if (!asJob().isBuildable()) {
            throw HttpResponses.error(SC_CONFLICT, new IOException(asJob().getFullName() + " is not buildable!"));
        }
        if (pp != null) {
            pp.buildWithParameters(req, rsp, delay);
        } else {
            throw new IllegalStateException("This build is not parameterized!");
        }
    }

    /**
     * Cancels a scheduled build.
     * Create a method on your job marked {@link RequirePOST} but with the same signature and delegate to this.
     */
    @RequirePOST
    public final void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
        asJob().checkPermission(Item.CANCEL);
        Jenkins.getInstance().getQueue().cancel(asJob());
        rsp.forwardToPreviousPage(req);
    }

    /**
     * Use from a {@link Job#makeSearchIndex} override.
     * @param sib the super value
     * @return the value to return
     */
    public final SearchIndexBuilder extendSearchIndex(SearchIndexBuilder sib) {
        if (asJob().isBuildable() && asJob().hasPermission(Item.BUILD)) {
            sib.add("build", "build");
        }
        return sib;
    }

    /**
     * Computes the build cause, using RemoteCause or UserCause as appropriate.
     */
    @Restricted(NoExternalUse.class)
    public static final CauseAction getBuildCause(ParameterizedJob job, StaplerRequest req) {
        Cause cause;
        @SuppressWarnings("deprecation")
        hudson.model.BuildAuthorizationToken authToken = job.getAuthToken();
        if (authToken != null && authToken.getToken() != null && req.getParameter("token") != null) {
            // Optional additional cause text when starting via token
            String causeText = req.getParameter("cause");
            cause = new Cause.RemoteCause(req.getRemoteAddr(), causeText);
        } else {
            cause = new Cause.UserIdCause();
        }
        return new CauseAction(cause);
    }

    /**
     * Allows customization of the human-readable display name to be rendered in the <i>Build Now</i> link.
     * @see #getBuildNowText
     * @since 1.624
     */
    public static final AlternativeUiTextProvider.Message<ParameterizedJob> BUILD_NOW_TEXT = new AlternativeUiTextProvider.Message<ParameterizedJob>();

    /**
     * Suggested implementation of {@link ParameterizedJob#getBuildNowText}.
     * Uses {@link #BUILD_NOW_TEXT}.
     */
    public final String getBuildNowText() {
        return isParameterized() ? AlternativeUiTextProvider.get(BUILD_NOW_TEXT, asJob(), Messages.ParameterizedJobMixIn_build_with_parameters())
                : AlternativeUiTextProvider.get(BUILD_NOW_TEXT, asJob(), Messages.ParameterizedJobMixIn_build_now());
    }

    /**
     * Checks for the existence of a specific trigger on a job.
     * @param <T> a trigger type
     * @param job a job
     * @param clazz the type of the trigger
     * @return a configured trigger of the requested type, or null if there is none such, or {@code job} is not a {@link ParameterizedJob}
     * @since 1.621
     */
    public static @CheckForNull <T extends Trigger<?>> T getTrigger(Job<?,?> job, Class<T> clazz) {
        if (!(job instanceof ParameterizedJob)) {
            return null;
        }
        for (Trigger<?> t : ((ParameterizedJob) job).getTriggers().values()) {
            if (clazz.isInstance(t)) {
                return clazz.cast(t);
            }
        }
        return null;
    }

    /**
     * Marker for job using this mixin.
     */
    public interface ParameterizedJob extends hudson.model.Queue.Task, hudson.model.Item {

        @SuppressWarnings("deprecation")
        @CheckForNull hudson.model.BuildAuthorizationToken getAuthToken();

        int getQuietPeriod();

        String getBuildNowText();

        /**
         * Gets currently configured triggers.
         * You may use {@code <p:config-trigger/>} to configure them.
         * @return a map from trigger kind to instance
         * @see #getTrigger
         */
        Map<TriggerDescriptor,Trigger<?>> getTriggers();

    }

}
