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

import com.intellij.ProjectTopics;
import com.intellij.facet.Facet;
import com.intellij.facet.FacetManager;
import com.intellij.facet.FacetManagerAdapter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.CompilerModuleExtension;
import com.intellij.openapi.roots.JdkOrderEntry;
import com.intellij.openapi.roots.LibraryOrderEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleRootListener;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.RootProvider;
import com.intellij.openapi.roots.RootProvider.RootSetChangedListener;
import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable.Listener;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.CommonProcessors.FindProcessor;
import com.intellij.util.Processor;
import com.intellij.util.messages.MessageBusConnection;
import jetbrains.mps.extapi.module.ModuleFacetBase;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.idea.core.facet.MPSFacet;
import jetbrains.mps.idea.core.facet.MPSFacetType;
import jetbrains.mps.idea.core.library.ModuleLibrariesUtil;
import jetbrains.mps.idea.core.library.ModuleLibraryType;
import jetbrains.mps.idea.core.project.module.ModuleMPSSupport;
import jetbrains.mps.idea.core.project.stubs.DifferentSdkException;
import jetbrains.mps.idea.core.project.stubs.JdkStubSolutionManager;
import jetbrains.mps.idea.core.project.stubs.MultipleSdkProblemNotifier;
import jetbrains.mps.idea.core.psi.impl.PsiModelReloadListener;
import jetbrains.mps.module.SDependencyImpl;
import jetbrains.mps.project.ModuleId;
import jetbrains.mps.project.Solution;
import jetbrains.mps.project.facets.JavaModuleFacet;
import jetbrains.mps.project.facets.JavaModuleFacetImpl;
import jetbrains.mps.project.structure.modules.Dependency;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.project.structure.modules.SolutionDescriptor;
import jetbrains.mps.smodel.SModelAdapter;
import jetbrains.mps.smodel.SModelInternal;
import jetbrains.mps.smodel.event.SModelLanguageEvent;
import jetbrains.mps.smodel.event.SModelListener;
import jetbrains.mps.vfs.FileSystem;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.ModelAccess;
import org.jetbrains.mps.openapi.module.SDependency;
import org.jetbrains.mps.openapi.module.SDependencyScope;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleListener;
import org.jetbrains.mps.openapi.module.SModuleListenerBase;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.persistence.Memento;
import org.jetbrains.mps.openapi.persistence.ModelRoot;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class SolutionIdea extends Solution {
  @NotNull
  private final Module myModule;
  private ModelAccess myModelAccess;
  private List<SDependency> myDependencies;
  private Set<ModelRoot> myContributedModelRoots;
  private MessageBusConnection myConnection;
  private final SolutionIdea.MyRootSetChangedListener myRootSetListener = new MyRootSetChangedListener();
  private final SolutionIdea.MyLibrariesListener myLibrariesListener = new MyLibrariesListener();

  public SolutionIdea(@NotNull Module module, SolutionDescriptor descriptor) {
    super(descriptor, null);
    myModule = module;
    myModelAccess = ProjectHelper.getModelAccess(myModule.getProject());
    assert myModelAccess != null;
    // TODO: simply set solution descriptor local variable?
    setModuleDescriptor(descriptor);
    myConnection = myModule.getProject().getMessageBus().connect();
    myConnection.subscribe(ProjectTopics.PROJECT_ROOTS, new MyModuleRootListener());
    myConnection.subscribe(FacetManager.FACETS_TOPIC, new MyFacetManagerAdapter());
    final ProjectLibraryTable projectLibraryTable = (ProjectLibraryTable) ProjectLibraryTable.getInstance(myModule.getProject());
    myModelAccess.runReadAction(new Runnable() {
      @Override
      public void run() {
        for (final Library library : projectLibraryTable.getLibraries()) {
          if (ModuleLibraryType.isModuleLibrary(library)) {
            library.getRootProvider().addRootSetChangedListener(myRootSetListener);
          }
        }
        for (SModel model : getModels()) {
          ((SModelInternal) model).addModelListener(MODEL_RUNTIME_IMPORTER);
        }
      }
    });
    projectLibraryTable.addListener(myLibrariesListener);
    addModuleListener(myModule.getProject().getComponent(PsiModelReloadListener.class));
    addModuleListener(MODULE_RUNTIME_IMPORTER);
  }

  @Override
  protected void doSetModuleDescriptor(ModuleDescriptor moduleDescriptor) {
    assert moduleDescriptor instanceof SolutionDescriptor;
    SolutionDescriptor newDescriptor = (SolutionDescriptor) moduleDescriptor;
    myDependencies = null;
    myContributedModelRoots = null;
    newDescriptor.setNamespace(myModule.getName());
//    addLibs(newDescriptor);
    super.doSetModuleDescriptor(newDescriptor);

    try {
      ApplicationManager.getApplication().getComponent(JdkStubSolutionManager.class).claimSdk(myModule);
    } catch (final DifferentSdkException e) {
      myModule.getProject().getComponent(MultipleSdkProblemNotifier.class).reportSdkProblem(myModule, e);
    }
  }

  @Override
  public void dispose() {
    final ProjectLibraryTable projectLibraryTable = (ProjectLibraryTable) ProjectLibraryTable.getInstance(myModule.getProject());
    myModelAccess.runReadAction(new Runnable() {
      @Override
      public void run() {
        for (final Library library : projectLibraryTable.getLibraries()) {
          if (ModuleLibraryType.isModuleLibrary(library)) {
            library.getRootProvider().removeRootSetChangedListener(myRootSetListener);
          }
        }
      }
    });
    projectLibraryTable.removeListener(myLibrariesListener);
    ApplicationManager.getApplication().getComponent(JdkStubSolutionManager.class).releaseSdk(myModule);
    super.dispose();
    myConnection.disconnect();
  }

  @Override
  protected SolutionDescriptor loadDescriptor() {
    return getModuleDescriptor();
  }

  @Override
  protected Iterable<ModelRoot> loadRoots() {
    if (myContributedModelRoots == null) {
      myContributedModelRoots = new HashSet<ModelRoot>();
      for (ModelRootContributorEP e : ModelRootContributorEP.EP_NAME.getExtensions()) {
        for (ModelRoot root : e.getModelRootContribitor().getModelRoots(myModule)) {
          myContributedModelRoots.add(root);
        }
      }
    }

    List<ModelRoot> sum = new ArrayList<ModelRoot>();
    for (ModelRoot mr : super.loadRoots()) {
      sum.add(mr);
    }

    sum.addAll(myContributedModelRoots);

    return sum;
  }

  @Override
  public Iterable<SDependency> getDeclaredDependencies() {
    if (myDependencies == null) {
      myDependencies = new ArrayList<SDependency>();

      ArrayList<Module> usedModules = new ArrayList<Module>(Arrays.asList(ModuleRootManager.getInstance(myModule).getDependencies()));
      for (Module usedModule : usedModules) {
        MPSFacet usedModuleMPSFacet = FacetManager.getInstance(usedModule).getFacetByType(MPSFacetType.ID);
        if (usedModuleMPSFacet != null && usedModuleMPSFacet.wasInitialized()) {
          myDependencies.add(new SDependencyImpl(usedModuleMPSFacet.getSolution(), SDependencyScope.DEFAULT, false));
        }
      }

      addUsedSdk(myDependencies);
      addUsedLibraries(myDependencies);

      // adding JDK module to a set of dependencies
      // why, oh, why are we doing it?
      // FIXME, PLEASE!
//      Solution jdkSolution = StubSolutionIdea.getJdkSolution();
//      if (jdkSolution != null) {
//        myDependencies.add(new Dependency(jdkSolution.getModuleReference(), false));
//      }
    }
    return myDependencies;
  }

  private void addUsedSdk(final List<SDependency> dependencies) {
    Solution sdkSolution = ApplicationManager.getApplication().getComponent(JdkStubSolutionManager.class).getModuleSdkSolution(myModule);
    if (sdkSolution != null) {
      dependencies.add(new SDependencyImpl(sdkSolution, SDependencyScope.DEFAULT, false));
    }
  }

  private void addUsedLibraries(final List<SDependency> dependencies) {
    dependencies.addAll(calculateLibraryDependencies(ModuleRootManager.getInstance(myModule).orderEntries(), true));
  }

  public List<SDependency> calculateLibraryDependencies(OrderEnumerator orderEnumerator, final boolean includeStubs) {
    final Map<SModule, Boolean> modules = new HashMap<SModule, Boolean>();
    orderEnumerator.forEach(new Processor<OrderEntry>() {
      public boolean process(OrderEntry oe) {
        if (!(oe instanceof LibraryOrderEntry)) {
          return true;
        }
        LibraryOrderEntry loe = (LibraryOrderEntry) oe;
        Library library = loe.getLibrary();
        if (loe.isModuleLevel() || library == null) {
          return true;
        }

        if (ModuleLibraryType.isModuleLibrary(library)) {
          Set<SModuleReference> moduleReferences = ModuleLibrariesUtil.getModules(ProjectHelper.getProjectRepository(myModule.getProject()), library);
          for (SModuleReference moduleReference : moduleReferences) {
            SModule m = moduleReference.resolve(getRepository());
            if (m == null) {
              continue;
            }
            if (modules.containsKey(moduleReference)) {
              if (loe.isExported()) {
                modules.put(m, true);
              }
            } else {
              modules.put(m, loe.isExported());
            }
          }
        } else if (includeStubs) {
          // try to find stub solution
          SModule s = getRepository().getModule(ModuleId.foreign(library.getName()));
          if (s != null) {
            modules.put(s, loe.isExported());
          }
        }
        return true;
      }
    });
    List<SDependency> result = new ArrayList<SDependency>();
    for (Entry<SModule, Boolean> entry : modules.entrySet()) {
      result.add(new SDependencyImpl(entry.getKey(), SDependencyScope.DEFAULT, entry.getValue()));
    }
    return result;
  }

  @Override
  public Dependency addDependency(@NotNull SModuleReference moduleRef, boolean reexport) {
    // we do not add a dependency into solution, we add dependency to idea module instead
    ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(myModule).getModifiableModel();

    SModule sModule = moduleRef.resolve(getRepository());

    if (sModule instanceof SolutionIdea) {
      // we add dependency between idea modules
      Module otherIdeaModule = ((SolutionIdea) sModule).getIdeaModule();
      modifiableModel.addModuleOrderEntry(otherIdeaModule);
    } else {
      ModuleRuntimeLibrariesImporter.importForUsedModules(myModule, Collections.singleton(moduleRef), modifiableModel);
    }
    modifiableModel.commit();
    return null;
  }

  @Override
  protected void dependenciesChanged() {
    super.dependenciesChanged();
    myDependencies = null;
  }

  @Override
  public boolean isPackaged() {
    return false;
  }

  @Override
  public void save() {
    // TODO: implement saving functionality here.
    // should this methods really do something?
//        super.save();    //To change body of overridden methods use File | Settings | File Templates.
  }

  @Override
  public IFile getDescriptorFile() {
    return getFileSystem().getFile(myModule.getModuleFilePath());
  }

  private void handleFacetChanged(Facet facet) {
    if (skipFacetNotification(facet)) {
      return;
    }
    myModelAccess.runWriteInEDT(new Runnable() {
      @Override
      public void run() {
        setModuleDescriptor(getModuleDescriptor());
      }
    });
  }

  private boolean skipFacetNotification(Facet facet) {
    if (!myModule.getProject().equals(facet.getModule().getProject())) {
      return true;
    }
    Module[] dependencies = ModuleRootManager.getInstance(myModule).getDependencies();
    Module facetModule = facet.getModule();
    for (Module dependency : dependencies) {
      if (dependency.equals(facetModule)) {
        return false;
      }
    }
    return true;
  }

  @Override
  protected ModuleFacetBase setupFacet(ModuleFacetBase facet, Memento memento) {
    if (facet instanceof JavaModuleFacet) {
      facet = new JavaModuleFacetImpl() {
        @Override
        public IFile getClassesGen() {
          IFile descriptorFile = getDescriptorFile();
          if (descriptorFile != null && descriptorFile.isReadOnly()) {
            return super.getClassesGen();
          }

          CompilerModuleExtension compilerModuleExtension = ModuleRootManager.getInstance(myModule).getModuleExtension(CompilerModuleExtension.class);
          VirtualFile compilerOutputPath = compilerModuleExtension.getCompilerOutputPath();
          if (compilerOutputPath == null) {
            return null;
          }
          return FileSystem.getInstance().getFileByPath(compilerOutputPath.getPath());
        }
      };
    }
    return super.setupFacet(facet, memento);
  }

  private void addLibs(SolutionDescriptor solutionDescriptor) {
    // adding libraries
    for (OrderEntry e : ModuleRootManager.getInstance(myModule).getOrderEntries()) {
      if (!(e instanceof LibraryOrderEntry)) continue;

      LibraryOrderEntry loe = (LibraryOrderEntry) e;
      if (!loe.isModuleLevel()) continue;

      Library library = loe.getLibrary();
      if (library == null) continue;

      StubSolutionIdea.addModelRoots(solutionDescriptor, library.getFiles(OrderRootType.CLASSES));
    }
  }

  public Module getIdeaModule() {
    return myModule;
  }

  private class MyModuleRootListener implements ModuleRootListener {
    @Override
    public void beforeRootsChange(ModuleRootEvent event) {
    }

    @Override
    public void rootsChanged(ModuleRootEvent event) {
      if (myModule.getProject().equals(event.getSource())) {
        reset();
      }
    }
  }

  private void reset() {
    myModelAccess.runWriteAction(new Runnable() {
      public void run() {
        ApplicationManager.getApplication().getComponent(JdkStubSolutionManager.class).releaseSdk(myModule);
        // this is to prevent a delayed write to be executed after the module has already been disposed
        // TODO: find a better solution
        if (myModule.isDisposed()) {
          return;
        }
        setModuleDescriptor(getModuleDescriptor());
      }
    });
  }

  private class MyFacetManagerAdapter extends FacetManagerAdapter {
    @Override
    public void facetAdded(@NotNull Facet facet) {
      handleFacetChanged(facet);
    }

    @Override
    public void facetRemoved(@NotNull Facet facet) {
      handleFacetChanged(facet);
    }
  }

  private class MyRootSetChangedListener implements RootSetChangedListener {
    @Override
    public void rootSetChanged(final RootProvider wrapper) {
      FindProcessor<OrderEntry> processor = new FindProcessor<OrderEntry>() {
        @Override
        protected boolean accept(OrderEntry orderEntry) {
          return orderEntry instanceof LibraryOrderEntry && ((LibraryOrderEntry) orderEntry).getLibrary().getRootProvider() == wrapper
            || orderEntry instanceof JdkOrderEntry;
        }
      };
      ModuleRootManager.getInstance(myModule).orderEntries().forEach(processor);
      if (processor.isFound()) {
        reset();
      }
    }
  }

  private class MyLibrariesListener implements Listener {

    @Override
    public void afterLibraryAdded(final Library newLibrary) {
      if (ModuleLibraryType.isModuleLibrary(newLibrary)) {
        newLibrary.getRootProvider().addRootSetChangedListener(myRootSetListener);
      }
    }

    @Override
    public void afterLibraryRenamed(Library library) {
    }

    @Override
    public void beforeLibraryRemoved(Library library) {
      library.getRootProvider().addRootSetChangedListener(myRootSetListener);
    }

    @Override
    public void afterLibraryRemoved(Library library) {
    }
  }

  @Override
  public String toString() {
    return getModuleName() + " [idea module derived solution]";
  }

  private final SModuleListener MODULE_RUNTIME_IMPORTER = new SModuleListenerBase() {
    @Override
    public void modelAdded(SModule module, SModel model) {
      if (!(model instanceof SModelInternal)) return;
      ((SModelInternal) model).addModelListener(MODEL_RUNTIME_IMPORTER);
    }
  };

  private final SModelListener MODEL_RUNTIME_IMPORTER = new SModelAdapter() {
    @Override
    public void languageAdded(SModelLanguageEvent event) {
      SModuleReference langRef = event.getLanguageNamespace();
      ModuleMPSSupport.getInstance().fixImports(myModule, Collections.singleton(langRef));
    }
  };
}
