/*
 * Copyright 2003-2012 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.idea.core.project;

import com.intellij.facet.impl.ui.FacetEditorContextBase;
import com.intellij.facet.ui.FacetEditorContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.ui.configuration.projectRoot.LibrariesContainer;
import com.intellij.openapi.roots.ui.configuration.projectRoot.LibrariesContainerFactory;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.idea.core.library.ModuleLibrariesUtil;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager.Deptype;
import jetbrains.mps.project.dependency.UsedModulesCollector;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import jetbrains.mps.smodel.BootstrapLanguages;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.ModuleRepositoryFacade;

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

/**
 * User: shatalin
 * Date: 6/8/12
 */
public abstract class ModuleRuntimeLibrariesImporter {
  private static final Logger LOG = Logger.getInstance(ModuleRuntimeLibrariesImporter.class);

  private Project myProject;
  private final Collection<? extends SModuleReference> myAddedModules;
  private final ModifiableRootModel myModifiableRootModel;
  private final LibrariesContainer myLibrariesContainer;

  public ModuleRuntimeLibrariesImporter(FacetEditorContext context, Collection<? extends SModuleReference> addedModules) {
    this(addedModules, context.getModifiableRootModel(), ((FacetEditorContextBase) context).getContainer());
    myProject = context.getProject();
  }

  public ModuleRuntimeLibrariesImporter(Module ideaModule, Collection<? extends SModuleReference> addedModules, ModifiableRootModel modifiableModel) {
    this(addedModules, modifiableModel, LibrariesContainerFactory.createContainer(ideaModule));
    myProject = ideaModule.getProject();
  }

  private ModuleRuntimeLibrariesImporter(Collection<? extends SModuleReference> addedModules, ModifiableRootModel modifiableModel, LibrariesContainer container) {
    myAddedModules = addedModules;
    myModifiableRootModel = modifiableModel;
    myLibrariesContainer = container;
  }

  public static void importForUsedLanguages(FacetEditorContext context, Collection<? extends SModuleReference> addedModules) {
    new UsedLanguagesImporter(context, addedModules).addMissingLibraries();
  }

  public static void importForUsedLanguages(Module ideaModule, Collection<SModuleReference> addedModules, ModifiableRootModel modifiableModel) {
    new UsedLanguagesImporter(ideaModule, addedModules, modifiableModel).addMissingLibraries();
  }

  public static void importForUsedModules(Module ideaModule, Collection<SModuleReference> addedModules, ModifiableRootModel modifiableModel) {
    new UsedModulesImporter(ideaModule, addedModules, modifiableModel).addMissingLibraries();
  }

  public void addMissingLibraries() {
    Set<SModuleReference> alreadyImported = ModuleLibrariesUtil.getModules(ProjectHelper.getProjectRepository(myProject), myModifiableRootModel.getOrderEntries());

    Collection<Library> projectLibs2Add = new HashSet<Library>();
    for (SModule usedModule : collectRuntimeModules(myAddedModules)) {
      if (BootstrapLanguages.jdkRef().equals(usedModule.getModuleReference())) {
        continue;
      }

      if (alreadyImported.contains(usedModule.getModuleReference())) {
        continue;
      }

      // todo how to deal with sdk stubs?

      Library library = null;
      if (usedModule instanceof StubSolutionIdea) {
        library = StubSolutionIdea.findLibrary((StubSolutionIdea) usedModule);
      } else {
        library = ModuleLibrariesUtil.getOrCreateAutoLibrary((AbstractModule) usedModule, getProject(), myLibrariesContainer);
      }

      if (library != null) {
        projectLibs2Add.add(library);
      }
    }

    for (Library projectLibrary : projectLibs2Add) {
      myModifiableRootModel.addLibraryEntry(projectLibrary);
    }
  }

  private Project getProject() {
    return myLibrariesContainer.getProject();
  }

  protected abstract Set<SModule> collectRuntimeModules(Collection<? extends SModuleReference> moduleReferences);

  private static class UsedLanguagesImporter extends ModuleRuntimeLibrariesImporter {

    public UsedLanguagesImporter(FacetEditorContext context, Collection<? extends SModuleReference> addedModules) {
      super(context, addedModules);
    }

    public UsedLanguagesImporter(Module ideaModule, Collection<SModuleReference> addedModules, ModifiableRootModel modifiableModel) {
      super(ideaModule, addedModules, modifiableModel);
    }

    @Override
    protected Set<SModule> collectRuntimeModules(Collection<? extends SModuleReference> moduleReferences) {
      Set<SModule> runtimeDependencies = new HashSet<SModule>();
      UsedModulesCollector usedModulesCollector = new UsedModulesCollector();
      for (SModuleReference moduleReference : moduleReferences) {
        Language language = ModuleRepositoryFacade.getInstance().getModule(moduleReference, Language.class);
        LOG.assertTrue(language != null, "Can not find language by reference " + moduleReference);
        collectRuntimeModules(runtimeDependencies, language, usedModulesCollector);
      }
      return runtimeDependencies;
    }

    private void collectRuntimeModules(Set<SModule> runtimeDependencies, Language language, UsedModulesCollector usedModulesCollector) {
      for (SModuleReference runtimeModuleReference : language.getRuntimeModulesReferences()) {
        SModule runtimeModule = ModuleRepositoryFacade.getInstance().getModule(runtimeModuleReference);
        if (runtimeModule != null) {
          collectRuntimeDependencies(runtimeModule, runtimeDependencies, usedModulesCollector);
        }
      }
    }

    private void collectRuntimeDependencies(SModule module, Set<SModule> result, UsedModulesCollector usedModulesCollector) {
      // todo: extract some other methods in GlobalModuleDependenciesManager. Like getDependencies(Iterable<> addedModules, Iterable<> addedUsedModules, Deptype)
      if (result.contains(module)) {
        return;
      }
      result.add(module);
      for (SModule usedModule : usedModulesCollector.directlyUsedModules(module, Deptype.EXECUTE.reexportAll, Deptype.EXECUTE.runtimes)) {
        collectRuntimeDependencies(usedModule, result, usedModulesCollector);
      }
    }
  }

  private static class UsedModulesImporter extends ModuleRuntimeLibrariesImporter {
    public UsedModulesImporter(Module ideaModule, Collection<? extends SModuleReference> addedModules, ModifiableRootModel modifiableModel) {
      super(ideaModule, addedModules, modifiableModel);
    }

    @Override
    protected Set<SModule> collectRuntimeModules(Collection<? extends SModuleReference> moduleReferences) {
      Set<SModule> runtimeDependencies = new HashSet<SModule>();
      for (SModuleReference moduleReference : moduleReferences) {
        SModule module = ModuleRepositoryFacade.getInstance().getModule(moduleReference);
        collectRuntimeModules(runtimeDependencies, module);
      }
      return runtimeDependencies;
    }

    private void collectRuntimeModules(Set<SModule> runtimeDependencies, SModule module) {
      runtimeDependencies.add(module);
      runtimeDependencies.addAll(new GlobalModuleDependenciesManager(Collections.singleton(module)).getModules(Deptype.COMPILE));
    }
  }
}
