/*
 * Copyright 2003-2016 JetBrains s.r.o.
 *
 * 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 jetbrains.mps.generator.cache;

import jetbrains.mps.cleanup.CleanupListener;
import jetbrains.mps.cleanup.CleanupManager;
import jetbrains.mps.components.CoreComponent;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.util.Pair;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;

import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Per-repository, model-associated caches.
 */
public abstract class BaseModelCache<T> implements CoreComponent, CleanupListener {
  // absence of model in the cache means we have no idea about present cache state.
  // if model is in the cache, we do know both IFile and cached object
  private final ConcurrentMap<SModelReference, Pair<IFile, T>> myCache = new ConcurrentHashMap<SModelReference, Pair<IFile, T>>();
  protected final SRepository myRepository;
  private final CleanupManager myCleanupManager;
  private final SRepositoryContentAdapter myRepoListener = new MyRepositoryListener();

  @Nullable
  protected abstract T readCache(SModel model);

  @NotNull
  public abstract String getCacheFileName();

  @Nullable
  protected IFile getCacheFile(SModel modelDescriptor) {
    IFile cachesDir = getCachesDirInternal(SModelOperations.getOutputCacheLocation(modelDescriptor));
    if (cachesDir == null) {
      return null;
    }

    return cachesDir.getDescendant(getCacheFileName());
  }

  // In fact, can be application-wide if we use compound key (repo+modelref)
  protected BaseModelCache(SRepository repository, CleanupManager cleanupManager) {
    myRepository = repository;
    myCleanupManager = cleanupManager;
  }

  @Override
  public void init() {
    myRepository.addRepositoryListener(myRepoListener);
    myCleanupManager.addCleanupListener(this);
  }

  @Override
  public void dispose() {
    myCleanupManager.removeCleanupListener(this);
    myRepository.removeRepositoryListener(myRepoListener);
  }

  @Nullable
  public T get(@NotNull SModel model) {
    final SModelReference mr = model.getReference();
    Pair<IFile, T> rv = myCache.get(mr);
    if (rv != null) {
      return rv.o2;
    }
    IFile cacheFile = getCacheFile(model);
    if (cacheFile == null) {
      return null;
    }
    return readAndUpdateCache(cacheFile, model);
  }

  private T readAndUpdateCache(IFile cacheFile, SModel model) {
    final SModelReference mr = model.getReference();
    T cache = readCache(model);
    if (cache == null) {
      return null;
    }
    final Pair<IFile, T> entry = new Pair<IFile, T>(cacheFile, cache);
    Pair<IFile, T> existing = myCache.putIfAbsent(mr, entry);
    if (existing != null) {
      return existing.o2;
    }
    return cache;
  }

  public void invalidateCacheForFile(IFile cacheFile) {
    SModelReference mr = findCachedModelForFile(cacheFile);
    if (mr == null) {
      return;
    }
    myCache.remove(mr);
  }

  @Nullable
  protected SModelReference findCachedModelForFile(IFile cacheFile) {
    for (Entry<SModelReference, Pair<IFile, T>> entry : myCache.entrySet()) {
      if (cacheFile.equals(entry.getValue().o1)) {
        return entry.getKey();
      }
    }
    return null;
  }

  @Nullable
  protected IFile getCachesDirInternal(@Nullable IFile defaultCachesDir) {
    // XXX there's little reason to keep this method, GenerationDependenciesCache shall override getCacheFile() directly.
    return defaultCachesDir;
  }

  /**
   * Invoke to set new cached value
   */
  protected final void update(SModel model, T cache) {
    final SModelReference mr = model.getReference();
    Pair<IFile, T> entry = myCache.remove(mr);
    if (entry != null) {
      // decided not to update with incomplete entry, although perhaps it won't hurt (file == null))
      myCache.put(mr, new Pair<IFile, T>(entry.o1, cache));
    }
  }

  /**
   * Forget cached state, if any; unlike {@link #discard(org.jetbrains.mps.openapi.model.SModel)} does not touch persisted/serialized state.
   */
  public final void clean(@NotNull SModel model) {
    myCache.remove(model.getReference());
  }

  protected final void clean(SModelReference modelRef) {
    myCache.remove(modelRef);
  }

  /**
   * Forget cached state and scrap any persisted/serialized state. Does its best to ensure serialized state got discarded, but doesn't guarantee that.
   */
  public void discard(@NotNull SModel model) {
    final Pair<IFile, T> removed = myCache.remove(model.getReference());
    IFile cachedFile = removed == null ? null : removed.o1;
    IFile actualCacheFile = getCacheFile(model);
    if (actualCacheFile != null) {
      actualCacheFile.delete();
    }
    if (cachedFile != null && cachedFile != actualCacheFile) {
      cachedFile.delete();
    }
  }

  @Override
  public void performCleanup() {
    myCache.clear();
  }

  private class MyRepositoryListener extends SRepositoryContentAdapter {
    @Override
    public void beforeModelRemoved(SModule module, SModel model) {
      clean(model);
    }

    @Override
    public void modelAdded(SModule module, SModel model) {
      clean(model);
    }

    @Override
    public void modelRenamed(SModule module, SModel model, SModelReference oldRef) {
      clean(model);
    }

    @Override
    public void modelReplaced(SModel model) {
      clean(model);
    }
  }
}
