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

import jetbrains.mps.progress.EmptyProgressMonitor;
import jetbrains.mps.project.dependency.GeneratorModuleScanner;
import jetbrains.mps.project.structure.modules.Dependency;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.ModelDependencyScanner;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.util.annotation.ToRemove;
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.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.ProgressMonitor;

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

// FIXME (1) OrganizeImports (there's no optimization)
// FIXME (2) anything but empty dialog when OI for a generator module
// FIXME (3) sort dependencies by name, group by kind(SDependencyScope) to make dependencies look uniform and easy to grasp.
// FIXME (4) Refactor the class, reporting and breathe some OOP in here
public class OptimizeImportsHelper {
  private static final Logger LOG = LogManager.getLogger(OptimizeImportsHelper.class);

  private final SRepository myRepository;

  /**
   * @param repository -- is a context repository which contains the modules/models the client want to resolve
   */
  public OptimizeImportsHelper(@NotNull SRepository repository) {
    myRepository = repository;
  }

  //----public optimizeX methods--------

  @NotNull
  public String optimizeProjectImports(Project p, ProgressMonitor monitor) {
    return optimizeProjectImports_internal(p, monitor).myReport;
  }

  /**
   * Optimizes project imports. Might take some time.
   * @deprecated use {@link #optimizeProjectImports(Project, ProgressMonitor)} instead
   */
  @Deprecated
  @ToRemove(version = 3.3)
  public String optimizeProjectImports(Project p) {
    return optimizeProjectImports(p, new EmptyProgressMonitor());
  }

  @NotNull
  public String optimizeSolutionImports(Solution solution) {
    return optimizeSolutionImports_internal(solution).myReport;
  }

  @NotNull
  public String optimizeLanguageImports(Language language) {
    return optimizeLanguageImports_internal(language).myReport;
  }

  @NotNull
  public String optimizeModelsImports(List<SModel> modelsToOptimize, ProgressMonitor monitor) {
    return optimizeModelsImports_internal(modelsToOptimize, monitor).myReport;
  }

  /**
   * Optimizes imports for a list of models. Might take some time, so please pass the monitor parameter
   * @deprecated use {@link #optimizeModelsImports(List, ProgressMonitor)}
   */
  @Deprecated
  @ToRemove(version = 3.3)
  @NotNull
  public String optimizeModelsImports(List<SModel> modelsToOptimize) {
    return optimizeModelsImports(modelsToOptimize, new EmptyProgressMonitor());
  }

  @NotNull
  public String optimizeModelImports(SModel modelDescriptor) {
    return optimizeModelImports_internal(modelDescriptor).myReport;
  }

  //----internal optimizeX methods--------

  private Result optimizeProjectImports_internal(Project p, ProgressMonitor monitor) {
    Result result = new Result();

    List<Language> projectLangs = p.getProjectModules(Language.class);
    List<Solution> projectSolutions = p.getProjectModules(Solution.class);
    monitor.start("Optimizing project imports", projectLangs.size() + projectSolutions.size());

    try {
      for (Language l : projectLangs) {
        monitor.step(l.toString());
        result.add(optimizeLanguageImports_internal(l));
        monitor.advance(1);
        if (monitor.isCanceled()) {
          return result;
        }
      }
      for (Solution s : p.getProjectModules(Solution.class)) {
        monitor.step(s.toString());
        result.add(optimizeSolutionImports_internal(s));
        monitor.advance(1);
        if (monitor.isCanceled()) {
          return result;
        }
      }
    } finally {
      monitor.done();
    }

    return result;
  }

  private Result optimizeSolutionImports_internal(Solution solution) {
    List<SModel> modelsToOptimize = solution.getModels();
    Result result = optimizeModelsImports_internal(modelsToOptimize, new EmptyProgressMonitor());
    result.myReport = optimizeModuleImports(solution, result, Collections.<SModuleReference>emptySet()) + "\n\n" + result.myReport;
    return result;
  }

  private Result optimizeLanguageImports_internal(Language language) {
    List<SModel> modelsToOptimize = new ArrayList<SModel>();
    for (SModel model : language.getModels()) {
      if (SModelStereotype.isDescriptorModelStereotype(SModelStereotype.getStereotype(model)) || SModelStereotype.isStubModel(model)) {
        // with @desciptor model hosting imports to activate generation of custom aspects, it's not wise to remove these.
        // stub models aren't best candidates for organize imports, too.
        continue;
      }
      modelsToOptimize.add(model);
    }
    for (Generator g : language.getGenerators()) {
      modelsToOptimize.addAll(g.getModels());
    }
    Result result = optimizeModelsImports_internal(modelsToOptimize, new EmptyProgressMonitor());
    myRepository.saveAll();
    for (Generator g : language.getGenerators()) {
      GeneratorModuleScanner gms = new GeneratorModuleScanner();
      gms.walkPriorityRules(g);
      result.myReport = optimizeModuleImports(g, result, gms.getReferencedGenerators()) + "\n\n" + result.myReport;
    }
    result.myReport = optimizeModuleImports(language, result, Collections.<SModuleReference>emptySet()) + "\n\n" + result.myReport;

    return result;
  }

  private Result optimizeModelsImports_internal(List<SModel> modelsToOptimize, ProgressMonitor monitor) {
    Result result = new Result();
    monitor.start("working", modelsToOptimize.size());
    try {
      for (SModel model : modelsToOptimize) {
        monitor.step(model.toString());
        if (SModelStereotype.isStubModel(model)) {
          // todo: looks like WTF
          result.add(collectModelDependencies(model));
        } else {
          result.add(optimizeModelImports_internal(model));
        }
        monitor.advance(1);
        if (monitor.isCanceled()) {
          return result;
        }
      }
    } finally {
      monitor.done();
    }
    return result;
  }

  private Result optimizeModelImports_internal(SModel modelDescriptor) {
    Result result = collectModelDependencies(modelDescriptor);

    Set<SModelReference> unusedModels = new HashSet<SModelReference>();
    for (SModelReference model : SModelOperations.getImportedModelUIDs(modelDescriptor)) {
      if (result.myUsedModels.contains(model)) continue;

      // FIXME
      //this is a temp code to fix http://youtrack.jetbrains.com/issue/MPS-19621
      //we should re-save models and make them findModules through modules, not just by ID
      //this code is supposed to be deleted after 3.1 release
      SModel md = model.resolve(myRepository);
      if (md == null) continue;

      if (result.myUsedModels.contains(md.getReference())) continue;
      //end of tmp code

      unusedModels.add(model);
    }

    Set<SLanguage> unusedLanguages = new HashSet<SLanguage>();
    for (SLanguage languageRef : ((jetbrains.mps.smodel.SModelInternal) modelDescriptor).importedLanguageIds()) {
      if (isUnusedLanguageRef(result, languageRef)) {
        unusedLanguages.add(languageRef);
      }
    }

    Set<SModuleReference> unusedDevkits = new HashSet<SModuleReference>();
    for (SModuleReference devkitRef : ((jetbrains.mps.smodel.SModelInternal) modelDescriptor).importedDevkits()) {
      if (ModelsAutoImportsManager.getDevKits(modelDescriptor.getModule(), modelDescriptor).contains(devkitRef)) {
        continue;
      }
      if (isUnusedDevkitRef(result, devkitRef)) {
        unusedDevkits.add(devkitRef);
      }
    }

    result.myReport = removeFromImports(modelDescriptor, unusedModels, unusedLanguages, unusedDevkits);
    return result;
  }

  private Result collectModelDependencies(SModel model) {
    Result result = new Result();

    /*
    FIXME how come we take engaged languages into account as 'used'. I'd rather demand them explicitly added as 'used', rather
    than implicitly taken from 'engaged'
    result.myUsedLanguages.addAll(((jetbrains.mps.smodel.SModelInternal) model).engagedOnGenerationLanguages());
    */
    ModelDependencyScanner modelScanner = new ModelDependencyScanner().crossModelReferences(true).usedLanguages(true).walk(model);
    result.myUsedLanguages.addAll(modelScanner.getUsedLanguages());
    result.myUsedModels.addAll(modelScanner.getCrossModelReferences());

    // add auto imports as dependencies
    for (SLanguage l : ModelsAutoImportsManager.getLanguages(model.getModule(), model)) {
      result.myUsedLanguages.add(l);
    }
    for (SModel m : ModelsAutoImportsManager.getAutoImportedModels(model.getModule(), model)) {
      result.addUsedModel(m.getReference());
    }

    return result;
  }

  //----additional methods--------

  private String optimizeModuleImports(AbstractModule module, Result result, Collection<SModuleReference> toKeep) {
    List<Dependency> unusedDeps = new ArrayList<Dependency>();
    final SModuleReference optimizedModuleReference = module.getModuleReference();
    HashSet<SModuleReference> inUse = new HashSet<SModuleReference>(toKeep);
    SRepository repository = module.getRepository();
    // from used models, find out modules we need
    for (SModelReference mr : result.myUsedModels) {
      SModuleReference moduleInUse = mr.getModuleReference();
      if (moduleInUse == null) {
        if (repository != null) {
          SModel model = mr.resolve(repository);
          if (model != null && model.getModule() != null) {
            inUse.add(model.getModule().getModuleReference());
          }
        }
      } else {
        inUse.add(moduleInUse);
      }
    }
    for (Dependency d : module.getModuleDescriptor().getDependencies()) {
      if (d.getModuleRef().equals(optimizedModuleReference)) {
        unusedDeps.add(d);
        continue;
      }
      if (inUse.contains(d.getModuleRef())) {
        continue;
      }
      unusedDeps.add(d);
    }

    return removeFromImports(module, unusedDeps);
  }

  private boolean isUnusedDevkitRef(Result result, SModuleReference devkitRef) {
    DevKit dk = ((DevKit) devkitRef.resolve(myRepository));
    if (dk == null) {
      return false;
    }

    for (SLanguage lang : dk.getAllExportedLanguageIds()) {
      if (!isUnusedLanguageRef(result, lang)) {
        return false;
      }
    }

    for (Solution solution : dk.getAllExportedSolutions()) {
      for (SModel model : solution.getModels()) {
        if (result.myUsedModels.contains(model.getReference())) {
          return false;
        }
      }
    }
    return true;
  }

  private boolean isUnusedLanguageRef(Result result, SLanguage languageRef) {
    if (result.myUsedLanguages.contains(languageRef)) {
      return false;
    }

    final SModule sourceModule = languageRef.getSourceModule();
    if (sourceModule instanceof Language) {
      for (SModel md : ((Language) sourceModule).getAccessoryModels()) {
        if (result.myUsedModels.contains(md.getReference())) return false;
      }
    }

    return true;
  }

  private String removeFromImports(SModel modelDescriptor, Set<SModelReference> unusedModels, Set<SLanguage> unusedLanguages,
      Set<SModuleReference> unusedDevkits) {
    StringBuilder report = new StringBuilder("Import for model " + modelDescriptor.getReference() + " were optimized \n");

    for (SLanguage langRef : unusedLanguages) {
      ((jetbrains.mps.smodel.SModelInternal) modelDescriptor).deleteLanguageId(langRef);
      report.append("Language ").append(langRef.getQualifiedName()).append(" was removed from imports\n");
    }

    for (SModuleReference dkRef : unusedDevkits) {
      ((jetbrains.mps.smodel.SModelInternal) modelDescriptor).deleteDevKit(dkRef);
      report.append("Devkit ").append(dkRef.getModuleName()).append(" was removed from imports\n");
    }

    for (SModelReference model : unusedModels) {
      ((jetbrains.mps.smodel.SModelInternal) modelDescriptor).deleteModelImport(model);
      report.append("Model ").append(model.getModelName()).append(" was removed from imports\n");
    }

    return report.toString();
  }

  private String removeFromImports(AbstractModule module, List<Dependency> unusedDeps) {
    StringBuilder report = new StringBuilder("Import for module " + module.getModuleName() + " were optimized \n");

    for (Dependency dep : unusedDeps) {
      module.removeDependency(dep);
      report.append("Dependency on ").append(dep.getModuleRef().getModuleName()).append(" was removed\n");
    }

    return report.toString();
  }


  private static class Result {
    public String myReport = "";
    public final Set<SLanguage> myUsedLanguages = new HashSet<SLanguage>();
    public final Set<SModelReference> myUsedModels = new HashSet<SModelReference>();

    public void add(Result addition) {
      myReport = myReport + addition.myReport + "\n";
      myUsedLanguages.addAll(addition.myUsedLanguages);
      myUsedModels.addAll(addition.myUsedModels);
    }

    public void addUsedModel(SModelReference ref) {
      if (ref == null) {
        // todo: ? can be in case of DynamicReference in stubs
        return;
      }
      myUsedModels.add(ref);
    }
  }
}
