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

import jetbrains.mps.smodel.BootstrapLanguages;
import jetbrains.mps.smodel.adapter.ids.SLanguageId;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import jetbrains.mps.smodel.runtime.ILanguageAspect;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.module.SModuleReference;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Runtime representation of a language, extension point for various language aspects.
 * Dependency from typesystem, find usages and other language aspects is transitional, eventually (after 3.2)
 * this class shall be generic and aware of {@link jetbrains.mps.smodel.runtime.ILanguageAspect} only.
 * It shall not load any classes through reflection (any class-loading of generated code/aspects is responsibility of
 * generated language runtime class).
 * <p/>
 * Language runtime keeps track of aspects queried (instantiates them lazily).
 */
public abstract class LanguageRuntime {
  private final ConcurrentMap<Class<? extends ILanguageAspect>, ILanguageAspect> myAspectDescriptors = new ConcurrentHashMap<>();
  // FIXME AP: is there a contract on duplication????
  private final List<LanguageRuntime> myExtendingLanguages = new ArrayList<>();
  private final List<LanguageRuntime> myExtendedLanguages = new ArrayList<>();

  /**
   * @return full name of the language, never {@code null}.
   */
  public abstract String getNamespace();

  /**
   * @return identity of the language, never {@code null}. Generated LanguageRuntime classes shall override return value
   */
  public abstract SLanguageId getId(); // FIXME supply a cons that takes mandatory values (id, name), rather than overriding methods in generated classes

  /**
   * Generated LanguageRuntime classes shall override this method
   * Denoted with @ToRemove just to ease later discovery, it's method implementation to be removed, not the method itself
   *
   * @return 0 now
   * @since 3.2
   */
  public abstract int getVersion();

  public Collection<? extends GeneratorRuntime> getGenerators() {
    ArrayList<GeneratorRuntime> rv = new ArrayList<>(4);
    populateRegisteredGenerators(rv);
    return rv;
  }

  /**
   * Provide aspect instance associated with the language. Aspect is instantiated only once, lazily (the first time asked)
   * and the same instance is returned for each subsequent calls.
   * <p>
   * At the moment, sole mechanism to supply new aspect is code in generated language runtime subclass (i.e. there's no mechanism yet to
   * add aspects dynamically).
   * <p>
   * Calls {@link LanguageRuntimeAware#setLanguageRuntime(LanguageRuntime)} on aspects implementing {@link LanguageRuntimeAware} after creation, passing itself
   * as the parameter.
   *
   * @param aspectClass identifies aspect to retrieve
   * @param <T>         subtype of {@link jetbrains.mps.smodel.runtime.ILanguageAspect}
   * @return instance of aspect implementation if there's one for the language
   * @see #createAspect(Class)
   * @see jetbrains.mps.smodel.runtime.ILanguageAspect
   */
  public final <T extends ILanguageAspect> T getAspect(@NotNull Class<T> aspectClass) {
    try {
      T aspectDescriptor = aspectClass.cast(myAspectDescriptors.get(aspectClass));
      if (aspectDescriptor == null) {
        aspectDescriptor = createAspect(aspectClass);
        if (aspectDescriptor == null) {
          return null;
        }
        if (aspectDescriptor instanceof LanguageRuntimeAware) {
          ((LanguageRuntimeAware) aspectDescriptor).setLanguageRuntime(this);
        }
        T alreadyThere = aspectClass.cast(myAspectDescriptors.putIfAbsent(aspectClass, aspectDescriptor));
        if (alreadyThere != null) {
          return alreadyThere;
        }
      }
      return aspectDescriptor;
    } catch (Throwable th) {
      String msg = String.format("Failed to instantiate aspect %s in language %s", aspectClass, getNamespace());
      Logger.getLogger(LanguageRuntime.class).error(msg, th);
      return null;
    }
  }

  /**
   * Method every language shall implement to tell its capabilities.
   * Implementation doesn't need to keep state, {@link #getAspect(Class)} does that.
   * @param aspectClass never null identifying interface of the aspect
   * @param <T> aspect class
   * @return may return {@code null} indicating language has no such aspect
   */
  protected abstract <T extends ILanguageAspect> T createAspect(Class<T> aspectClass);

  /*
   * perhaps, could use WeakHashMap, although proper registration/un-registration sequence shall enforce no stale entries
   */
  private final Map<SModuleReference, GeneratorRuntime> myRegisteredGenerators = new HashMap<>();

  protected final void populateRegisteredGenerators(List<? super GeneratorRuntime> consumer) {
    consumer.addAll(myRegisteredGenerators.values());
  }

  /*package*/ void register(GeneratorRuntime runtime) {
    myRegisteredGenerators.put(runtime.getModuleReference(), runtime);
  }

  /*package*/ void unregister(GeneratorRuntime runtime) {
    myRegisteredGenerators.remove(runtime.getModuleReference());
  }

  /**
   * Closure of all languages that extend this one, exclusive.
   *
   * @return unmodifiable collection of languages
   */
  @NotNull
  public Iterable<LanguageRuntime> getExtendingLanguages() {
    return myExtendingLanguages;
  }

  /**
   * Closure of all languages this language extends, exclusive.
   * Referenced languages are from the same LanguageRegistry as this one.
   * (Although there's only one LanguageRegistry at the moment, it's likely to change in the future)
   * <p/>
   * Collection captures only languages actually available, and might not reflect all dependencies of the language, i.e.
   * presents state of language relationship through a LanguageRegistry perspective. E.g. if language descriptor states 'extends' dependency
   * from a language missing in the LanguageRegistry instance, that extended language will be ignored and collection returned won't mention it.
   *
   * @return unmodifiable collection of languages
   */
  @NotNull
  public Collection<LanguageRuntime> getExtendedLanguages() {
    return Collections.unmodifiableCollection(myExtendedLanguages);
  }

  /**
   * @deprecated override {@link #fillExtendedLanguages(Collection)} instead.
   */
  @Deprecated
  @ToRemove(version = 3.5)
  protected String[] getExtendedLanguageIDs() {
    // non-abstract to facilitate transition
    return new String[0];
  }

  /**
   * Subclasses shall override to report languages they extend (fill in supplied collection).
   * It's not necessary to override the method if language doesn't extend any other, not to invoke super, this method is no-op.
   * <p/>
   * DESIGN NOTE: while it's sufficient to know SLanguageId only, I stick to SLanguage to keep namespace as debug information.
   *              Perhaps, shall pass an object that could take different alternatives (e.g. SLanguageId, (long,long), module reference)?
   */
  protected void fillExtendedLanguages(Collection<SLanguage> extendedLanguages) {
    // intentionally non-abstract and no-op
  }

  private void registerExtendingLanguage(LanguageRuntime extendingLanguage) {
    myExtendingLanguages.add(extendingLanguage);
    extendingLanguage.myExtendedLanguages.add(this);
  }

  @ToRemove(version = 3.5)
  private void fillLegacyExtendedLanguages(Collection<SLanguage> extendedLanguages, LanguageRegistry registry) {
    for (String namespace : getExtendedLanguageIDs()) {
      LanguageRuntime l = registry.getLanguage(namespace);
      if (l != null) {
        extendedLanguages.add(MetaAdapterFactory.getLanguage(l.getId(), l.getNamespace()));
      }
    }
  }

  void initialize(LanguageRegistry registry) {
    Queue<SLanguage> extendedLanguageIDs = new ArrayDeque<>();
    fillExtendedLanguages(extendedLanguageIDs);
    fillLegacyExtendedLanguages(extendedLanguageIDs, registry);
    Set<SLanguageId> visitedLanguages = new HashSet<>();
    visitedLanguages.add(getId());
    while (!extendedLanguageIDs.isEmpty()) {
      SLanguage nextLanguageID = extendedLanguageIDs.remove();
      LanguageRuntime extendedLanguage = registry.getLanguage(nextLanguageID);
      if (extendedLanguage != null && visitedLanguages.add(extendedLanguage.getId())) {
        extendedLanguage.registerExtendingLanguage(this);
        extendedLanguage.fillExtendedLanguages(extendedLanguageIDs);
        extendedLanguage.fillLegacyExtendedLanguages(extendedLanguageIDs, registry);
      }
    }
    // generally, should never happen, but doesn't hurt to ensure exclusive contract of getExtended/getExtendingLanguages()
    myExtendingLanguages.remove(this);
    myExtendedLanguages.remove(this);
    // Here's a copy of the hack from smodel.Language#getExtendedLanguageRefs(). We used to manifest lang.core
    // as extended language for any other language module, and once we switched to SLanguage, shall do the same at least for compatibility reasons.
    // Once we generate this extends inside #getExtendedLanguageIDs() (better the new one that yield SLanguage instead of String),
    // AND there are no old LanguageRuntime classes (i.e. past MPS 3.3), the hack shall cease to exist.
    //
    // XXX OTOH, does it make sense to force generation of explicit extends lang.core in each language?
    LanguageRuntime langCore = registry.getLanguage(BootstrapLanguages.getLangCore());
    assert langCore != null;
    if (this != langCore && !visitedLanguages.contains(langCore.getId())) {
      langCore.registerExtendingLanguage(this);
    }
  }

  void deinitialize() {
    myExtendingLanguages.clear();
    myExtendedLanguages.clear();
  }

  @Override
  public String toString() {
    return getNamespace() + " runtime";
  }
}
