/**
 * Copyright (C) 2006 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.opensymphony.xwork2.inject.util;

import static com.opensymphony.xwork2.inject.util.ReferenceType.STRONG;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.concurrent.*;

/**
 * Extends {@link ReferenceMap} to support lazy loading values by overriding
 * {@link #create(Object)}.
 *
 * @author crazybob@google.com (Bob Lee)
 */
public abstract class ReferenceCache<K, V> extends ReferenceMap<K, V> {

  private static final long serialVersionUID = 0;

  transient ConcurrentMap<Object, Future<V>> futures =
      new ConcurrentHashMap<Object, Future<V>>();

  transient ThreadLocal<Future<V>> localFuture = new ThreadLocal<Future<V>>();

  public ReferenceCache(ReferenceType keyReferenceType,
      ReferenceType valueReferenceType) {
    super(keyReferenceType, valueReferenceType);
  }

  /**
   * Equivalent to {@code new ReferenceCache(STRONG, STRONG)}.
   */
  public ReferenceCache() {
    super(STRONG, STRONG);
  }

  /**
   * Override to lazy load values. Use as an alternative to {@link
   * #put(Object,Object)}. Invoked by getter if value isn't already cached.
   * Must not return {@code null}. This method will not be called again until
   * the garbage collector reclaims the returned value.
   */
  protected abstract V create(K key);

  V internalCreate(K key) {
    try {
      FutureTask<V> futureTask = new FutureTask<V>(
          new CallableCreate(key));

      // use a reference so we get the same equality semantics.
      Object keyReference = referenceKey(key);
      Future<V> future = futures.putIfAbsent(keyReference, futureTask);
      if (future == null) {
        // winning thread.
        try {
          if (localFuture.get() != null) {
            throw new IllegalStateException(
                "Nested creations within the same cache are not allowed.");
          }
          localFuture.set(futureTask);
          futureTask.run();
          V value = futureTask.get();
          putStrategy().execute(this,
              keyReference, referenceValue(keyReference, value));
          return value;
        } finally {
          localFuture.remove();
          futures.remove(keyReference);
        }
      } else {
        // wait for winning thread.
        return future.get();
      }
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    } catch (ExecutionException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      } else if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException(cause);
    }
  }

  /**
   * {@inheritDoc}
   *
   * If this map does not contain an entry for the given key and {@link
   * #create(Object)} has been overridden, this method will create a new
   * value, put it in the map, and return it.
   *
   * @throws NullPointerException if {@link #create(Object)} returns null.
   * @throws java.util.concurrent.CancellationException if the creation is
   *  cancelled. See {@link #cancel()}.
   */
  @SuppressWarnings("unchecked")
  @Override public V get(final Object key) {
    V value = super.get(key);
    return (value == null)
      ? internalCreate((K) key)
      : value;
  }

  /**
   * Cancels the current {@link #create(Object)}. Throws {@link
   * java.util.concurrent.CancellationException} to all clients currently
   * blocked on {@link #get(Object)}.
   */
  protected void cancel() {
    Future<V> future = localFuture.get();
    if (future == null) {
      throw new IllegalStateException("Not in create().");
    }
    future.cancel(false);
  }

  class CallableCreate implements Callable<V> {

    K key;

    public CallableCreate(K key) {
      this.key = key;
    }

    public V call() {
      // try one more time (a previous future could have come and gone.)
      V value = internalGet(key);
      if (value != null) {
        return value;
      }

      // create value.
      value = create(key);
      if (value == null) {
        throw new NullPointerException(
            "create(K) returned null for: " + key);
      }
      return value;
    }
  }

  /**
   * Returns a {@code ReferenceCache} delegating to the specified {@code
   * function}. The specified function must not return {@code null}.
   */
  public static <K, V> ReferenceCache<K, V> of(
      ReferenceType keyReferenceType,
      ReferenceType valueReferenceType,
      final Function<? super K, ? extends V> function) {
    ensureNotNull(function);
    return new ReferenceCache<K, V>(keyReferenceType, valueReferenceType) {
      @Override
      protected V create(K key) {
        return function.apply(key);
      }
      private static final long serialVersionUID = 0;
    };
  }

  private void readObject(ObjectInputStream in) throws IOException,
      ClassNotFoundException {
    in.defaultReadObject();
    this.futures = new ConcurrentHashMap<Object, Future<V>>();
    this.localFuture = new ThreadLocal<Future<V>>();
  }

}
