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

import jetbrains.mps.baseLanguage.search.MPSBaseLanguage;
import jetbrains.mps.classloading.CustomClassLoadingFacet;
import jetbrains.mps.core.platform.Platform;
import jetbrains.mps.core.platform.PlatformFactory;
import jetbrains.mps.core.platform.PlatformOptionsBuilder;
import jetbrains.mps.extapi.module.ModuleFacetBase;
import jetbrains.mps.extapi.module.SRepositoryExt;
import jetbrains.mps.idea.core.make.MPSMakeConstants;
import jetbrains.mps.idea.core.module.CachedModuleData;
import jetbrains.mps.idea.core.module.CachedRepositoryData;
import jetbrains.mps.jps.build.MPSCompilerUtil;
import jetbrains.mps.jps.persistence.CachedDefaultModelRoot;
import jetbrains.mps.jps.persistence.CachedJavaClassStubsModelRoot;
import jetbrains.mps.jps.project.JpsLibSolution;
import jetbrains.mps.jps.project.JpsMPSProject;
import jetbrains.mps.jps.project.JpsSolutionIdea;
import jetbrains.mps.library.ModulesMiner;
import jetbrains.mps.library.ModulesMiner.ModuleHandle;
import jetbrains.mps.persistence.MementoImpl;
import jetbrains.mps.persistence.PersistenceRegistry;
import jetbrains.mps.project.ModuleId;
import jetbrains.mps.project.structure.modules.ModuleFacetDescriptor;
import jetbrains.mps.project.structure.modules.SolutionDescriptor;
import jetbrains.mps.smodel.BaseMPSModuleOwner;
import jetbrains.mps.smodel.MPSModuleOwner;
import jetbrains.mps.smodel.ModuleRepositoryFacade;
import jetbrains.mps.util.SNodeOperations;
import jetbrains.mps.util.io.ModelInputStream;
import jetbrains.mps.vfs.FileRefresh;
import jetbrains.mps.vfs.FileSystem;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.messages.BuildMessage.Kind;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.model.JpsProject;
import org.jetbrains.jps.model.java.JpsJavaSdkType;
import org.jetbrains.jps.model.library.JpsLibrary;
import org.jetbrains.jps.model.module.JpsDependencyElement;
import org.jetbrains.jps.model.module.JpsModule;
import org.jetbrains.jps.model.module.JpsSdkDependency;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.Memento;
import org.jetbrains.mps.openapi.persistence.ModelRoot;
import org.jetbrains.mps.openapi.persistence.ModelRootFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * evgeny, 12/3/12
 */
public class JpsMPSRepositoryFacade implements MPSModuleOwner {

  private static final JpsMPSRepositoryFacade INSTANCE = new JpsMPSRepositoryFacade();
  public static final UUID JDK_UUID = UUID.fromString("6354ebe7-c22a-4a0f-ac54-50b52ab9b065");
  private static final BaseMPSModuleOwner OWNER = new BaseMPSModuleOwner() {};

  private Platform myPlatform;
  private MPSBaseLanguage myMPSBaseLanguage;
  private volatile boolean isInitialized = false;
  private CachedRepositoryData myRepo;
  private Map<JpsModule, JpsSolutionIdea> myJpsToMpsModules = new HashMap<JpsModule, JpsSolutionIdea>();
  private JpsMPSProject myProject;
  private SRepository myRepository;

  public JpsMPSRepositoryFacade() {
  }

  public static JpsMPSRepositoryFacade getInstance() {
    return INSTANCE;
  }

  public void init(final CompileContext context) {
    if (isInitialized) {
      return;
    }
    initMPS();

    myProject = new JpsMPSProject(context.getProjectDescriptor().getProject());
    myRepository = myProject.getRepository();

    myRepository.getModelAccess().runWriteAction(new Runnable() {
      @Override
      public void run() {
        long start = System.nanoTime();
        initRepository(context,
          context.getBuilderParameter(MPSMakeConstants.MPS_LANGUAGES.toString()),
          context.getBuilderParameter(MPSMakeConstants.MPS_REPOSITORY.toString()));

        initProject(context);

        if (MPSCompilerUtil.isTracingMode()) {
          context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "MPS loaded in " + (System.nanoTime() - start) / 1000000 + " ms"));
        }
        isInitialized = true;
      }
    });
  }

  public JpsMPSProject getProject() {
    return myProject;
  }

  public JpsSolutionIdea getSolution(JpsModule module) {
    if (!isInitialized) throw new IllegalStateException("Not initialized yet");
    return myJpsToMpsModules.get(module);
  }


  private void initRepository(CompileContext context, String languages, String repoFile) {
    final ModuleRepositoryFacade repoFacade = new ModuleRepositoryFacade(myRepository);
    if (repoFile != null) {
      File f = new File(repoFile);
      ModelInputStream mos = null;
      try {
        long start = System.nanoTime();
        mos = new ModelInputStream(new FileInputStream(f));
        myRepo = CachedRepositoryData.load(mos);
        if (MPSCompilerUtil.isTracingMode()) {
          context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "loaded " + myRepo.getModules().size() + " modules in " + (System.nanoTime() - start) / 1000000 + " ms"));
        }

        // use optimized implementation of default model root
        PersistenceRegistry.getInstance().setModelRootFactory(PersistenceRegistry.DEFAULT_MODEL_ROOT, new ModelRootFactory() {
          @NotNull
          @Override
          public ModelRoot create() {
            return new CachedDefaultModelRoot(myRepo);
          }
        });

        PersistenceRegistry.getInstance().setModelRootFactory(PersistenceRegistry.JAVA_CLASSES_ROOT, new ModelRootFactory() {
          @NotNull
          @Override
          public ModelRoot create() {
            return new CachedJavaClassStubsModelRoot(myRepo);
          }
        });

        start = System.nanoTime();
        for (CachedModuleData data : myRepo.getModules()) {
          repoFacade.instantiateModule(data.getHandle(), this);
        }
        if (MPSCompilerUtil.isTracingMode()) {
          context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "instantiated " + myRepo.getModules().size() + " modules in " + (System.nanoTime() - start) / 1000000 + " ms"));
        }
        return;
      } catch (IOException e) {
        context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, e));
        context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.WARNING, "cannot load cache, generation may be slow"));
      } finally {
        jetbrains.mps.util.FileUtil.closeFileSafe(mos);
      }
    } else if (languages != null) {
      // TODO split by semicolon, etc.

      long start = System.nanoTime();
      List<ModuleHandle> loadedModules = new ArrayList<ModuleHandle>();
      List<IFile> filesToLoad = new ArrayList<>();
      for (String path: languages.split(";")) {
        IFile ipath = FileSystem.getInstance().getFileByPath(path);
        filesToLoad.add(ipath);
      }
      new FileRefresh(filesToLoad).run();
      ModulesMiner modulesMiner = new ModulesMiner();
      for (IFile ipath : filesToLoad) {
        modulesMiner.collectModules(ipath);
      }

      if (MPSCompilerUtil.isTracingMode()) {
        context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "loaded " + modulesMiner.getCollectedModules().size() + " modules in " + (System.nanoTime() - start) / 1000000 + " ms"));
      }

      start = System.nanoTime();
      for (ModuleHandle moduleHandle : modulesMiner.getCollectedModules()) {
        repoFacade.instantiateModule(moduleHandle, OWNER);
      }

      if (MPSCompilerUtil.isTracingMode()) {
        context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "instantiated " + modulesMiner.getCollectedModules().size() + " modules in " + (System.nanoTime() - start) / 1000000 + " ms"));
      }

      return;
    }

    context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.WARNING, "cannot start MPS, no repository provided"));
  }

  private void initProject(CompileContext context) {
    long start = System.nanoTime();

    JpsProject jpsProject = context.getProjectDescriptor().getProject();

    Set<JpsLibrary> processedSdks = new HashSet<JpsLibrary>();

    JpsLibrary jdk = null;
    for (JpsModule mod : jpsProject.getModules()) {
      JpsMPSModuleExtension extension = JpsMPSExtensionService.getInstance().getExtension(mod);

      if (extension == null) {
        continue;
      }

      if (MPSCompilerUtil.isTracingMode()) {
        context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "Creating solution for " + mod.getName()));
      }

      SolutionDescriptor descriptor = extension.getConfiguration().getSolutionDescriptor();
      descriptor.setNamespace(mod.getName());
      MPSCompilerUtil.debug(context, "UUID " + descriptor.getId());
      // Commeted out. See SolutionIdea: solutions don't have foreign ids, rather regular
//      descriptor.setId(ModuleId.foreign(mod.getName()));

      JpsSolutionIdea module = new JpsSolutionIdea(mod, descriptor, context);
      JpsSolutionIdea solutionIdea = ((SRepositoryExt) myRepository).registerModule(module, myProject);
      if (module == solutionIdea) {
        solutionIdea.updateModelsSet();
      }
      myProject.addModule(solutionIdea);

      myJpsToMpsModules.put(mod, solutionIdea);

      // let's handle module sdkLib
      for (JpsLibrary sdk: getModuleSdks(mod, context)) {
        MPSCompilerUtil.debug(context, "SDK name" + sdk.getName() + " type: " + sdk.getType());

        JpsLibSolution sdkSolution = createLibSolution(sdk, jdk, context);
        JpsLibSolution regSolution = ((SRepositoryExt) myRepository).registerModule(sdkSolution, myProject);
        MPSCompilerUtil.debug(context, "SDK " + regSolution.getModuleReference().toString());
        if (sdkSolution == regSolution) {
          MPSCompilerUtil.debug(context, "SDK updating model set for " + sdk.getName());
          sdkSolution.updateModelsSet();
        }

        if (JpsJavaSdkType.INSTANCE.equals(sdk.getType()) && !processedSdks.contains(sdk)) {
          jdk = jdk != null ? jdk : sdk;
          processedSdks.add(sdk);
        }
      }
    }

    if (processedSdks.size() > 1) {
      context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.ERROR, "Different SDKs in modules with MPS facets are not supported"));
    }

    // maybe libraries should be put into repository before modules, so that SolutionIdea already has its dependencies at hand

    for (JpsLibrary jpsLib : jpsProject.getLibraryCollection().getLibraries()) {
      JpsLibSolution libSolution = createLibSolution(jpsLib, jdk, context);
      JpsLibSolution regSolution = ((SRepositoryExt) myRepository).registerModule(libSolution, myProject);
      MPSCompilerUtil.debug(context, "LIB " + regSolution.getModuleReference().toString());
      if (libSolution == regSolution) {
        MPSCompilerUtil.debug(context, "LIB updating model set for " + jpsLib.getName());
        libSolution.updateModelsSet();
      }
      if (MPSCompilerUtil.isExtraTracingMode()) {
        for (SModel desc : regSolution.getModels()) {
          MPSCompilerUtil.debug(context, "LIB model " + desc.getModelName());
        }
      }
    }

    if (MPSCompilerUtil.isTracingMode()) {
      context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "Project modules loaded in " + (System.nanoTime() - start) / 1000000 + " ms"));

      if (MPSCompilerUtil.isExtraTracingMode()) {
        for (SModule m : new ModuleRepositoryFacade(myRepository).getModules(myProject, null)) {
          context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "Debug output: module " + m.getModuleReference().toString()));

          for (SModel d : m.getModels()) {
            context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "Debug output: model " + SNodeOperations.getModelLongName(d) + " / " + d.getReference().toString()));
            // It makes model loading non-lazy and kills the whole thing if stubs are built for everything (like SDK, libs, etc)
  //          for (SNode n : d.getRootNodes()) {
  //            context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "node: " + n.getName() + " id: " + n.getSNodeId().toString()));
  //            if (n.getName().equals("PsiListener") || n.getName().equals("PsiChangesWatcher")) {
  //              for (SNode n2 : n.getChildren()) {
  //                context.processMessage(new CompilerMessage(MPSMakeConstants.BUILDER_ID, Kind.INFO, "child: " + n2.getName() + " id: " + n2.getSNodeId().toString()));
  //              }
  //            }
  //          }
          }
        }
      }
    }
  }

  private JpsLibSolution createLibSolution(JpsLibrary lib, JpsLibrary jdk, CompileContext ctx) {
    String name = lib.getName();
    SolutionDescriptor desc = new SolutionDescriptor();
    desc.setNamespace(name);

    if (JpsJavaSdkType.INSTANCE.equals(lib.getType()) && jdk == null) {
      ModuleId jdkId = ModuleId.regular(JDK_UUID);
      SModule existingModule = myRepository.getModule(jdkId);
      if (existingModule != null) {
        desc.setNamespace(existingModule.getModuleName());
        CustomClassLoadingFacet facet = existingModule.getFacet(CustomClassLoadingFacet.class);
        assert facet != null;
        Memento memento = new MementoImpl();
        facet.save(memento);
        desc.getModuleFacetDescriptors().add(new ModuleFacetDescriptor(((ModuleFacetBase) facet).getFacetType(), memento));
        Set<MPSModuleOwner> owners = new HashSet<MPSModuleOwner>(new ModuleRepositoryFacade(myRepository).getModuleOwners(existingModule));
        for (MPSModuleOwner owner : owners) {
//          if (owner == this) continue;
          // fixme wanted to used ModuleRepositoryFacade but it doesn't have exactly this method
          ((SRepositoryExt) myRepository).unregisterModule(existingModule, owner);
        }
      }
      desc.setId(jdkId);
    } else {
      desc.setId(ModuleId.foreign(name));
    }
    return new JpsLibSolution(desc, lib, jdk, ctx);
  }


  private List<JpsLibrary> getModuleSdks(JpsModule module, CompileContext ctx) {
    List<JpsLibrary> sdks = new ArrayList<JpsLibrary>();
    for (JpsDependencyElement dep : module.getDependenciesList().getDependencies()) {
      if (!(dep instanceof JpsSdkDependency)) continue;
      JpsLibrary lib = ((JpsSdkDependency) dep).resolveSdk();
      if (lib != null) {
        sdks.add(lib);
      }
    }
    return sdks;
  }

  public void dispose() {
    if (!isInitialized) {
      return;
    }
    myRepository.getModelAccess().runWriteAction(new Runnable() {
      @Override
      public void run() {
        new ModuleRepositoryFacade(myRepository).unregisterModules(myProject);
        myProject.dispose();
        myJpsToMpsModules.clear();
        myRepo = null;
      }
    });
    disposeMPS();
    isInitialized = false;
  }


  private void initMPS() {
    myPlatform = PlatformFactory.initPlatform(PlatformOptionsBuilder.ALL);

    myMPSBaseLanguage = new MPSBaseLanguage();
    myMPSBaseLanguage.init();
  }

  private void disposeMPS() {
    myMPSBaseLanguage.dispose();
    myPlatform.dispose();
  }

  @Override
  public boolean isHidden() {
    return true;
  }
}
