/*
 * 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.smodel;

import jetbrains.mps.components.CoreComponent;
import jetbrains.mps.extapi.module.SRepositoryRegistry;
import jetbrains.mps.extapi.persistence.FileDataSource;
import jetbrains.mps.extapi.persistence.FolderDataSource;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.ModelComputeRunnable;
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 org.jetbrains.mps.openapi.persistence.DataSource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Bridge VFS and Model worlds in MPS.
 * Capable to tell {@link SModel} for a {@link IFile}
 * Models are tied to repository, same file may get loaded into few distinct repositories, that's why
 * instances of the tracker are per-repository.
 *
 * IMPLEMENTATION: now it tracks all model files and {@link #findModel(IFile) answers} quickly. Do we care to do it really fast at expense of huge
 * (there are thousands of models in a project) memory footprint? FIXME Shall measure footprint first, then re-consider
 *
 * XXX CoreComponent: what if we introduce notion of 'services' for an SRepository, so that there's no need to use static accessor (getInstance(SRepo))?
 *                    i.e. instead we'd have smth like SRepo.getService(SModelFileTracker.class)
 */
public class SModelFileTracker {
  private static final List<SModelFileTracker> ourModelTrackers = new CopyOnWriteArrayList<SModelFileTracker>();

  private final RepositoryModelTracker myListener = new RepositoryModelTracker();
  private final SRepository myRepository;

  private SModelFileTracker(SRepository repository) {
    myRepository = repository;
  }

  @NotNull
  public static SModelFileTracker getInstance(SRepository repository) {
    SModelFileTracker t = findInstance(repository);
    if (t != null) {
      return t;
    }
    synchronized (ourModelTrackers) {
      // despite ourModelTrackers is COW list, we don't want two parallel threads to add two identical SModelFileTracker instances,
      // hence we lock for modifications and check again prior to adding a new one.
      t = findInstance(repository);
      if (t != null) {
        return t;
      }
      ourModelTrackers.add(t = new SModelFileTracker(repository));
    }
    t.attach();
    return t;
  }

  private static SModelFileTracker findInstance(SRepository repository) {
    for (SModelFileTracker t : ourModelTrackers) {
      if (t.myRepository == repository) {
        return t;
      }
    }
    return null;
  }

  /*package*/ void attach() {
    new RepoListenerRegistrar(myRepository, myListener).attach();
  }

  /*package*/ void detach() {
    new RepoListenerRegistrar(myRepository, myListener).detach();
    // FIXME At the moment, there's no notification mechanism to find out about repository gone.
  }

  @Nullable
  public SModel findModel(@Nullable IFile modelFile) {
    if (modelFile == null) {
      return null;
    }
    SModelReference mr = myListener.findModel(modelFile);
    if (mr == null) {
      return null;
    }
    // FIXME shall I extract this class, similar to RepoListenerRegistrar? Or shall I demand model read provided clients are gonna read model anyway?
    class ModelResolve implements Computable<SModel> {
      private final SModelReference myModelReference;
      private final SRepository myRepository;

      public ModelResolve(SModelReference modelReference, SRepository repository) {
        myModelReference = modelReference;
        myRepository = repository;
      }

      @Override
      public SModel compute() {
        return myModelReference.resolve(myRepository);
      }

      public SModel resolve() {
        if (myRepository.getModelAccess().canRead()) {
          return compute();
        } else {
          return new ModelComputeRunnable<SModel>(this).runRead(myRepository.getModelAccess());
        }
      }
    }
    return new ModelResolve(mr, myRepository).resolve();
  }

  public static class Plug implements CoreComponent {
    public Plug(SRepositoryRegistry repositoryRegistry) {
      // FIXME may listen to repositories come and go, instantiate (and dispose!) trackers as appropriate
    }

    @Override
    public void init() {
    }

    @Override
    public void dispose() {
      ourModelTrackers.clear();
    }
  }

  private static class RepositoryModelTracker extends SRepositoryContentAdapter {
    private final Map<IFile, SModelReference> myPathsToModels = new HashMap<IFile, SModelReference>(256);

    @Override
    protected void startListening(SModel model) {
      addModelToFileCache(model);
    }

    @Override
    protected void stopListening(SModel model) {
      removeModelFromFileCache(model);
    }

    @Override
    public void beforeModelRenamed(SModule module, SModel model, SModelReference newRef) {
      removeModelFromFileCache(model);
    }

    @Override
    public void modelRenamed(SModule module, org.jetbrains.mps.openapi.model.SModel model, SModelReference oldRef) {
      addModelToFileCache(model);
    }

    private void addModelToFileCache(SModel md) {
      DataSource source = md.getSource();
      if (!(source instanceof FileDataSource || source instanceof FolderDataSource)) {
        return;
      }

      IFile file = source instanceof FileDataSource
          ? ((FileDataSource) source).getFile()
          : ((FolderDataSource) source).getFolder();
      myPathsToModels.put(file, md.getReference());
    }

    private void removeModelFromFileCache(SModel md) {
      DataSource source = md.getSource();
      if (!(source instanceof FileDataSource || source instanceof FolderDataSource)) return;

      IFile file = source instanceof FileDataSource
          ? ((FileDataSource) source).getFile()
          : ((FolderDataSource) source).getFolder();
      myPathsToModels.remove(file);
    }

    /*package*/ SModelReference findModel(IFile modelFile) {
      return myPathsToModels.get(modelFile);
    }
  }
}
