/*
 * Copyright 2003-2017 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;

import jetbrains.mps.extapi.model.GeneratableSModel;
import jetbrains.mps.generator.impl.GenControllerContext;
import jetbrains.mps.generator.impl.GenerationController;
import jetbrains.mps.generator.impl.GeneratorLoggerAdapter;
import jetbrains.mps.generator.impl.ModelStreamManager;
import jetbrains.mps.generator.impl.ModelStreamProviderImpl;
import jetbrains.mps.generator.impl.dependencies.GenerationDependencies;
import jetbrains.mps.generator.impl.dependencies.GenerationDependenciesCache;
import jetbrains.mps.messages.IMessageHandler;
import jetbrains.mps.smodel.LanguageAspect;
import jetbrains.mps.smodel.SModelStereotype;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import org.jetbrains.mps.openapi.util.ProgressMonitor;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Entry point to model transformation (aka generation) process. Populate with relevant context information:
 * {@link #messages(IMessageHandler)}  to receive generator messages (optional);
 * {@link #transients(TransientModelsProvider)} where to keep transient models (mandatory);
 * {@link #taskHandler(GeneratorTaskListener)} get notified about progress (optional);
 * then fire off with {@link #process(ProgressMonitor, List)}
 *
 * IMPLEMENTATION NOTE:
 *  transformation requires model read lock for a repository of transformed model. At certain moments, it also requires write lock on a repository with
 *  transient modules. Although I can (and would like to) hide appropriate locks inside TransientModelsProvider, now these are explicit and are outside
 *  of the class to avoid accidental 'can't write from read'. Shall investigate if there's true need to expose transient modules in a repository right
 *  from the very beginning. If yes, shall double efforts to get distinct repository for transient modules (so that only transient repo is write-locked,
 *  while input model's repo is read-locked).
 *
 * @author Artem Tikhomirov
 * @author Evgeny Gryaznov, 1/25/11
 */
public final class GenerationFacade {

  public static Collection<SModel> getModifiedModels(Collection<? extends SModel> models) {
    Set<SModel> result = new LinkedHashSet<>();
    ModelGenerationStatusManager statusManager = ModelGenerationStatusManager.getInstance();
    for (SModel sm : models) {
      if (statusManager.generationRequired(sm)) {
        result.add(sm);
        continue;
      }

      // TODO regenerating all dependant models can be slow, option?
      if (!(SModelStereotype.DESCRIPTOR.equals(SModelStereotype.getStereotype(sm)) || LanguageAspect.BEHAVIOR.is(sm) || LanguageAspect.CONSTRAINTS.is(sm))) {
        // temporary solution: only descriptor/behavior/constraints models
        continue;
      }

      final SRepository repository = sm.getRepository();
      if (repository == null) {
        // no idea how to treat a model which hands in the air; expect it to be editable and tell isChanged if desires re-generation
        continue;
      }
      GenerationDependencies oldDependencies = GenerationDependenciesCache.getInstance().get(sm);
      // FIXME use SRepository to pick proper GenerationDependenciesCache instance
      if (oldDependencies == null) {
        // TODO turn on when generated file will be mandatory
        //result.add(sm);
        continue;
      }



      Map<String, String> externalHashes = oldDependencies.getExternalHashes();
      for (Entry<String, String> entry : externalHashes.entrySet()) {
        String modelReference = entry.getKey();
        SModel rmd = PersistenceFacade.getInstance().createModelReference(modelReference).resolve(repository);
        if (rmd == null) {
          result.add(sm);
          break;
        }
        String oldHash = entry.getValue();
        if (oldHash == null) {
          continue;
        }
        String newHash = statusManager.currentHash(rmd);
        if (newHash == null || !oldHash.equals(newHash)) {
          result.add(sm);
          break;
        }
      }
    }

    return result;
  }

  public static boolean canGenerate(SModel sm) {
    return sm instanceof GeneratableSModel && ((GeneratableSModel) sm).isGeneratable();
  }


  private final SRepository myRepository;
  private final GenerationOptions myGenerationOptions;
  private GeneratorTaskListener<GeneratorTask> myTaskListener;
  private TransientModelsProvider myTransientModelsProvider;
  private IMessageHandler myMessageHandler = IMessageHandler.NULL_HANDLER;
  private ModelStreamManager.Provider myStreamProvider;

  public GenerationFacade(@NotNull SRepository repository, @NotNull GenerationOptions generationOptions) {
    myRepository = repository;
    myGenerationOptions = generationOptions;
  }


  /**
   * Optional handler to get notified about generation process
   * @param taskHandler receives notifications
   * @return <code>this</code> for convenience
   */
  public GenerationFacade taskHandler(@Nullable GeneratorTaskListener<GeneratorTask> taskHandler) {
    myTaskListener = taskHandler;
    return this;
  }

  /**
   * Register facility responsible for transient model handling, <em>mandatory</em>.
   * @param transientModelsProvider transient model facility
   * @return <code>this</code> for convenience
   */
  public GenerationFacade transients(@NotNull TransientModelsProvider transientModelsProvider) {
    myTransientModelsProvider = transientModelsProvider;
    return this;
  }

  /**
   * Optional destination of all messages reported by generator, if none specified (or <code>null</code>), messages get discarded.
   * @param messages destination of generator messages, or <code>null</code>
   * @return <code>this</code> for convenience
   */
  public GenerationFacade messages(@Nullable IMessageHandler messages) {
    myMessageHandler = messages == null ? IMessageHandler.NULL_HANDLER : messages;
    return this;
  }

  /**
   * Configure access to auxiliary data associated with model
   * FIXME public
   * @param streamProvider facility to keep model-associated data
   * @return <code>this</code> for convenience
   */
  private GenerationFacade modelStreams(ModelStreamManager.Provider streamProvider) {
    myStreamProvider = streamProvider;
    return this;
  }

  /**
   * Generate single model. {@link GenerationFacade} instance can be reused then for other generation activities.
   * IMPORTANT: unlike {@link #process(ProgressMonitor, List)}, requires model write lock (on a repository of TransientModelsProvider)
   * as it needs to create and publish module with transient models.
   * @param monitor report progress/cancellation
   * @param model input
   * @return status object that describes generation outcome
   */
  public GenerationStatus process(@NotNull final ProgressMonitor monitor, @NotNull SModel model) {
    final GeneratorTaskListener<GeneratorTask> originalListener = myTaskListener;
    final GenerationTaskRecorder<GeneratorTask> recorder = new GenerationTaskRecorder<>(originalListener);
    myTaskListener = recorder;
    try {
      final GeneratorTaskBase task = new GeneratorTaskBase(model);
      TransientModelsModule tm = myTransientModelsProvider.createModule(model.getModule().getModuleName());
      myTransientModelsProvider.associate(task, tm);
      modelStreams(new ModelStreamProviderImpl());
      process0(monitor, Collections.singletonList(task));
      myTransientModelsProvider.publishAll();
      return recorder.getRecorded(task);
    } finally {
      myTaskListener = originalListener;
    }
  }

  /**
   * Feed transformation process with sequence of task. Tasks are processed in the order given. If a task deals with a model
   * from a repository, calling code shall ensure respective read lock.
   * @param monitor report progress/cancellation
   * @param tasks models to generate
   */
  public void process(@NotNull final ProgressMonitor monitor, @NotNull final List<? extends GeneratorTask> tasks) {
    modelStreams(new ModelStreamProviderImpl());
    process0(monitor, tasks);
  }

  private void process0(@NotNull final ProgressMonitor monitor, @NotNull final List<? extends GeneratorTask> tasks) {
    myTransientModelsProvider.startGeneration(myGenerationOptions.getNumberOfModelsToKeep());

    final GeneratorLoggerAdapter logger = new GeneratorLoggerAdapter(myMessageHandler, myGenerationOptions.isShowInfo(), myGenerationOptions.isShowWarnings());

    GenControllerContext ctx = new GenControllerContext(myRepository, myGenerationOptions, myTransientModelsProvider, myStreamProvider);
    GeneratorTaskListener<GeneratorTask> taskListener;
    if (myTaskListener != null) {
      taskListener = myTaskListener;
    } else {
      taskListener = new EmptyTaskListener();
    }

    final GenerationController gc = new GenerationController(tasks, ctx, taskListener, logger);
    gc.generate(monitor);
  }

  private static class EmptyTaskListener implements GeneratorTaskListener<GeneratorTask> {
    @Override
    public void start(@NotNull GeneratorTask task) {
      // no-op
    }

    @Override
    public void done(@NotNull GeneratorTask task, @NotNull GenerationStatus status) {
      // no-op
    }
  }
}
