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

import com.intellij.facet.FacetManager;
import com.intellij.facet.ModifiableFacetModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.ModuleAdapter;
import com.intellij.openapi.project.Project;
import com.intellij.util.Function;
import jetbrains.mps.ide.findusages.model.scopes.ProjectScope;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.progress.EmptyProgressMonitor;
import jetbrains.mps.project.Solution;
import jetbrains.mps.smodel.ModelImports;
import jetbrains.mps.smodel.StaticReference;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelId;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.FindUsagesFacade;
import org.jetbrains.mps.openapi.module.ModelAccess;
import org.jetbrains.mps.openapi.module.SearchScope;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Created by danilla on 6/15/15.
 */
public class ModuleRenameHandler extends ModuleAdapter {
  @Override
  public void modulesRenamed(@NotNull final Project project, @NotNull List<Module> modules, @NotNull Function<Module, String> oldNameProvider) {
    for (final Module module : modules) {
      final FacetManager facetManager = FacetManager.getInstance(module);
      final MPSFacet facet = facetManager.getFacetByType(MPSFacetType.ID);
      if (facet == null) {
        continue;
      }

      ModelAccess modelAccess = ProjectHelper.getModelAccess(project);
      if (modelAccess == null) {
        return;
      }

      modelAccess.executeCommand(() -> {
        Set<SModelReference> renamedModelRefs = new HashSet<>();

        // collecting all models of the renamed module, their SModelReferences will be changed
        for (SModel model : facet.getSolution().getModels()) {
          renamedModelRefs.add(model.getReference());
        }

        // finding usages before we modify the solution
        SearchScope projectScope = new ProjectScope(ProjectHelper.fromIdeaProject(project));
        Set<SModel> modelUsages = FindUsagesFacade.getInstance().findModelUsages(projectScope, renamedModelRefs, new EmptyProgressMonitor());

        // now handling the case when there are usages within the renamed module itself
        // if we don't handle it, we'll end up patching refs in models which are already disposed,
        // because we patch after solution reload
        Iterator<SModel> usagesIterator = modelUsages.iterator();
        // remembering ids of such in-module usages
        Set<SModelId> ids = new HashSet<>();
        while (usagesIterator.hasNext()) {
          SModel usage = usagesIterator.next();
          if (usage.getModule() == facet.getSolution() && usage instanceof EditableSModel) {
            // it's in the same module and it's not derived (it's editable)
            ids.add(usage.getModelId());
            // don't want _this_instance_ of SModel in usages, as it will be disposed after we reload the module
            usagesIterator.remove();
          }
        }

        // resetting facet => reloading solution
        final Solution newSolution = resetFacet(facet);

        // adding to usages re-created in-modules models: those models which were found as usages,
        // but which have been re-created by now as a result of solution reload
        for (SModelId id: ids) {
          modelUsages.add(newSolution.getModel(id));
        }

        // actually fixing usages
        renameUsages(renamedModelRefs, modelUsages, mRef -> {
          String modelName = mRef.getModelName();
          for (SModel m: newSolution.getModels()) {
            if (modelName.equals(m.getName().getValue())) {
              return m.getReference();
            }
          }
          assert false : "Cannot recover model usages after idea module rename";
          return null;
        });
      });
    }
  }

  private Solution resetFacet(MPSFacet facet) {
    Module module = facet.getModule();
    FacetManager facetManager = FacetManager.getInstance(module);

    ModifiableFacetModel mm = facetManager.createModifiableModel();
    mm.removeFacet(facet);
    mm.commit();

    mm = facetManager.createModifiableModel();
    facet.getConfiguration().getBean().setIdByModuleName(module.getName());
    facet.getConfiguration().getBean().getSolutionDescriptor().setNamespace(module.getName());
    final MPSFacet newFacet = new MPSFacet(new MPSFacetType(), module, MPSFacetConstants.MPS_FACET_NAME, facet.getConfiguration(), null);
    mm.addFacet(newFacet);
    mm.commit();

    return newFacet.getSolution();
  }

  // todo reuse from PackageRenameListener
  private void renameUsages(Set<SModelReference> renamedModels, Set<SModel> modelUsages, Function<SModelReference, SModelReference> renameFunc) {
    for (SModel model : modelUsages) {
      ModelImports modelImports = new ModelImports(model);

      Map<SModelReference, SModelReference> changes = new HashMap<>();

      for (SModelReference modelReference : modelImports.getImportedModels()) {
        if (!renamedModels.contains(modelReference)) {
          continue;
        }

        SModelReference newModelRef = renameFunc.fun(modelReference);
        modelImports.removeModelImport(modelReference);
        modelImports.addModelImport(newModelRef);
        changes.put(modelReference, newModelRef);
      }

      updateSReferences(model, changes);
    }
  }

  private void updateSReferences(SModel model, Map<SModelReference, SModelReference> modelRefChange) {
    for (SNode node : SNodeUtil.getDescendants(model)) {
      for (SReference ref : node.getReferences()) {
        if (!(ref instanceof StaticReference)) {
          continue;
        }
        SModelReference modelRef = ref.getTargetSModelReference();
        if (modelRefChange.containsKey(modelRef)) {
          ((StaticReference) ref).setTargetSModelReference(modelRefChange.get(modelRef));
        }
      }
    }
  }

}
