/**
 * 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.waveprotocol.box.server.waveserver;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.typesafe.config.Config;
import org.waveprotocol.box.common.ExceptionalIterator;
import org.waveprotocol.box.server.CoreSettingsNames;
import org.waveprotocol.box.server.executor.ExecutorAnnotations.LookupExecutor;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.id.WaveletName;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

/**
 * A collection of wavelets, local and remote, held in memory.
 *
 * @author soren@google.com (Soren Lassen)
 */
public class WaveMap {

  /**
   * Returns a future whose result is the ids of stored wavelets in the given wave.
   * Any failure is reported as a {@link PersistenceException}.
   */
  private static ListenableFuture<ImmutableSet<WaveletId>> lookupWavelets(
      final WaveId waveId, final WaveletStore<?> waveletStore, Executor lookupExecutor) {
    ListenableFutureTask<ImmutableSet<WaveletId>> task =
        ListenableFutureTask.create(
           new Callable<ImmutableSet<WaveletId>>() {
             @Override
             public ImmutableSet<WaveletId> call() throws PersistenceException {
               return waveletStore.lookup(waveId);
             }
           });
    lookupExecutor.execute(task);
    return task;
  }

  private final LoadingCache<WaveId, Wave> waves;
  private final WaveletStore<?> store;

  @Inject
  public WaveMap(final DeltaAndSnapshotStore waveletStore,
      final WaveletNotificationSubscriber notifiee,
      final LocalWaveletContainer.Factory localFactory,
      final RemoteWaveletContainer.Factory remoteFactory,
      @Named(CoreSettingsNames.WAVE_SERVER_DOMAIN) final String waveDomain,
      Config config,
      @LookupExecutor final Executor lookupExecutor) {

    this.store = waveletStore;
    waves = CacheBuilder.newBuilder()
            .maximumSize(config.getInt("core.wave_cache_size"))
            .expireAfterAccess(config.getDuration("core.wave_cache_expire", TimeUnit.MINUTES), TimeUnit.MINUTES)
            .build(new CacheLoader<WaveId, Wave>() {
      @Override
      public Wave load(WaveId waveId) throws Exception {
        ListenableFuture<ImmutableSet<WaveletId>> lookedupWavelets =
          lookupWavelets(waveId, waveletStore, lookupExecutor);
        return new Wave(waveId, lookedupWavelets, notifiee, localFactory, remoteFactory, waveDomain);
      }
    });
  }

  /**
   * Loads all wavelets from storage.
   *
   * @throws WaveletStateException if storage access fails.
   */
  public void loadAllWavelets() throws WaveletStateException {
    try {
      ExceptionalIterator<WaveId, PersistenceException> itr = store.getWaveIdIterator();
      while (itr.hasNext()) {
        WaveId waveId = itr.next();
        lookupWavelets(waveId);
      }
    } catch (PersistenceException e) {
      throw new WaveletStateException("Failed to scan waves", e);
    }
  }

  /**
   * Unloads all wavelets from memory.
   *
   * @throws WaveletStateException if storage access fails.
   */
  public void unloadAllWavelets() throws WaveletStateException {
    waves.asMap().clear();
  }

  /**
   * Returns defensive copy of the map that holds waves.
   */
  Map<WaveId, Wave> getWaves() {
    return ImmutableMap.copyOf(waves.asMap());
  }

  public ExceptionalIterator<WaveId, WaveServerException> getWaveIds() {
    Iterator<WaveId> inner = waves.asMap().keySet().iterator();
    return ExceptionalIterator.FromIterator.create(inner);
  }

  public ImmutableSet<WaveletId> lookupWavelets(WaveId waveId) throws WaveletStateException {
    try {
      ListenableFuture<ImmutableSet<WaveletId>> future = waves.get(waveId).getLookedupWavelets();
      return FutureUtil.getResultOrPropagateException(future, PersistenceException.class);
    } catch (PersistenceException e) {
      throw new WaveletStateException("Failed to look up wave " + waveId, e);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new WaveletStateException("Interrupted while looking up wave " + waveId, e);
    } catch (ExecutionException ex) {
      throw new RuntimeException(ex);
    }
  }

  /**
   * Tries to fetch the wavelet as remote, if not found - fetches as local
   * wavelet.
   *
   * @param waveletName the wavelet name
   * @return the local or remote wavelet.
   * @throws WaveletStateException if something goes wrong
   */
  public WaveletContainer getWavelet(WaveletName waveletName) throws WaveletStateException {
    WaveletContainer waveletContainer = null;
    try {
      waveletContainer = getRemoteWavelet(waveletName);
    } catch (WaveletStateException | NullPointerException e) {
      // Ignored.
    }

    if (waveletContainer == null) {
      waveletContainer = getLocalWavelet(waveletName);
    }
    return waveletContainer;
  }

  public LocalWaveletContainer getLocalWavelet(WaveletName waveletName)
      throws WaveletStateException {
    try {
      return waves.get(waveletName.waveId).getLocalWavelet(waveletName.waveletId);
    } catch (ExecutionException ex) {
      throw new RuntimeException(ex);
    }
  }

  public RemoteWaveletContainer getRemoteWavelet(WaveletName waveletName)
      throws WaveletStateException {
    try {
      return waves.get(waveletName.waveId).getRemoteWavelet(waveletName.waveletId);
    } catch (ExecutionException ex) {
      throw new RuntimeException(ex);
    }
  }

  public LocalWaveletContainer getOrCreateLocalWavelet(WaveletName waveletName) {
    try {
      return waves.get(waveletName.waveId).getOrCreateLocalWavelet(waveletName.waveletId);
    } catch (ExecutionException ex) {
      throw new RuntimeException(ex);
    }
  }

  public RemoteWaveletContainer getOrCreateRemoteWavelet(WaveletName waveletName) {
    try {
      return waves.get(waveletName.waveId).getOrCreateRemoteWavelet(waveletName.waveletId);
    } catch (ExecutionException ex) {
      throw new RuntimeException(ex);
    }
  }
}
