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

import jetbrains.mps.generator.GenerationFacade;
import jetbrains.mps.project.Solution;
import jetbrains.mps.project.dependency.VisibilityUtil;
import jetbrains.mps.project.validation.ValidationProblem.Severity;
import jetbrains.mps.smodel.BootstrapLanguages;
import jetbrains.mps.smodel.ConceptDeclarationScanner;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.LanguageAspect;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import org.jetbrains.annotations.NotNull;
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.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.util.Processor;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.stream.Collectors;

/**
 * Checks for Language (source) module.
 * Code extracted from ValidationUtil full of statics (which in turn melded distinct validation classes)
 * and is revival of LanguageValidator that used to be there till March 18, 2015.
 * @author Artem Tikhomirov
 * @since 3.4
 */
public class LanguageValidator {
  private final Language myLanguage;
  private final SRepository myRepository;
  private final Processor<ValidationProblem> myProcessor;

  // XXX Decide whether to have per-language instance of validator or to pass language as validate() argument.
  // Is there anything we can reuse in former case? If we would pass progress monitor, however, there might be too many arguments in the latter.
  public LanguageValidator(@NotNull Language language, @NotNull SRepository repository, @NotNull Processor<ValidationProblem> processor) {
    myLanguage = language;
    myRepository = repository;
    myProcessor = processor;
  }


  public void validate() {
    if (!ValidationUtil.validateAbstractModule(myLanguage, myProcessor)) {
      // FIXME validation of abstract module has to be in superclass.
      return;
    }

    ArrayDeque<Language> extendedLanguages = new ArrayDeque<>();
    Collection<SModuleReference> extendedLanguagesFromStructure = getActuallyExtendedLanguagesFromStructure();
    for (SModuleReference el : myLanguage.getExtendedLanguageRefs()) {
      if (!extendedLanguagesFromStructure.contains(el) && !BootstrapLanguages.coreLanguageRef().equals(el)) {
        // Language.getExtendedLanguageRefs() adds implicitly extended lang.core, we don't need to warn about it.
        // Perhaps, lang.core has not be part of getExtendedLanguageRefs(), but added at RT only? Do we need to manifest it at source level? To reference
        // core stuff without direct import?
        if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Superficial extended module %s, not referenced from structure aspect", el.getModuleName())))) {
          return;
        }
      }
      final SModule resolved = el.resolve(myRepository);
      if (resolved instanceof Language) {
        extendedLanguages.add((Language) resolved);
        continue;
      }
      if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format(resolved == null ? "Can't find extended language: %s" : "Module %s is not a language, can't extend it", el.getModuleName())))) {
        return;
      }
    }

    // XXX why it's essential to have behavior aspects in extended languages?
    HashSet<Language> visited = new HashSet<>();
    if (LanguageAspect.BEHAVIOR.get(myLanguage) == null) {
      if (!myProcessor.process(new ValidationProblem(Severity.ERROR, "Behavior aspect is absent"))) {
        return;
      }
    }
    visited.add(myLanguage);

    while (!extendedLanguages.isEmpty()) {
      Language l = extendedLanguages.removeFirst();
      if (l == myLanguage) {
        if (!myProcessor.process(new ValidationProblem(Severity.WARNING, "Cycle in extended language hierarchy"))) {
          return;
        }
      }
      if (!visited.add(l)) {
        continue;
      }
      for (SModuleReference el : l.getExtendedLanguageRefs()) {
        final SModule resolved = el.resolve(myRepository);
        if (resolved instanceof Language) {
          extendedLanguages.add((Language) resolved);
        }
      }
      SModel descriptor = LanguageAspect.BEHAVIOR.get(l);
      if (descriptor != null) {
        continue;
      }
      if (!myProcessor.process(new ValidationProblem(Severity.ERROR, "Cannot extend language without behavior aspect: " + l.getModuleName()))) {
        return;
      }
    }


    for (SModuleReference mr : myLanguage.getRuntimeModulesReferences()) {
      SModule runtimeModule = mr.resolve(myRepository);
      if (runtimeModule == null) {
        if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Missing runtime module %s", mr.getModuleName())))) {
          return;
        }
        continue;
      }
      if (runtimeModule instanceof Solution) {
        continue;
      }

      if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format("Runtime module %s is not a solution", runtimeModule)))) {
        return;
      }
    }

    for (SModelReference accessory : myLanguage.getModuleDescriptor().getAccessoryModels()) {
      //this check is wrong in common as we don't know what the user wants to do with the acc model in build.
      //but I'll not delete it until accessories removal just to have some warning on project consistency
      SModel accModel = accessory.resolve(myRepository);
      if (accModel == null) {
        if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Missing accessory model %s", accessory.getModelName())))) {
          return;
        }
        continue;
      }
      if (!VisibilityUtil.isVisible(myLanguage, accModel)) {
        if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format("Accessory model %s is not visible in the module", accessory.getModelName())))) {
          return;
        }
      }
      if (accModel.getModule() == myLanguage) {
        // Towards clear scenario to use accessory models: we intend them to be design-time auxiliary elements, with no generated code generally.
        // Generally, accessory models are written in a language they are part of. However, we can't generate them as there's no generator the moment language is built
        // (although it's a bit superficial limitation, due to build dependency chunk being of module size). Generally, nobody could expect to build reasonable code
        // from language's accessory model which uses the language, but it doesn't hurt to report an error so that LD get a chance to understand what's going on.
        if (GenerationFacade.canGenerate(accModel)) {
          SLanguage checkedLanguage = MetaAdapterFactory.getLanguage(myLanguage.getModuleReference());
          if (SModelOperations.getAllLanguageImports(accModel).contains(checkedLanguage)) {
            // accessory model written in a language it's part of. We could not possibly generate this model.
            if (!myProcessor.process(new ValidationProblem(Severity.ERROR, String.format("Accessory model %s uses language it's part of. Mark the model as 'do not generate' to avoid unnecessary bootstrap dependency", accModel.getName())))) {
              return;
            }
            return;
          } else {
            // just a model to generate some code, not the best way to utilize accessory models
            if (!myProcessor.process(new ValidationProblem(Severity.WARNING, String.format("Accessory models are deemed design-time facility and generally shall be marked as 'do not generate': %s", accModel.getName())))) {
              return;
            }
          }
        }
      }
    }
  }

  /**
   * TODO For the time being, we just look at structure model dependencies from other structure models.
   *      However, we shall look into actual references to tell e.g. aggregation of foreign concept from extension
   */
  private Collection<SModuleReference> getActuallyExtendedLanguagesFromStructure() {
    SModel structureModel = LanguageAspect.STRUCTURE.get(myLanguage);
    if (structureModel == null) {
      return Collections.emptyList();
    }
    // find structure models, their modules is what we truly need to extend
    // except for lang.core, which is extended by default.
    ConceptDeclarationScanner cds = new ConceptDeclarationScanner().omitLangCore();
    return cds.scan(structureModel).getDependencyModules().stream().map(SModule::getModuleReference).collect(Collectors.toList());
  }
}
