/*
 * 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.lazy;

import hudson.Extension;
import hudson.model.AbstractItem;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.Queue;
import hudson.model.Run;
import hudson.model.RunMap;
import hudson.model.listeners.ItemListener;
import hudson.model.queue.SubTask;
import hudson.widgets.BuildHistoryWidget;
import hudson.widgets.HistoryWidget;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;

import static java.util.logging.Level.FINER;
import jenkins.model.RunIdMigrator;

/**
 * Makes it easier to use a lazy {@link RunMap} from a {@link Job} implementation.
 * Provides method implementations for some abstract {@link Job} methods,
 * as well as some methods which are not abstract but which you should override.
 * <p>Should be kept in a {@code transient} field in the job.
 * @since 1.556
 */
@SuppressWarnings({"unchecked", "rawtypes"}) // BuildHistoryWidget, and AbstractItem.getParent
public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyBuildMixIn.LazyLoadingRun<JobT,RunT>> {

    private static final Logger LOGGER = Logger.getLogger(LazyBuildMixIn.class.getName());

    @SuppressWarnings("deprecation") // [JENKINS-15156] builds accessed before onLoad or onCreatedFromScratch called
    private @Nonnull RunMap<RunT> builds = new RunMap<RunT>();

    /**
     * Initializes this mixin.
     * Call this from a constructor and {@link AbstractItem#onLoad} to make sure it is always initialized.
     */
    protected LazyBuildMixIn() {}

    protected abstract JobT asJob();

    /**
     * Gets the raw model.
     * Normally should not be called as such.
     * Note that the initial value is replaced during {@link #onCreatedFromScratch} or {@link #onLoad}.
     */
    public final @Nonnull RunMap<RunT> getRunMap() {
        return builds;
    }

    /**
     * Same as {@link #getRunMap} but suitable for {@link Job#_getRuns}.
     */
    public final RunMap<RunT> _getRuns() {
        assert builds.baseDirInitialized() : "neither onCreatedFromScratch nor onLoad called on " + asJob() + " yet";
        return builds;
    }

    /**
     * Something to be called from {@link Job#onCreatedFromScratch}.
     */
    public final void onCreatedFromScratch() {
        builds = createBuildRunMap();
    }

    /**
     * Something to be called from {@link Job#onLoad}.
     */
    @SuppressWarnings("unchecked")
    public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
        RunMap<RunT> _builds = createBuildRunMap();
        int max = _builds.maxNumberOnDisk();
        int next = asJob().getNextBuildNumber();
        if (next <= max) {
            LOGGER.log(Level.WARNING, "JENKINS-27530: improper nextBuildNumber {0} detected in {1} with highest build number {2}; adjusting", new Object[] {next, asJob(), max});
            asJob().updateNextBuildNumber(max + 1);
        }
        RunMap<RunT> currentBuilds = this.builds;
        if (parent != null) {
            // are we overwriting what currently exist?
            // this is primarily when Jenkins is getting reloaded
            Item current;
            try {
                current = parent.getItem(name);
            } catch (RuntimeException x) {
                LOGGER.log(Level.WARNING, "failed to look up " + name + " in " + parent, x);
                current = null;
            }
            if (current != null && current.getClass() == asJob().getClass()) {
                currentBuilds = (RunMap<RunT>) ((LazyLoadingJob) current).getLazyBuildMixIn().builds;
            }
        }
        if (currentBuilds != null) {
            // if we are reloading, keep all those that are still building intact
            for (RunT r : currentBuilds.getLoadedBuilds().values()) {
                if (r.isBuilding()) {
                    // Do not use RunMap.put(Run):
                    _builds.put(r.getNumber(), r);
                    LOGGER.log(Level.FINE, "keeping reloaded {0}", r);
                }
            }
        }
        this.builds = _builds;
    }

    private RunMap<RunT> createBuildRunMap() {
        RunMap<RunT> r = new RunMap<RunT>(asJob().getBuildDir(), new RunMap.Constructor<RunT>() {
            @Override public RunT create(File dir) throws IOException {
                return loadBuild(dir);
            }
        });
        RunIdMigrator runIdMigrator = asJob().runIdMigrator;
        assert runIdMigrator != null;
        r.runIdMigrator = runIdMigrator;
        return r;
    }

    /**
     * Type token for the build type.
     * The build class must have two constructors:
     * one taking the project type ({@code P});
     * and one taking {@code P}, then {@link File}.
     */
    protected abstract Class<RunT> getBuildClass();

    /**
     * Loads an existing build record from disk.
     * The default implementation just calls the ({@link Job}, {@link File}) constructor of {@link #getBuildClass}.
     */
    public RunT loadBuild(File dir) throws IOException {
        try {
            return getBuildClass().getConstructor(asJob().getClass(), File.class).newInstance(asJob(), dir);
        } catch (InstantiationException e) {
            throw new Error(e);
        } catch (IllegalAccessException e) {
            throw new Error(e);
        } catch (InvocationTargetException e) {
            throw handleInvocationTargetException(e);
        } catch (NoSuchMethodException e) {
            throw new Error(e);
        }
    }

    /**
     * Creates a new build of this project for immediate execution.
     * Calls the ({@link Job}) constructor of {@link #getBuildClass}.
     * Suitable for {@link SubTask#createExecutable}.
     */
    public final synchronized RunT newBuild() throws IOException {
        try {
            RunT lastBuild = getBuildClass().getConstructor(asJob().getClass()).newInstance(asJob());
            builds.put(lastBuild);
            lastBuild.getPreviousBuild(); // JENKINS-20662: create connection to previous build
            return lastBuild;
        } catch (InvocationTargetException e) {
            LOGGER.log(Level.WARNING, String.format("A new build could not be created in job %s", asJob().getFullName()), e);
            throw handleInvocationTargetException(e);
        } catch (ReflectiveOperationException | IllegalStateException e) {
            LOGGER.log(Level.WARNING, String.format("A new build could not be created in job %s", asJob().getFullName()), e);
            throw new Error(e);
        }
    }

    private IOException handleInvocationTargetException(InvocationTargetException e) {
        Throwable t = e.getTargetException();
        if (t instanceof Error) {
            throw (Error) t;
        }
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        }
        if (t instanceof IOException) {
            return (IOException) t;
        }
        throw new Error(t);
    }

    /**
     * Suitable for {@link Job#removeRun}.
     */
    public final void removeRun(RunT run) {
        if (!builds.remove(run)) {
            LOGGER.log(Level.WARNING, "{0} did not contain {1} to begin with", new Object[] {asJob(), run});
        }
    }

    /**
     * Suitable for {@link Job#getBuild}.
     */
    public final RunT getBuild(String id) {
        return builds.getById(id);
    }

    /**
     * Suitable for {@link Job#getBuildByNumber}.
     */
    public final RunT getBuildByNumber(int n) {
        return builds.getByNumber(n);
    }

    /**
     * Suitable for {@link Job#getFirstBuild}.
     */
    public final RunT getFirstBuild() {
        return builds.oldestBuild();
    }

    /**
     * Suitable for {@link Job#getLastBuild}.
     */
    public final @CheckForNull RunT getLastBuild() {
        return builds.newestBuild();
    }

    /**
     * Suitable for {@link Job#getNearestBuild}.
     */
    public final RunT getNearestBuild(int n) {
        return builds.search(n, AbstractLazyLoadRunMap.Direction.ASC);
    }

    /**
     * Suitable for {@link Job#getNearestOldBuild}.
     */
    public final RunT getNearestOldBuild(int n) {
        return builds.search(n, AbstractLazyLoadRunMap.Direction.DESC);
    }

    /**
     * Suitable for {@link Job#createHistoryWidget}.
     */
    public final HistoryWidget createHistoryWidget() {
        return new BuildHistoryWidget(asJob(), builds, Job.HISTORY_ADAPTER);
    }

    /**
     * Marker for a {@link Job} which uses this mixin.
     */
    public interface LazyLoadingJob<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyLoadingRun<JobT,RunT>> {
        LazyBuildMixIn<JobT,RunT> getLazyBuildMixIn();
    }

    public interface LazyLoadingRun<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyLoadingRun<JobT,RunT>> {
        RunMixIn<JobT,RunT> getRunMixIn();
    }

    /**
     * Accompanying helper for the run type.
     * Stateful but should be held in a {@code transient final} field.
     */
    public static abstract class RunMixIn<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyLoadingRun<JobT,RunT>> {

        /**
         * Pointers to form bi-directional link between adjacent runs using
         * {@link LazyBuildMixIn}.
         *
         * <p>
         * Some {@link Run}s do lazy-loading, so we don't use
         * {@link #previousBuildR} and {@link #nextBuildR}, and instead use these
         * fields and point to {@link #selfReference} (or {@link #none}) of
         * adjacent builds.
         */
        private volatile BuildReference<RunT> previousBuildR, nextBuildR;

        /**
         * Used in {@link #previousBuildR} and {@link #nextBuildR} to indicate
         * that we know there is no next/previous build (as opposed to {@code null},
         * which is used to indicate we haven't determined if there is a next/previous
         * build.)
         */
        @SuppressWarnings({"unchecked", "rawtypes"})
        private static final BuildReference NONE = new BuildReference("NONE", null);

        @SuppressWarnings("unchecked")
        private BuildReference<RunT> none() {
            return NONE;
        }

        private BuildReference<RunT> selfReference;

        protected RunMixIn() {}

        protected abstract RunT asRun();

        /**
         * To implement {@link Run#createReference}.
         */
        public final synchronized BuildReference<RunT> createReference() {
            if (selfReference == null) {
                selfReference = new BuildReference<RunT>(asRun().getId(), asRun());
            }
            return selfReference;
        }

        /**
         * To implement {@link Run#dropLinks}.
         */
        public final void dropLinks() {
            if (nextBuildR != null) {
                RunT nb = nextBuildR.get();
                if (nb != null) {
                    nb.getRunMixIn().previousBuildR = previousBuildR;
                }
            }
            if (previousBuildR != null) {
                RunT pb = previousBuildR.get();
                if (pb != null) {
                    pb.getRunMixIn().nextBuildR = nextBuildR;
                }
            }

            // make this build object unreachable by other Runs
            createReference().clear();
        }

        /**
         * To implement {@link Run#getPreviousBuild}.
         */
        public final RunT getPreviousBuild() {
            while (true) {
                BuildReference<RunT> r = previousBuildR;    // capture the value once

                if (r == null) {
                    // having two neighbors pointing to each other is important to make RunMap.removeValue work
                    JobT _parent = asRun().getParent();
                    if (_parent == null) {
                        throw new IllegalStateException("no parent for " + asRun().number);
                    }
                    RunT pb = _parent.getLazyBuildMixIn()._getRuns().search(asRun().number - 1, AbstractLazyLoadRunMap.Direction.DESC);
                    if (pb != null) {
                        pb.getRunMixIn().nextBuildR = createReference();   // establish bi-di link
                        this.previousBuildR = pb.getRunMixIn().createReference();
                        LOGGER.log(FINER, "Linked {0}<->{1} in getPreviousBuild()", new Object[]{this, pb});
                        return pb;
                    } else {
                        this.previousBuildR = none();
                        return null;
                    }
                }
                if (r == none()) {
                    return null;
                }

                RunT referent = r.get();
                if (referent != null) {
                    return referent;
                }

                // the reference points to a GC-ed object, drop the reference and do it again
                this.previousBuildR = null;
            }
        }

        /**
         * To implement {@link Run#getNextBuild}.
         */
        public final RunT getNextBuild() {
            while (true) {
                BuildReference<RunT> r = nextBuildR;    // capture the value once

                if (r == null) {
                    // having two neighbors pointing to each other is important to make RunMap.removeValue work
                    RunT nb = asRun().getParent().getLazyBuildMixIn()._getRuns().search(asRun().number + 1, AbstractLazyLoadRunMap.Direction.ASC);
                    if (nb != null) {
                        nb.getRunMixIn().previousBuildR = createReference();   // establish bi-di link
                        this.nextBuildR = nb.getRunMixIn().createReference();
                        LOGGER.log(FINER, "Linked {0}<->{1} in getNextBuild()", new Object[]{this, nb});
                        return nb;
                    } else {
                        this.nextBuildR = none();
                        return null;
                    }
                }
                if (r == none()) {
                    return null;
                }

                RunT referent = r.get();
                if (referent != null) {
                    return referent;
                }

                // the reference points to a GC-ed object, drop the reference and do it again
                this.nextBuildR = null;
            }
        }

    }

    @Restricted(DoNotUse.class)
    @Extension public static final class ItemListenerImpl extends ItemListener {
        @Override public void onLocationChanged(Item item, String oldFullName, String newFullName) {
            if (item instanceof LazyLoadingJob) {
                RunMap<?> builds = ((LazyLoadingJob) item).getLazyBuildMixIn().builds;
                builds.updateBaseDir(((Job) item).getBuildDir());
            }
        }
    }

}
