/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.jclouds.rest.suppliers;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static org.jclouds.util.Throwables2.getFirstThrowableOfType;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.jclouds.rest.AuthorizationException;

import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ForwardingObject;
import com.google.common.util.concurrent.UncheckedExecutionException;

/**
 * This will retry the supplier if it encounters a timeout exception, but not if it encounters an
 * AuthorizationException.
 * <p/>
 * A shared exception reference is used so that anyone who encounters an authorizationexception will be short-circuited.
 * This prevents accounts from being locked out.
 * 
 * <h3>details</h3>
 * http://code.google.com/p/google-guice/issues/detail?id=483 guice doesn't remember when singleton providers throw
 * exceptions. in this case, if the supplier fails with an authorization exception, it is called again for each provider
 * method that depends on it. To short-circuit this, we remember the last exception trusting that guice is
 * single-threaded.
 * 
 * Note this implementation is folded into the same class, vs being decorated as stacktraces are exceptionally long and
 * difficult to grok otherwise. We use {@link LoadingCache} to deal with concurrency issues related to the supplier.
 */
public class MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier<T> extends ForwardingObject implements
      Supplier<T> {
   
   static class SetAndThrowAuthorizationExceptionSupplierBackedLoader<V> extends CacheLoader<String, Optional<V>> {

      private final Supplier<V> delegate;
      private final AtomicReference<AuthorizationException> authException;
      private final ValueLoadedCallback<V> valueLoadedCallback;

      public SetAndThrowAuthorizationExceptionSupplierBackedLoader(Supplier<V> delegate,
            AtomicReference<AuthorizationException> authException, ValueLoadedCallback<V> valueLoadedCallback) {
         this.delegate = checkNotNull(delegate, "delegate");
         this.authException = checkNotNull(authException, "authException");
         this.valueLoadedCallback = checkNotNull(valueLoadedCallback, "valueLoadedCallback");
      }

      @Override
      public Optional<V> load(String key) {
         if (authException.get() != null)
            throw authException.get();
         try {
            Optional<V> value = Optional.fromNullable(delegate.get());
            valueLoadedCallback.valueLoaded(value);
            return value;
         } catch (Exception e) {
            AuthorizationException aex = getFirstThrowableOfType(e, AuthorizationException.class);
            if (aex != null) {
               authException.set(aex);
               throw aex;
            }
            throw propagate(e);
         }
      }

      @Override
      public String toString() {
         return MoreObjects.toStringHelper(this).add("delegate", delegate).toString();
      }
   }
   
   public static class ValueLoadedEvent<V> {
      private final Object eventKey;
      private final Optional<V> value;

      public ValueLoadedEvent(Object eventKey, Optional<V> value) {
         this.eventKey = checkNotNull(eventKey, "eventKey");
         this.value = checkNotNull(value, "value");
      }

      public Object getEventKey() {
         return eventKey;
      }

      public Optional<V> getValue() {
         return value;
      }
   }

   private final Supplier<T> delegate;
   private final long duration;
   private final TimeUnit unit;
   private final LoadingCache<String, Optional<T>> cache;

   public static <T> MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier<T> create(
         AtomicReference<AuthorizationException> authException, Supplier<T> delegate, long duration, TimeUnit unit) {
      return new MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier<T>(authException, delegate, duration,
            unit, new ValueLoadedCallback.NoOpCallback<T>());
   }
   
   /**
    * Creates a memoized supplier that calls the given callback each time values are loaded.
    */
   public static <T> MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier<T> create(
         AtomicReference<AuthorizationException> authException, Supplier<T> delegate, long duration, TimeUnit unit,
         ValueLoadedCallback<T> valueLoadedCallback) {
      return new MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier<T>(authException, delegate, duration,
            unit, valueLoadedCallback);
   }
   
   MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier(AtomicReference<AuthorizationException> authException,
         Supplier<T> delegate, long duration, TimeUnit unit, ValueLoadedCallback<T> valueLoadedCallback) {
      this.delegate = delegate;
      this.duration = duration;
      this.unit = unit;
      this.cache = CacheBuilder.newBuilder().expireAfterWrite(duration, unit)
            .build(new SetAndThrowAuthorizationExceptionSupplierBackedLoader<T>(delegate, authException, valueLoadedCallback));
   }

   @Override
   protected Supplier<T> delegate() {
      return delegate;
   }

   @Override
   public T get() {
      try {
         return cache.get("FOO").orNull();
      } catch (UncheckedExecutionException e) {
         throw propagate(e.getCause());
      } catch (ExecutionException e) {
         throw propagate(e.getCause());
      }
   }

   @Override
   public String toString() {
      return MoreObjects.toStringHelper(this).add("delegate", delegate).add("duration", duration).add("unit", unit)
            .toString();
   }

}
