/*
 * The MIT License
 *
 * Copyright 2015 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.tasks;

import hudson.AbortException;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.console.ConsoleLogFilter;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildWrapper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
 * A generalization of {@link BuildWrapper} that, like {@link SimpleBuildStep}, may be called at various points within a build.
 * <p>Such a build wrapper would typically be written according to make few assumptions about how it is being used.
 * Some hints about this refactoring:
 * <ul>
 * <li>Replace {@link AbstractBuild#getWorkspace} with the provided path.
 * <li>Replace {@link AbstractBuild#getProject} with {@link Run#getParent}.
 * <li>Use {@link FilePath#toComputer} rather than {@link Computer#currentComputer}.
 * <li>Do not bother with {@link AbstractBuild#getBuildVariables} if you are not passed an {@link AbstractBuild} (treat it like an empty map).
 * <li>The {@link Disposer} must be safely serializable. This means it should be a {@code static} class if nested, and define a {@code serialVersionUID}.
 * </ul>
 * @since 1.599
 */
@SuppressWarnings("rawtypes") // inherited
public abstract class SimpleBuildWrapper extends BuildWrapper {

    /**
     * Called when a segment of a build is started that is to be enhanced with this wrapper.
     * @param context a way of collecting modifications to the environment for nested steps
     * @param build a build being run
     * @param workspace a workspace of the build
     * @param launcher a way to start commands
     * @param listener a way to report progress
     * @param initialEnvironment the environment variables set at the outset
     * @throws IOException if something fails; {@link AbortException} for user errors
     * @throws InterruptedException if setup is interrupted
     */
    public abstract void setUp(Context context, Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException;

    /**
     * Parameter passed to {@link #setUp} to allow an implementation to specify its behavior after the initial setup.
     */
    public static final class Context {
        private Disposer disposer;
        private final Map<String,String> env = new HashMap<String,String>();
        public @Nonnull Map<String,String> getEnv() {
            return env;
        }
        /**
         * Specify an environment variable override to apply to processes launched within the block.
         * If unspecified, environment variables will be inherited unmodified.
         * @param key handles the special {@code PATH+SOMETHING} syntax as in {@link EnvVars#override}
         */
        public void env(String key, String value) {
            if (env.containsKey(key)) {
                throw new IllegalStateException("just one binding for " + key);
            }
            env.put(key, value);
        }
        public @CheckForNull Disposer getDisposer() {
            return disposer;
        }
        /**
         * Specify an action to take when the block ends.
         * If not specified, nothing special happens.
         */
        public void setDisposer(@Nonnull Disposer disposer) {
            if (this.disposer != null) {
                throw new IllegalStateException("just one disposer");
            }
            this.disposer = disposer;
        }
    }

    /**
     * An optional callback to run at the end of the wrapped block.
     * Must be safely serializable, so it receives runtime context comparable to that of the original setup.
     */
    public static abstract class Disposer implements Serializable {
        /**
         * Attempt to clean up anything that was done in the initial setup.
         * @param build a build being run
         * @param workspace a workspace of the build
         * @param launcher a way to start commands
         * @param listener a way to report progress
         * @throws IOException if something fails; {@link AbortException} for user errors
         * @throws InterruptedException if tear down is interrupted
         */
        public abstract void tearDown(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException;
    }

    /**
     * By default, when run as part of an {@link AbstractBuild}, will run late, in the {@link #setUp(AbstractBuild, Launcher, BuildListener)} phase.
     * May be overridden to return true, in which case this will run earlier, in the {@link #preCheckout} phase.
     * Ignored when not run as part of an {@link AbstractBuild}.
     */
    protected boolean runPreCheckout() {
        return false;
    }

    @Override public final Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
        if (runPreCheckout()) {
            return new Environment() {};
        } else {
            final Context c = new Context();
            setUp(c, build, build.getWorkspace(), launcher, listener, build.getEnvironment(listener));
            return new EnvironmentWrapper(c, launcher);
        }
    }

    @Override public final void preCheckout(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
        if (runPreCheckout()) {
            final Context c = new Context();
            setUp(c, build, build.getWorkspace(), launcher, listener, build.getEnvironment(listener));
            build.getEnvironments().add(new EnvironmentWrapper(c, launcher));
        }
    }

    private class EnvironmentWrapper extends Environment {
        private final Context c;
        private final Launcher launcher;
        EnvironmentWrapper(Context c, Launcher launcher) {
            this.c = c;
            this.launcher = launcher;
        }
        @Override public void buildEnvVars(Map<String,String> env) {
            if (env instanceof EnvVars) {
                ((EnvVars) env).overrideAll(c.env);
            } else { // ?
                env.putAll(c.env);
            }
        }
        @Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {
            if (c.disposer != null) {
                c.disposer.tearDown(build, build.getWorkspace(), launcher, listener);
            }
            return true;
        }
    }

    /**
     * Allows this wrapper to decorate log output.
     * @param build as is passed to {@link #setUp(Context, Run, FilePath, Launcher, TaskListener, EnvVars)}
     * @return a filter which ignores its {@code build} parameter and is {@link Serializable}; or null (the default)
     * @since 1.608
     */
    public @CheckForNull ConsoleLogFilter createLoggerDecorator(@Nonnull Run<?,?> build) {
        return null;
    }

    @Override public final OutputStream decorateLogger(AbstractBuild build, OutputStream logger) throws IOException, InterruptedException, Run.RunnerAbortedException {
        ConsoleLogFilter filter = createLoggerDecorator(build);
        return filter != null ? filter.decorateLogger(build, logger) : logger;
    }

    /**
     * May be overridden but this will only take effect when used as a {@link BuildWrapper} on an {@link AbstractProject}.
     * <p>{@inheritDoc}
     * @since 1.608
     */
    @Override public Launcher decorateLauncher(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException, Run.RunnerAbortedException {
        return super.decorateLauncher(build, launcher, listener);
        // TODO reasonable to decorate Launcher within a dynamic scope, but this signature does not mix well with Context pattern.
        // Called from AbstractBuildExecution.createLauncher; how do we track what is decorating what?
        // Would have to keep something like a LaunchedDecorator, not an actual Launcher, in Context.
        // And createLauncher is called before even preCheckout, so much too early for the Context to have been prepared.
        // Could perhaps create a proxy Launcher whose launch method checks some field in the Context remembered for the build.
    }

    /**
     * May be overridden but this will only take effect when used as a {@link BuildWrapper} on an {@link AbstractProject}.
     * <p>{@inheritDoc}
     * @since 1.608
     */
    @Override public void makeBuildVariables(AbstractBuild build, Map<String,String> variables) {
        super.makeBuildVariables(build, variables);
    }

    /**
     * May be overridden but this will only take effect when used as a {@link BuildWrapper} on an {@link AbstractProject}.
     * <p>{@inheritDoc}
     * @since 1.608
     */
    @Override public void makeSensitiveBuildVariables(AbstractBuild build, Set<String> sensitiveVariables) {
        super.makeSensitiveBuildVariables(build, sensitiveVariables);
        // TODO determine if there is a meaningful way to generalize this; perhaps as a new [Run]Action recording sensitiveVariables?
        // Complicated by the fact that in principle someone could call getSensitiveBuildVariables *before* the wrapper starts and actually sets those variables,
        // though in practice the likely use cases would come later, and perhaps it is acceptable to omit the names of variables which are yet to be set.
        // Also unclear if there is any use case for calling this method after the build is done (or Jenkins is restarted);
        // most likely it is only used during the build itself.
        // Would be much cleaner if EnvVars itself recorded which keys had sensitive values.
    }

    /**
     * {@inheritDoc}
     * @return an empty set; this might never be called if the step is not part of the static configuration of a project; instead, add a {@link SimpleBuildStep.LastBuildAction} to a build when run
     */
    @Override public final Collection<? extends Action> getProjectActions(AbstractProject job) {
        return Collections.emptySet();
    }

}
