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

import jetbrains.mps.smodel.Language;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.event.SNodeAddEvent;
import org.jetbrains.mps.openapi.event.SNodeRemoveEvent;
import org.jetbrains.mps.openapi.event.SPropertyChangeEvent;
import org.jetbrains.mps.openapi.event.SReferenceChangeEvent;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModel.Problem;
import org.jetbrains.mps.openapi.model.SModelListener;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNodeChangeListener;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryAttachListener;
import org.jetbrains.mps.openapi.module.SRepositoryListenerBase;

import java.util.HashSet;
import java.util.Set;

/**
 * Tracks changes to language concepts throughout whole repository.
 * <pre>
 *   SModel modelWithInstancesOfSomeConcepts;
 *   StructureAspectChangeTracker t = new StructureAspectChangeTracker(null, new ModuleListener() {
 *     void structureAspectChanged(...) {
 *       revalidateCaches();
 *     }
 *   });
 *   t.attachTo(model.getRepository());
 *   ...
 *   // code that relies on meta-model, and needs to update once meta-model changes (e.g. caches by concept FQN)
 *   SNode n;
 *   myCacheMap.put(n.getConcept().getQualifiedName(), ...);
 *   ...
 *   // much later
 *   t.detachFrom(model.getRepository());
 * </pre>
 * @author Artem Tikhomirov
 */
public final class StructureAspectChangeTracker extends SRepositoryListenerBase implements SRepositoryAttachListener, SNodeChangeListener, SModelListener {
  // I assume model changes come in a single thread, as well as commandFinished notification, and do not care to synchronize
  private final Set<SModelReference> myChangedModels = new HashSet<SModelReference>();
  private final Set<SModuleReference> myChangedModules = new HashSet<SModuleReference>();
  private final ModelListener myModelListener;
  private final ModuleListener myModuleListener;

  // it's reasonable to share SACT instance and to register listeners with it, but at the moment there's no place where
  // I can keep this instance, hence use single listener approach.
  public StructureAspectChangeTracker(ModelListener l1, ModuleListener l2) {
    assert l1 != null || l2 != null;
    myModelListener = l1;
    myModuleListener = l2;
  }

  @Override
  public void startListening(@NotNull SRepository repository) {
    for (SModule module : repository.getModules()) {
      startListening(module);
    }
  }

  @Override
  public void stopListening(SRepository repository) {
    for (SModule module : repository.getModules()) {
      stopListening(module);
    }
  }

  private boolean isLanguageAndCanChange(SModule module) {
    return !module.isReadOnly() && module instanceof Language;
  }

  private void startListening(SModule module) {
    if (!isLanguageAndCanChange(module)) {
      return;
    }
    final SModel structureModel = ((Language) module).getStructureModelDescriptor();
    if (structureModel == null) {
      return;
    }
    if (structureModel instanceof EditableSModel) {
      ((EditableSModel) structureModel).addChangeListener(this);
    }
    structureModel.addModelListener(this);
  }

  private void stopListening(SModule module) {
    if (!isLanguageAndCanChange(module)) {
      return;
    }
    final SModel structureModel = ((Language) module).getStructureModelDescriptor();
    if (structureModel == null) {
      return;
    }
    if (structureModel instanceof EditableSModel) {
      ((EditableSModel) structureModel).removeChangeListener(this);
    }
    structureModel.removeModelListener(this);
  }

  @Override
  public void moduleAdded(@NotNull SModule module) {
    startListening(module);
  }

  @Override
  public void beforeModuleRemoved(@NotNull SModule module) {
    stopListening(module);
  }

  @Override
  public void nodeAdded(@NotNull SNodeAddEvent event) {
    structureModelChanged(event.getModel());
  }

  @Override
  public void nodeRemoved(@NotNull SNodeRemoveEvent event) {
    structureModelChanged(event.getModel());
  }

  @Override
  public void propertyChanged(@NotNull SPropertyChangeEvent event) {
    // e.g. 'name' property change constitutes concept rename refactoring
    structureModelChanged(event.getModel());
  }

  @Override
  public void referenceChanged(@NotNull SReferenceChangeEvent event) {
    // e.g. adding 'extends' references might change concept hierarchy completely
    structureModelChanged(event.getModel());
  }

  private void structureModelChanged(SModel model) {
    if (myModelListener != null) {
      myChangedModels.add(model.getReference());
    }
    if (myModuleListener != null) {
      myChangedModules.add(model.getModule().getModuleReference());
    }
  }

  @Override
  public void commandFinished(SRepository repository) {
    fireStructureAspectChanged();
  }

  //
  // SModelListener - care about model unload/replace, fire structure aspect changed event right away
  //

  @Override
  public void modelLoaded(SModel model, boolean partially) {
    // no-op
  }

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

  @Override
  public void modelUnloaded(SModel model) {
    structureModelChanged(model);
    fireStructureAspectChanged();
  }

  @Override
  public void modelSaved(SModel model) {
    // no-op
  }

  @Override
  public void conflictDetected(SModel model) {
    // no-op
  }

  @Override
  public void problemsDetected(SModel model, Iterable<Problem> problems) {
    // no-op
  }

  @Override
  public void modelAttached(SModel model, SRepository repository) {
    // no-op
  }

  @Override
  public void modelDetached(SModel model, SRepository repository) {
    // no-op
  }

  //
  // SModelListener
  //


  private void fireStructureAspectChanged() {
    if (!myChangedModels.isEmpty()) {
      HashSet<SModelReference> copy = new HashSet<SModelReference>(myChangedModels);
      myChangedModels.clear();
      myModelListener.structureAspectChanged(copy);
    }
    if (!myChangedModules.isEmpty()) {
      HashSet<SModuleReference> copy = new HashSet<SModuleReference>(myChangedModules);
      myChangedModules.clear();
      myModuleListener.structureAspectChanged(copy);
    }
  }

  public interface ModelListener {
    void structureAspectChanged(Set<SModelReference> changedStructureModels);
  }
  public interface ModuleListener {
    void structureAspectChanged(Set<SModuleReference> changedModules);
  }
}
