/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
 *
 * 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 hudson.util;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Fingerprint;
import hudson.model.FingerprintMap;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Convenient base class for implementing data storage.
 *
 * <p>
 * One typical pattern of data storage in Hudson is the one that {@link Fingerprint}
 * uses, where each data is keyed by an unique key (MD5 sum), and that key is used
 * to determine the file system location of the data.
 *
 * On memory, each data is represented by one object ({@link Fingerprint}), and
 * write access to the same data is coordinated by using synchronization.
 *
 * <p>
 * With such storage, care has to be taken to ensure that there's only one data
 * object in memory for any given key. That means load and create operation
 * needs to be synchronized. This class implements this logic in a fairly efficient
 * way, and thus intends to help plugins that want to use such data storage.
 *
 * @since 1.87
 * @author Kohsuke Kawaguchi
 * @see FingerprintMap
 */
public abstract class KeyedDataStorage<T, P> {
    /**
     * The value is either {@code SoftReference<Fingerprint>} or {@link Loading}.
     *
     * If it's {@link SoftReference}, that represents the currently available value.
     * If it's {@link Loading}, then that indicates the fingerprint is being loaded.
     * The thread can wait on this object to be notified when the loading completes.
     */
    private final ConcurrentHashMap<String, Object> core = new ConcurrentHashMap<>();

    /**
     * Used in {@link KeyedDataStorage#core} to indicate that the loading of a fingerprint
     * is in progress, so that we can avoid creating two {@link Fingerprint}s for the same hash code,
     * but do so without having a single lock.
     */
    private static class Loading<T> {
        private @CheckForNull T value;
        private boolean set;

        public synchronized void set(@CheckForNull T value) {
            this.set = true;
            this.value = value;
            notifyAll();
        }

        /**
         * Blocks until the value is {@link #set(Object)} by another thread
         * and returns the value.
         */
        public synchronized @CheckForNull T get() {
            try {
                while (!set)
                    wait();
                return value;
            } catch (InterruptedException e) {
                // assume the loading failed, but make sure we process interruption properly later
                Thread.currentThread().interrupt();
                return null;
            }
        }
    }

    /**
     * Atomically gets the existing data object if any, or if it doesn't exist
     * {@link #create(String,Object) create} it and return it.
     *
     * @param createParams
     *      Additional parameters needed to create a new data object. Can be null.
     * @return
     *      Item with the specified {@code key}.
     * @throws IOException Loading error
     */
    public @NonNull T getOrCreate(String key, P createParams) throws IOException {
        return get(key, true, createParams);
    }

    /**
     * Finds the data object that matches the given key if available, or null
     * if not found.
     * @return Item with the specified {@code key}
     * @throws IOException Loading error
     */
    public @CheckForNull T get(String key) throws IOException {
        return get(key, false, null);
    }

    /**
     * Implementation of get/getOrCreate.
     * @return Item with the specified {@code key}
     * @throws IOException Loading error
     */
    protected @CheckForNull T get(@NonNull String key, boolean createIfNotExist, P createParams) throws IOException {
        while (true) {
            totalQuery.incrementAndGet();
            Object value = core.get(key);

            if (value instanceof SoftReference) {
                SoftReference<T> wfp = (SoftReference<T>) value;
                T t = wfp.get();
                if (t != null) {
                    cacheHit.incrementAndGet();
                    return t;  // found it
                }
                weakRefLost.incrementAndGet();
            }
            if (value instanceof Loading) {
                // another thread is loading it. get the value from there.
                T t = ((Loading<T>) value).get();
                if (t != null || !createIfNotExist)
                    return t;   // found it (t!=null) or we are just 'get' (!createIfNotExist)
            }

            // the fingerprint doesn't seem to be loaded thus far, so let's load it now.
            // the care needs to be taken that other threads might be trying to do the same.
            Loading<T> l = new Loading<>();
            if (value == null ? core.putIfAbsent(key, l) != null : !core.replace(key, value, l)) {
                // the value has changed since then. another thread is attempting to do the same.
                // go back to square 1 and try it again.
                continue;
            }

            T t = null;
            try {
                t = load(key);
                if (t == null && createIfNotExist) {
                    t = create(key, createParams);    // create the new data
                    if (t == null)
                        throw new IllegalStateException("Bug in the derived class"); // bug in the derived classes
                }
            } catch (IOException e) {
                loadFailure.incrementAndGet();
                throw e;
            } finally {
                // let other threads know that the value is available now.
                // when the original thread failed to load, this should set it to null.
                l.set(t);
            }

            // the map needs to be updated to reflect the result of loading
            if (t != null)
                core.put(key, new SoftReference<>(t));
            else
                core.remove(key);

            return t;
        }

    }

    /**
     * Attempts to load an existing data object from disk.
     *
     * <p>
     * {@link KeyedDataStorage} class serializes the requests so that
     * no two threads call the {@link #load(String)} method with the
     * same parameter concurrently. This ensures that there's only
     * up to one data object for any key.
     *
     * @return
     *      null if no such data exists.
     * @throws IOException
     *      if load operation fails. This exception will be
     *      propagated to the caller.
     */
    protected abstract @CheckForNull T load(String key) throws IOException;

    /**
     * Creates a new data object.
     *
     * <p>
     * This method is called by {@link #getOrCreate(String,Object)}
     * if the data that matches the specified key does not exist.
     * <p>
     * Because of concurrency, another thread might call {@link #get(String)}
     * as soon as a new data object is created, so it's important that
     * this method returns a properly initialized "valid" object.
     *
     * @return
     *      Created item. If construction fails, abort with an exception.
     * @throws IOException
     *      if the method fails to create a new data object, it can throw
     *      {@link IOException} (or any other exception) and that will be
     *      propagated to the caller.
     */
    protected abstract @NonNull T create(@NonNull String key, @NonNull P createParams) throws IOException;

    public void resetPerformanceStats() {
        totalQuery.set(0);
        cacheHit.set(0);
        weakRefLost.set(0);
        loadFailure.set(0);
    }

    /**
     * Gets the short summary of performance statistics.
     */
    public String getPerformanceStats() {
        int total = totalQuery.get();
        int hit = cacheHit.get();
        int weakRef = weakRefLost.get();
        int failure = loadFailure.get();
        int miss = total - hit - weakRef;

        return MessageFormat.format("total={0} hit={1}% lostRef={2}% failure={3}% miss={4}%",
                total, hit, weakRef, failure, miss);
    }

    /**
     * Total number of queries into this storage.
     */
    public final AtomicInteger totalQuery = new AtomicInteger();
    /**
     * Number of cache hits (of all the total queries.)
     */
    public final AtomicInteger cacheHit = new AtomicInteger();
    /**
     * Among cache misses, number of times when we had {@link SoftReference}
     * but lost its value due to GC.
     *
     * {@code totalQuery-cacheHit-weakRefLost} means cache miss.
     */
    public final AtomicInteger weakRefLost = new AtomicInteger();
    /**
     * Number of failures in loading data.
     */
    public final AtomicInteger loadFailure = new AtomicInteger();
}
