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

import jetbrains.mps.generator.runtime.TemplateModule;
import jetbrains.mps.smodel.language.GeneratorRuntime;
import jetbrains.mps.smodel.language.LanguageRegistry;
import jetbrains.mps.smodel.language.LanguageRuntime;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
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.module.SModuleReference;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Find out generators for a model according to set of languages model actually uses.
 * NOTE, this is internal facility and NOT AN API. Made public for the sake of debug/info actions.
 * @author Artem Tikhomirov
 */
public final class EngagedGeneratorCollector {
  private static final Logger LOG = LogManager.getLogger(GenerationPlan.class);

  @NotNull
  private final SModel myModel;
  private final List<SLanguage> myAdditionalLanguages;
  private Collection<SLanguage> myDirectLangUse;
  private Collection<TemplateModule> myEngagedGenerators;
  private final Set<SLanguage> myBadLanguages = new HashSet<>();
  // all generators found during the process, with possible duplicates
  // e.g. L1 with G1 and L2 with G2, both G1 and G2 extend G3, which would show up twice in this case
  private final List<EngagedGenerator> myEngagedTrace = new ArrayList<>();

  public EngagedGeneratorCollector(@NotNull SModel model, @Nullable Collection<SLanguage> additionalLanguages) {
    myModel = model;
    myAdditionalLanguages = additionalLanguages == null ? Collections.emptyList() : new ArrayList<>(additionalLanguages);
  }

  /**
   * @return list of languages actually used in the model, including those specified with 'engaged on generation' model property.
   */
  public Collection<SLanguage> getDirectlyUsedLanguages() {
    if (myDirectLangUse == null) {
      myDirectLangUse = ModelContentUtil.getUsedLanguages(myModel);
    }
    return myDirectLangUse;
  }

  /**
   * @return list of used languages including additional languages supplied {@linkplain EngagedGeneratorCollector externally} (if any)
   */
  public Collection<SLanguage> getAllLanguages() {
    if (myAdditionalLanguages.isEmpty()) {
      return getDirectlyUsedLanguages();
    }
    Collection<SLanguage> l1 = getDirectlyUsedLanguages();
    ArrayList<SLanguage> rv = new ArrayList<>(l1.size() + myAdditionalLanguages.size());
    rv.addAll(l1);
    rv.addAll(myAdditionalLanguages);
    return rv;
  }

  public Collection<TemplateModule> getGenerators() {
    if (myEngagedGenerators == null) {
      myEngagedGenerators = build();
    }
    return myEngagedGenerators;
  }

  @NotNull
  private Collection<TemplateModule> build() {
    myBadLanguages.clear();
    myEngagedTrace.clear();
    final Collection<SLanguage> initialLanguages = getAllLanguages();
    Queue<EngagedLanguage> queue = new ArrayDeque<>(resolveLanguages(initialLanguages, null, null));

    // XXX could have used Set<SLanguage> here, but it's not easy to get SLanguage from LanguageRuntime (which I use to walk extended langs)
    Set<String> processedLanguages = new HashSet<>(toQualifiedName(initialLanguages));

    // set of languages either used (and/or demanded) explicitly in the model we're about to generate,
    // and languages that may appear during generation process (e.g. by applying some of generators)
    Set<EngagedLanguage> participatingLanguages = new HashSet<>(queue);


    while (!queue.isEmpty()) {
      EngagedLanguage next = queue.remove();

      for (LanguageRuntime extendedLang : next.getLanguage().getExtendedLanguages()) {
        if (processedLanguages.add(extendedLang.getNamespace())) {
          final EngagedLanguage engaged = new EngagedLanguage(extendedLang, next, "EXTENDS");
          participatingLanguages.add(engaged);
          queue.add(engaged);
        }
      }

      HashSet<EngagedLanguage> targetLanguages = new HashSet<>();

      // collect extra languages from generator module description
      myEngagedTrace.addAll(collectGeneratorsAndTargetLanguages(next, targetLanguages));

      for (EngagedLanguage t : targetLanguages) {
        if (processedLanguages.add(t.getName())) {
          participatingLanguages.add(t);
          queue.add(t);
        }
      }
    }

    // collect unique template models
    ArrayList<TemplateModule> all = new ArrayList<>();
    HashSet<SModuleReference> processedGenerators = new HashSet<>(myEngagedTrace.size() * 2);
    for (EngagedGenerator m : myEngagedTrace) {
      final TemplateModule tm = m.getGenerator();
      if (processedGenerators.add(tm.getModuleReference())) {
        all.add(tm);
      }
    }
    return Collections.unmodifiableList(all);
  }

  private List<EngagedGenerator> collectGeneratorsAndTargetLanguages(EngagedLanguage lang, Set<EngagedLanguage> targetLanguages) {
    Collection<? extends  GeneratorRuntime> generators = lang.getLanguage().getGenerators();
    if (generators == null) {
      return Collections.emptyList();
    }

    ArrayList<EngagedGenerator> langGenerators = new ArrayList<>(2 + generators.size());


    // collect extra languages from generator module description
    for (GeneratorRuntime gr : generators) {
      if (false == gr instanceof TemplateModule) {
        continue;
      }
      final TemplateModule generator = (TemplateModule) gr;
      EngagedGenerator eg = new EngagedGenerator(generator, lang, "OWNED");
      langGenerators.add(eg);


      // handle Used languages
      targetLanguages.addAll(resolveLanguages(generator.getTargetLanguages(), eg, "GENERATES INTO"));
      //
      // handle referenced generators
      for (TemplateModule tm : generator.getExtendedGenerators()) {
        langGenerators.add(new EngagedGenerator(tm, eg, "EXTENDED GENERATOR"));
      }
      for (TemplateModule tm : generator.getEmployedGenerators()) {
        langGenerators.add(new EngagedGenerator(tm, eg, "EMPLOYED GENERATOR"));
      }
    }
    return langGenerators;
  }

  private Collection<EngagedLanguage> resolveLanguages(Collection<SLanguage> languages, EngagedElement origin, Object engagementKind) {
    ArrayList<EngagedLanguage> rv = new ArrayList<>(languages.size());
    final LinkedHashSet<SLanguage> toResolve = new LinkedHashSet<>(languages);
    for (SLanguage next : toResolve) {
      if (myBadLanguages.contains(next)) {
        // do not resolve more than once
        continue;
      }
      LanguageRuntime language = LanguageRegistry.getInstance().getLanguage(next);
      if (language == null) {
        if (origin == null) {
          final String msg = "Model %s uses language %s which is missing (likely is not yet generated or is a bootstrap dependency)";
          LOG.error(String.format(msg, myModel.getName(), next));
        } else {
          String msg = "One of generators engaged for model %s needs ('%s') missing language %s. Please check generator %s";
          LOG.error(String.format(msg, myModel.getName(), engagementKind, next, origin.getName()));
        }
        myBadLanguages.add(next);
      } else {
        rv.add(new EngagedLanguage(language, origin, engagementKind));
      }
    }
    return rv;
  }

  /**
   * Pumps debug information about engaged generators and the way they got activated as a string.
   * I know it's better to provide some sort of structured info, but now seems not worth the effort.
   */
  public void dump(Consumer<String> traceConsumer) {
    myEngagedTrace.forEach(l -> traceConsumer.accept(new StringBuilder().append(' ').append(l).toString()));
  }

  // cease existence once we get rid of strings completely
  private static Collection<String> toQualifiedName(Collection<SLanguage> languages) {
    return languages.stream().map(SLanguage::getQualifiedName).collect(Collectors.toList());
  }

  private abstract static class EngagedElement {
    protected final EngagedElement myOrigin;
    protected final Object myEngagementKind;

    protected EngagedElement(EngagedElement origin, Object engagementKind) {
      myOrigin = origin;
      myEngagementKind = engagementKind;
    }

    public abstract String getName();

    public EngagedElement getOrigin() {
      return myOrigin;
    }

    @Override
    public String toString() {
      String msg = myOrigin == null ? "%s: %s" : "%s: %s as %s through [%s]";
      return String.format(msg, getClass().getSimpleName(), getName(), myEngagementKind, myOrigin);
    }
  }

  private static class EngagedLanguage extends EngagedElement {
    private final LanguageRuntime myLang;

    EngagedLanguage(@NotNull LanguageRuntime lang, @Nullable EngagedElement origin, @Nullable Object engagementKind) {
      super(origin, engagementKind);
      myLang = lang;
    }

    public LanguageRuntime getLanguage() {
      return myLang;
    }

    // official lang name aka namespace
    public String getName() {
      return myLang.getNamespace();
    }
  }

  private static class EngagedGenerator extends EngagedElement {
    private final TemplateModule myGenerator;

    EngagedGenerator(@NotNull TemplateModule generator, @NotNull EngagedElement origin, @Nullable Object engagementKind) {
      super(origin, engagementKind);
      myGenerator = generator;
    }

    public TemplateModule getGenerator() {
      return myGenerator;
    }

    public String getName() {
      return myGenerator.getAlias();
    }
  }
}
