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

import jetbrains.mps.module.ReloadableModuleBase;
import jetbrains.mps.module.SDependencyImpl;
import jetbrains.mps.project.ModelsAutoImportsManager;
import jetbrains.mps.project.ModelsAutoImportsManager.AutoImportsContributor;
import jetbrains.mps.project.ModuleId;
import jetbrains.mps.project.structure.modules.GeneratorDescriptor;
import jetbrains.mps.project.structure.modules.LanguageDescriptor;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_AbstractRef;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingPriorityRule;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SDependency;
import org.jetbrains.mps.openapi.module.SDependencyScope;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class Generator extends ReloadableModuleBase {
  public static final Logger LOG = LogManager.getLogger(Generator.class);

  static {
    ModelsAutoImportsManager.registerContributor(new GeneratorModelsAutoImports());
  }

  @NotNull private Language mySourceLanguage;
  private GeneratorDescriptor myGeneratorDescriptor;

  public Generator(@NotNull Language sourceLanguage, GeneratorDescriptor generatorDescriptor) {
    super(sourceLanguage.getFileSystem());
    mySourceLanguage = sourceLanguage;
    initGeneratorDescriptor(generatorDescriptor);
  }

  //models will be named like xxx.modelName, where xxx is a part of newName before sharp symbol
  @Override
  public void rename(@NotNull String newName) {
    int sharp = newName.indexOf("#");
    newName = sharp < 0 ? newName : newName.substring(sharp);
    renameModels(getSourceLanguage().getModuleName(), newName, false);

    //see MPS-18743, need to save before setting descriptor
    getRepository().saveAll();

    // MPS-22787 - update generator id
    String uid = myGeneratorDescriptor.getNamespace();
    int sharpIndex = uid.indexOf('#');
    myGeneratorDescriptor.setNamespace(newName + "#" + uid.substring(sharpIndex + 1));
    // FIXME why there's no fireModuleRenamed() as in super.rename()???
  }

  @Override
  public boolean isPackaged() {
    return getSourceLanguage().isPackaged();
  }

  public List<SModel> getOwnTemplateModels() {
    List<SModel> templateModels = new ArrayList<>();
    for (SModel modelDescriptor : getModels()) {
      if (SModelStereotype.isGeneratorModel(modelDescriptor)) {
        templateModels.add(modelDescriptor);
      }
    }
    return templateModels;
  }

  @NotNull
  @Override
  public GeneratorDescriptor getModuleDescriptor() {
    return myGeneratorDescriptor;
  }

  @Override
  public IFile getModuleSourceDir() {
    return mySourceLanguage.getModuleSourceDir();
  }

  @Override
  public IFile getDescriptorFile() {
    return null;
  }

  @Override
  protected void doSetModuleDescriptor(ModuleDescriptor moduleDescriptor) {
    assert moduleDescriptor instanceof GeneratorDescriptor;
    LanguageDescriptor languageDescriptor = getSourceLanguage().getModuleDescriptor();
    int index = languageDescriptor.getGenerators().indexOf(getModuleDescriptor());
    languageDescriptor.getGenerators().remove(index);
    languageDescriptor.getGenerators().add(index, (GeneratorDescriptor) moduleDescriptor);
    getSourceLanguage().setModuleDescriptor(languageDescriptor);
  }

  public String getAlias() {
    String name = myGeneratorDescriptor.getAlias();
    return getSourceLanguage().getModuleName() + "/" + (name == null ? "<no name>" : name);
  }

  public static String generateGeneratorUID(Language sourceLanguage) {
    return sourceLanguage.getModuleName() + "#" + jetbrains.mps.smodel.SModel.generateUniqueId();
  }

  public Language getSourceLanguage() {
    return mySourceLanguage;
  }

  /**
   * @return <code>true</code> if templates for this generator should be generated into Java code instead of being interpreted at runtime
   */
  public boolean generateTemplates() {
    return myGeneratorDescriptor.isGenerateTemplates();
  }

  public String toString() {
    return getAlias() + " [generator]";
  }

  /**
   * fixme why generator saves language??
   * generator is contained in language it must be the other way around!
   */
  @Override
  public void save() {
    super.save();
    mySourceLanguage.save();
  }

  @Override
  public Iterable<SDependency> getDeclaredDependencies() {
    HashSet<SDependency> rv = new HashSet<>(IterableUtil.asCollection(super.getDeclaredDependencies()));
    final SRepository repo = getRepository();

    // generator sees its source language
    rv.add(new SDependencyImpl(mySourceLanguage.getModuleReference(), repo, SDependencyScope.DEFAULT, false));
    for (SModuleReference rt : mySourceLanguage.getRuntimeModulesReferences()) {
      rv.add(new SDependencyImpl(rt, repo, SDependencyScope.RUNTIME, false));
    }

    // generator sees all dependent generators as non-reexport
    for (SModuleReference refGenerator : getReferencedGeneratorUIDs()) {
      // XXX not sure it's right to resolve modules through global repository if this module is not attached anywhere
      // FIXME all referenced generators are of 'extends' dependency at the moment
      // but this might need a change once we store extended generators as a regular SDependency
      // instead of hacky getReferencedGeneratorUIDs
      rv.add(new SDependencyImpl(refGenerator, repo, SDependencyScope.EXTENDS, false));
    }
    return rv;
  }

  public List<SModuleReference> getReferencedGeneratorUIDs() {
    return new ArrayList<>(myGeneratorDescriptor.getDepGenerators());
  }

  public boolean deleteReferenceFromPriorities(org.jetbrains.mps.openapi.model.SModelReference ref) {
    boolean[] descriptorChanged = new boolean[]{false};
    Iterator<MappingPriorityRule> it = myGeneratorDescriptor.getPriorityRules().iterator();
    while (it.hasNext()) {
      MappingPriorityRule rule = it.next();
      MappingConfig_AbstractRef right = rule.getRight();
      MappingConfig_AbstractRef left = rule.getLeft();
      if (right.removeModelReference(ref, descriptorChanged) || left.removeModelReference(ref, descriptorChanged)) {
        it.remove();
      }
    }
    return descriptorChanged[0];
  }

  /**
   * Internal method, used from the process of re-validating generators from the language module.
   *
   * We cannot call Generator.setModuleDescriptor() method from there because it is implemented to call
   * Language.setModuleDescriptor() starting generators re-validation process.
   *
   * This method can be removed if we separate generator module persistence from the language module persistence.
   *
   */
  final void updateGeneratorDescriptor(GeneratorDescriptor generatorDescriptor) {
    initGeneratorDescriptor(generatorDescriptor);
    reloadAfterDescriptorChange();
  }

  private void initGeneratorDescriptor(GeneratorDescriptor generatorDescriptor) {
    myGeneratorDescriptor = generatorDescriptor;

    String uid = myGeneratorDescriptor.getNamespace();
    if (uid == null) {
      // FIXME is missing namespace/uuid a valid scenario? If yes, why no alias init then?
      myGeneratorDescriptor.setNamespace(generateGeneratorUID(mySourceLanguage));
    }

    ModuleId uuid = myGeneratorDescriptor.getId();
    if (uuid == null) {
      uuid = ModuleId.regular();
      myGeneratorDescriptor.setId(uuid);
    }
    setModuleReference(myGeneratorDescriptor.getModuleReference());
  }

  private static class GeneratorModelsAutoImports extends AutoImportsContributor<Generator> {
    @NotNull
    @Override
    public Class<Generator> getApplicableSModuleClass() {
      return Generator.class;
    }

    @Override
    public Set<SModel> getAutoImportedModels(Generator contextGenerator, SModel model) {
      // likely, one needs to reference concepts of the source language:
      if (SModelStereotype.isGeneratorModel(model)) {
        // FIXME MM, please tell me what to use instead! SModuleOperations.getAspect(SModule, "structure") isn't nice alternative for hand-written code.
        SModel structureAspect = LanguageAspect.STRUCTURE.get(contextGenerator.getSourceLanguage());
        if (structureAspect != null) {
          // XXX when source language used to be 'used language', we've imported all extended languages as well. Shall we
          // import structures of extended language modules here as well?
          return Collections.singleton(structureAspect);
        }
      }
      return Collections.emptySet();
    }

    @NotNull
    @Override
    public Collection<SLanguage> getLanguages(Generator contextGenerator, SModel model) {
      // languages we are going to write templates at are not known at this moment,
      // generator languages are imported with dedicated templates devkit
      return Collections.emptySet();
    }

    @Override
    public Collection<SModuleReference> getDevKits(Generator contextModule, SModel forModel) {
      return Collections.singleton(BootstrapLanguages.getGeneratorTemplatesDevKit());
    }
  }

  @Override
  public ClassLoader getRootClassLoader() {
    return mySourceLanguage.getRootClassLoader();
  }
}
