/*
 * Copyright 2003-2025 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.extapi.module.SRepositoryExt;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.module.SDependencyImpl;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.io.DescriptorIO;
import jetbrains.mps.project.io.DescriptorIOFacade;
import jetbrains.mps.project.structure.modules.GeneratorDescriptor;
import jetbrains.mps.project.structure.modules.LanguageDescriptor;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import jetbrains.mps.smodel.language.LanguageAspectSupport;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.SDependency;
import org.jetbrains.mps.openapi.module.SDependencyScope;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class Language extends AbstractModule implements ReloadableModule {

  /**
   * Default, although not mandatory location we save our models to.
   * Made public just for the sake of tests.
   */
  public static final String LANGUAGE_MODELS = "models";
  /**
   * @deprecated Use of default value to detect aspect source root or to check module existence is wrong.
   */
@Deprecated(since = "3.3", forRemoval = true)
  public static final String LEGACY_LANGUAGE_MODELS = "languageModels";

  @NotNull private LanguageDescriptor myLanguageDescriptor;
  // modifications are guarded with model write lock, assertCanChange()
  private final List<Generator> myAttachedGenerators = new ArrayList<>(2);

  protected Language(@NotNull LanguageDescriptor descriptor, @Nullable IFile file) {
    super(file);
    myLanguageDescriptor = descriptor;
    setModuleReference(descriptor.getModuleReference());
  }

  @Override
  public void reloadAfterDescriptorChange() {
    super.reloadAfterDescriptorChange();
    revalidateGenerators();
  }

  public void addExtendedLanguage(@NotNull SModuleReference langRef) {
    if (this.getModuleReference().equals(langRef) || myLanguageDescriptor.getExtendedLanguages().contains(langRef)) {
      return;
    }
    LanguageDescriptor moduleDescriptor = getModuleDescriptor();
    moduleDescriptor.getExtendedLanguages().add(langRef);

    setChanged();

    fireChanged();
  }

  public Set<SModuleReference> getExtendedLanguageRefs() {
    HashSet<SModuleReference> res = new HashSet<>(myLanguageDescriptor.getExtendedLanguages());
    if (!BootstrapLanguages.coreLanguageRef().equals(getModuleReference())) {
      //this is needed now as we don't force the user to have an explicit dependency on core
      res.add(BootstrapLanguages.coreLanguageRef());
    }
    return res;
  }

  @Override
  public Iterable<SDependency> getDeclaredDependencies() {
    HashSet<SDependency> rv = new HashSet<>(IterableUtil.asCollection(super.getDeclaredDependencies()));
    final SRepository repo = getRepository();
    for (SModuleReference language : getExtendedLanguageRefs()) {
      // XXX not clear whether it's worth including implicit "extends lang.core" (see getExtendedLanguageRefs())
      // or adhere to 'declared' in the name of getDeclaredDependencies and use myLanguageDescriptor.getExtendedLanguages() only
      rv.add(new SDependencyImpl(language, repo, SDependencyScope.EXTENDS, true));
    }
    return rv;
  }

  /**
   * All the language modules extended by this one within the same repository this module is attached to.
   * For detached module, the set returned is empty. To access 'raw' information about extended languages,
   * one could use {@link #getExtendedLanguageRefs()}.
   *
   * This method requires model read access as it resolves modules.
   *
   * IMPORTANT: if any extended language is missing from the repository of the module, it's simply ignored and not included into outcome
   * (nor the closure of its extended languages).
   *
   * NOTE, implementation hides cyclic dependencies between languages, e.g if "A extends B extends A",
   * you'd get "A extends B" for A and "B extends A" for B.
   */
  @NotNull
  public Set<Language> getAllExtendedLanguages() {
    HashSet<Language> languages = new HashSet<>();
    final SRepository repository = getRepository();
    if (repository == null) {
      return languages;
    }

    ArrayDeque<Language> queue = new ArrayDeque<>();
    queue.add(this);

    do {
      Language current = queue.poll();
      if (!languages.add(current)) {
        continue;
      }
      for (SModuleReference lr : current.getExtendedLanguageRefs()) {
        final SModule l = lr.resolve(repository);
        if (l instanceof Language) {
          queue.add((Language) l);
        }
      }
    } while (!queue.isEmpty());

    return languages;
  }

  public Collection<SModuleReference> getRuntimeModulesReferences() {
    return Collections.unmodifiableSet(myLanguageDescriptor.getRuntimeModules());
  }


  @Override
  public void attach(@NotNull SRepository repository) {
    super.attach(repository);
    final SLanguage mineDeployedIdentity = MetaAdapterFactory.getLanguage(getModuleReference());
    for (Generator generator : new ModuleRepositoryFacade(repository).getAllModules(Generator.class)) {
      if (!mineDeployedIdentity.equals(generator.sourceLanguage())) {
        continue;
      }
      generator.setSourceLanguageInstance(this);
    }
  }



  /*
   * Update repository generator modules associated with this language with descriptors known to the language (registers new generators, if necessary)
   * This is another place in addition to ModulesMiner that knows about language-generator MD containment
   */
  private void revalidateGenerators() {
    if (myLanguageDescriptor.getDeploymentDescriptor() != null) {
      // do not process generators listed in a source descriptor of a deployed language, assume generators
      // are managed on their own (use of source MD in case of deployed module is sort of design defect we can hardly fix, but at least
      // we shall not use information sored therein if we know that generators are treated separately when language got module.xml DD)
      // Perhaps, this could be approached in another way, by using getOwnedGenerators (see below) and not reporting GD for a LD read from
      // source along with language's DD (ModulesMiner#loadDeploymentDescriptor). I like this approach more as it would keep knowledge
      // about deployment inside MM, however, I'm quite sure that would ruin some code that relies on Language's knowledge about its generators.
      return;
    }
    if (getRepository() == null) {
      // detached module, can not do anything about registration/un-registration
      // FIXME perhaps, whole reloadAfterDescriptorChange has to be guarded and get executed for attached modules only, including facet reload
      //       However, it's too much of a change right before the release, shall try in master, instead.
      //       Besides, CopyModuleHelper approach with model roots copied for instantiated modules is dubious, why can't
      //       we copy model root descriptors instead, and have it done prior to module instantiation? In this case,
      //       we build whole descriptor first, instantiate and register with project, no chances to get here w/o detached module instance.
      return;
    }
    // Fair implementation shall deal with getOwnedGenerators() only, however, at the moment, Generator module needs its source Language module
    // and it's tricky to write external code that would deal with standalone/external generators when language's MD changes (there's no proper notification
    // mechanism or anything else to react to MD change). That's why we control all generators associated with the language here.
    //
    // Another important note here is that getOwnedGenerators() in its present state may not give proper answer (e.g. if a changed MD doesn't list a generator
    // module that has been previously exposed
    LinkedList<Generator> existingGenerators = new LinkedList<>(getGenerators());

    SRepositoryExt moduleRepository = (SRepositoryExt) getRepository();
    ModuleRepositoryFacade mrf = new ModuleRepositoryFacade(moduleRepository);
    for (GeneratorDescriptor nextDescriptor : myLanguageDescriptor.getGenerators()) {
      Generator nextGenerator = null;
      for (Iterator<Generator> it = existingGenerators.iterator(); it.hasNext(); ) {
        // looking for the existing generator with same ID
        Generator nextGeneratorCandidate = it.next();
        GeneratorDescriptor nextGeneratorCandidateDescriptor = nextGeneratorCandidate.getModuleDescriptor();
        if (Objects.equals(nextGeneratorCandidateDescriptor.getId(), nextDescriptor.getId())) {
          nextGenerator = nextGeneratorCandidate;
          it.remove();
          break;
        }
      }

      if (nextGenerator != null) {
        nextGenerator.updateGeneratorDescriptor(nextDescriptor);
      } else {
        // new generator is registered with the same owners as this language
        // at the moment, we may use old cons that doesn't take explicit descriptor file (as it uses the one from this language, which is what we need there),
        // but it's fun to try a new one
        Generator generator = new Generator(MetaAdapterFactory.getLanguage(getModuleReference()), nextDescriptor, getDescriptorFile(), this);
        for (MPSModuleOwner moduleOwner : mrf.getModuleOwners(this)) {
          moduleRepository.registerModule(generator, moduleOwner);
        }
      }
    }
    // stale generator modules are unregistered from all owners
    // here we assume standalone generator modules could exist without their language
    // therefore we unregister only generator modules that are not standalone, assuming they originally came from MD of this very language.
    for (Generator stale : existingGenerators) {
      if (stale.getModuleDescriptor().isStandaloneModule()) {
        continue;
      }
      mrf.unregisterModule(stale);
    }
  }

  @Override
  public void dispose() {
    // though MPSModuleRepository.doUnregisterModule() cares to unregister language's generators properly, seems it
    // doesn't hurt to try to unregister them here as well. Either it would end up as no-op for an empty collection, or would keep
    // repository consistent (in case dispose() has been reached not through MPSModuleRepository)
//    final SRepository repo = getRepository();
//    if (repo != null) {
//      final ModuleRepositoryFacade mrf = new ModuleRepositoryFacade(repo);
//      // see revalidateGenerators(), above, for reasons why we unregister all associated generators, not only directly owned.
//      getGenerators().forEach(mrf::unregisterModule);
//    }
    super.dispose();
  }

  @NotNull
  @Override
  public LanguageDescriptor getModuleDescriptor() {
    return myLanguageDescriptor;
  }

  @Override
  public void doSetModuleDescriptor(ModuleDescriptor moduleDescriptor) {
    assert moduleDescriptor instanceof LanguageDescriptor;
    myLanguageDescriptor = (LanguageDescriptor) moduleDescriptor;
    SModuleReference reference = new jetbrains.mps.project.structure.modules.ModuleReference(myLanguageDescriptor.getNamespace(), myLanguageDescriptor.getId());
    setModuleReference(reference);
    if (getRepository() instanceof MPSModuleRepository) {
      ((MPSModuleRepository) getRepository()).invalidateCaches();
    }
  }

  public int getLanguageVersion() {
    return getModuleDescriptor().getLanguageVersion();
  }

  public void setLanguageVersion(int version) {
    getModuleDescriptor().setLanguageVersion(version);
    fireChanged();
    setChanged();
  }

  /**
   * @return all generators that treat this language as their source one.
   */
  public Collection<Generator> getGenerators() {
    assertCanRead();
    // Language module now tracks Generator modules it is owner to. Generator modules, once attached to a repo, tell their source language they are here
    // with #register(Generator), and tell they are gone with #unregister(Generator).
    return new ArrayList<>(myAttachedGenerators);
  }

  /**
   * PROVISIONAL API, DON'T USE UNLESS YOU'RE 100% SURE WHAT IS THE REASON FOR THAT, AND WHAT'S THE (UPCOMING) DIFFERENCE WITH {@link #getGenerators()}
   * NOTE: BE CAREFUL WHEN INVOKED FROM A CODE THAT REACTS TO MODULE DESCRIPTOR CHANGES
   *       if invoked with a changed MD, gives state according to MD contents, and not the one exposed in the repository (think about scenario when
   *       a repo-registered, language-owned generator has been removed from MD. This method would give empty set despite the fact generator module is there)
   * @return generators declared and controlled by this language module.
   */
  public Collection<Generator> getOwnedGenerators() {
    Set<SModuleReference> ownedGenerators = getModuleDescriptor().getGenerators().stream().map(ModuleDescriptor::getModuleReference).collect(Collectors.toSet());
    return getGenerators().stream().filter(g -> ownedGenerators.contains(g.getModuleReference())).collect(Collectors.toList());
  }

  /**
   * @deprecated method is not bad per se (Language module could tell SNode with concept declaration. However,
   *            it silently excludes Interface concepts, and likely its uses need attention and switch to SConcept.
   *            Then, we could decide whether we truly need access to language's concept nodes this way, or shall use
   *            LanguageAspects instead.
   */
@Deprecated(since = "3.4", forRemoval = true)
  public List<SNode> getConceptDeclarations() {
    // FIXME there are uses in mbeddr
    SModel structureModel = getStructureModelDescriptor();
    if (structureModel == null) return Collections.emptyList();
    return FastNodeFinderManager.get(structureModel).getNodes(SNodeUtil.concept_ConceptDeclaration, true);
  }

  public List<SModel> getUtilModels() {
    Set<SModel> models = new HashSet<>(getModels());
    models.removeAll(LanguageAspectSupport.getAspectModels(this));
    models.removeAll(getAccessoryModels());

    List<SModel> result = new ArrayList<>(models.size());
    for (SModel md : models) {
      if (SModelStereotype.isStubModel(md) || SModelStereotype.isDescriptorModel(md)) {
        // perhaps, we need more generic isPredefinedStereotypeMPS()
        continue;
      }
      result.add((md));
    }
    return result;
  }

  public SModel getStructureModelDescriptor() {
    return LanguageAspect.STRUCTURE.get(this);
  }

  /**
   * fixme why generator saves language??
   * generator is contained in language it must be the other way around!
   */
  @Override
  public void save() {
    super.save();
    if (isReadOnly() || getDescriptorFile() == null) {
      return;
    }

    if (myLanguageDescriptor.getLoadException() != null){
      return;
    }

    try {
      DescriptorIO<LanguageDescriptor> io = new DescriptorIOFacade().standardProvider().languageDescriptorIO();
      io.writeToFile(getModuleDescriptor(), getDescriptorFile());
    } catch (Exception ex) {
      Logger.getLogger(getClass()).error("Save failed", ex);
    }
  }

  public List<SModel> getAccessoryModels() {
    List<SModel> result = new LinkedList<>();
    for (SModelReference model : getModuleDescriptor().getAccessoryModels()) {
      SModel modelDescriptor = model.resolve(getRepository());
      if (modelDescriptor != null) {
        result.add(modelDescriptor);
      }
    }
    return result;
  }

  public boolean isAccessoryModel(org.jetbrains.mps.openapi.model.SModelReference modelReference) {
    return myLanguageDescriptor.getAccessoryModels().stream().anyMatch(Predicate.isEqual(modelReference));
  }

  public void removeAccessoryModel(org.jetbrains.mps.openapi.model.SModel sm) {
    // XXX why removal of accessory model is not done through ModuleDescriptor as other editing activities?
    //     i.e. module properties add accessory models through MD, but remove them through Language, which is odd.
    final SModelReference accessoryModelRef = sm.getReference();
    boolean changed = myLanguageDescriptor.getAccessoryModels().removeIf(accessoryModelRef::equals);
    if (changed) {
      // XXX Perhaps, setModuleDescriptor is too much, as it fires changed + dependenciesChange and eventually reloads classes,
      //     while change in accessory models unlikely to affect any compiled class.
      //     I'd stick to setChanged(true) + fireChanged() here, instead.
      setModuleDescriptor(myLanguageDescriptor);
    }
  }

  public String toString() {
    return getModuleName() + " [language]";
  }

@Deprecated(since = "3.3", forRemoval = true)
  //no full equivalent to this method, use appropriate method from LanguageAspectSupport
  private LanguageAspect getAspectForModel(@NotNull org.jetbrains.mps.openapi.model.SModel sm) {
    for (LanguageAspect la : LanguageAspect.values()) {
      if (la.get(this) == sm) {
        return la;
      }
    }
    return null;
  }

  public static Language getLanguageForLanguageAspect(org.jetbrains.mps.openapi.model.SModel modelDescriptor) {
    return getLanguageFor(modelDescriptor);
  }

@Deprecated(since = "3.3", forRemoval = true)
  //no full equivalent to this method, use appropriate method from LanguageAspectSupport
  //no usages in MPS, 4 uses in mbeddr
  @Nullable
  public static LanguageAspect getModelAspect(org.jetbrains.mps.openapi.model.SModel sm) {
    if (sm == null) return null;
    SModule module = sm.getModule();
    if (!(module instanceof Language)) {
      return null;
    }

    Language l = (Language) module;
    return l.getAspectForModel(sm);
  }

  public static boolean isLanguageOwnedAccessoryModel(org.jetbrains.mps.openapi.model.SModel sm) {
    SModule modelOwner = sm.getModule();
    if (modelOwner instanceof Language) {
      Language l = (Language) modelOwner;
      return l.isAccessoryModel(sm.getReference());
    }
    return false;
  }

  public static Language getLanguageFor(org.jetbrains.mps.openapi.model.SModel sm) {
    SModule owner = sm.getModule();
    if (owner instanceof Language) {
      return (Language) owner;
    }
    return null;
  }

  // TODO
//  @Nullable
//  @Override
//  public Language clone(String targetRoot, String targetNamespace) {
//    LanguageDescriptor targetDescriptor = new LanguageDescriptor();
//    IFile targetDescriptorFile = getFileSystem().getFile(targetRoot + File.separator + targetNamespace + MPSExtentions.DOT_LANGUAGE);
//
//    targetDescriptor.setId(ModuleId.regular());
//    targetDescriptor.setNamespace(targetNamespace);
//    getModuleDescriptor().cloneTo(targetDescriptor, PathConverters.forDescriptorFiles(targetDescriptorFile, getDescriptorFile()));
//    LanguageDescriptorPersistence.saveLanguageDescriptor(targetDescriptorFile, targetDescriptor, MacrosFactory.forModuleFile(targetDescriptorFile));
//
//    Language targetLanguage = new Language(targetDescriptor, targetDescriptorFile);
//    ModelRootCloneUtil.cloneModelRootsTo(getModelRoots(), targetLanguage);
//
//    Iterator<Generator> targetGenerators = targetLanguage.getGenerators().iterator();
//    for (Generator generator : getGenerators()) {
//      Generator targetGenerator = targetGenerators.next();
//      ModelRootCloneUtil.cloneModelRootsTo(generator.getModelRoots(), targetGenerator);
//    }
//
//    FIXME RADIMIR rename models here
//
//    return targetLanguage;
//  }


  /*package*/ void register(@NotNull Generator generator) {
    assertCanChange();
    myAttachedGenerators.add(generator);
  }
  /*package*/ void unregister(@NotNull Generator generator) {
    assertCanChange();
    if (!myAttachedGenerators.remove(generator)) {
      throw new IllegalStateException(String.format("Generator %s has not been previously registered with the language %s", generator.getModuleName(), getModuleName()));
    }
  }

  public static class LanguageModelsAutoImports extends jetbrains.mps.project.ModelsAutoImportsManager.AutoImportsContributor {
    @Override
    public boolean isApplicable(SModule module) {
      return module instanceof Language;
    }

    @NotNull
    @Override
    public Collection<SLanguage> getLanguages(SModule contextModule, SModel model) {
      return LanguageAspectSupport.getMainLanguages(model);
    }

    @Override
    public Collection<SModuleReference> getDevKits(SModule contextModule, SModel forModel) {
      SModuleReference defaultDevkit = LanguageAspectSupport.getDefaultDevkit(forModel);
      if(defaultDevkit != null) {
        return Collections.singleton(defaultDevkit);
      }
      return Collections.singleton(BootstrapLanguages.getGeneralPurposeDevKit());
    }
  }
}
