/*
 * Copyright 2003-2013 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.java.convert;

import com.intellij.facet.FacetManager;
import com.intellij.facet.FacetTypeRegistry;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMethod;
import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.ide.java.newparser.JavaParseException;
import jetbrains.mps.ide.java.newparser.JavaToMpsConverter;
import jetbrains.mps.ide.messages.MessagesViewTool;
import jetbrains.mps.ide.platform.watching.ReloadManager;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.vfs.IdeaFileSystem;
import jetbrains.mps.idea.core.facet.MPSFacet;
import jetbrains.mps.idea.core.facet.MPSFacetType;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNode;
import jetbrains.mps.idea.core.refactoring.NodePtr;
import jetbrains.mps.idea.java.psiStubs.JavaForeignIdBuilder;
import jetbrains.mps.progress.ProgressMonitorAdapter;
import jetbrains.mps.project.MPSProject;
import jetbrains.mps.smodel.DynamicReference;
import jetbrains.mps.smodel.StaticReference;
import jetbrains.mps.util.SNodeOperations;
import jetbrains.mps.vfs.FileSystem;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.FindUsagesFacade;
import org.jetbrains.mps.openapi.module.SModule;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * danilla 6/5/13
 */

public class ConvertPackageToModel extends AnAction {

  private static Logger LOG = Logger.getLogger("Convert java to mps");

  public ConvertPackageToModel() {
    super("Convert Java to MPS", "", null);
  }

  @Override
  public void update(AnActionEvent e) {
    PsiElement[] elements = e.getData(LangDataKeys.PSI_ELEMENT_ARRAY);
    Module module = e.getData(LangDataKeys.MODULE);

    if (elements == null
      || module == null
      || FacetManager.getInstance(module).getFacetByType(MPSFacetType.ID) == null
      || !containsJavaThings(elements)) {

      e.getPresentation().setVisible(false);
      e.getPresentation().setEnabled(false);
    }
  }

  @Override
  public void actionPerformed(final AnActionEvent e) {

    final PsiElement[] elements = e.getData(LangDataKeys.PSI_ELEMENT_ARRAY);
    final Module module = e.getData(LangDataKeys.MODULE);
    final Project project = e.getData(PlatformDataKeys.PROJECT);

    MPSFacet facet = FacetManager.getInstance(module).getFacetByType(MPSFacetType.ID);
    SModule mpsModule = facet.getSolution();
    final MPSProject mpsProject = e.getProject().getComponent(MPSProject.class);

    List<PsiJavaFile> psiJavaFiles = JavaConverterHelper.getFilesFromSelection(JavaConverterHelper.liftToFiles(Arrays.asList(elements)));

    Collection<Module> modulesWithoutFacet = JavaConverterHelper.getModulesThatNeedMPSFacet(psiJavaFiles);

    if (!modulesWithoutFacet.isEmpty()) {
      final AddFacetToModulesDialog dialog = new AddFacetToModulesDialog(project, module.getName(), modulesWithoutFacet);
      dialog.show();
      if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
        ApplicationManager.getApplication().runWriteAction(new Runnable() {
          @Override
          public void run() {
            for(Module moduleToAddFacet : dialog.getResult()) {
              FacetManager.getInstance(moduleToAddFacet).addFacet(
                FacetTypeRegistry.getInstance().findFacetType(MPSFacetType.ID), "", null);
            }
          }
        });
      }
    }

    final JavaToMpsConverter parser = new JavaToMpsConverter(mpsModule, mpsProject.getRepository(), true, true, project.getComponent(MessagesViewTool.class).newHandler());
    final List<IFile> javaFiles = toIFiles(psiJavaFiles);

    ApplicationManager.getApplication().saveAll();

    ProgressManager.getInstance().run(new Task.Modal(null, "Convert to MPS", false) {
      @Override
      public void run(@NotNull final ProgressIndicator progressIndicator) {

        try {
          parser.convertToMps(javaFiles, new ProgressMonitorAdapter(progressIndicator));

        } catch (JavaParseException exc) {
          throw new RuntimeException(exc);
        } catch (IOException exc) {
          throw new RuntimeException(exc);
        }
      }
    });

    // it was successful, so let's hack references that pointed to psi stubs and delete java files just converted

    mpsProject.getRepository().getModelAccess().executeCommand(new Runnable() {
      //    ApplicationManager.getApplication().runWriteAction(new Runnable() {
      @Override
      public void run() {

        Set<PsiClass> psiClasses = getPsiClasses(parser.getSuccessfulFiles(), PsiManager.getInstance(e.getProject()));

        Set<SNode> stubNodes = getStubNodes(psiClasses);
        Set<SNode> roots = parser.getRootsBuilt();
        List<SReference> referencesToFix = new ArrayList<SReference>();

        boolean wasUnresolved = false;

        Set<SReference> references = FindUsagesFacade.getInstance().findUsages(mpsProject.getScope(), stubNodes, null);
        for (SReference ref : references) {
          if (roots.contains(ref.getSourceNode().getContainingRoot())) {
            continue;
          }

          if (!(ref instanceof StaticReference)) {
            referencesToFix.add(ref);
            continue;
          }

          SNode source = ref.getSourceNode();
          String role = ref.getRole();
          SModelReference targetModelRef = ref.getTargetSModelReference();
          SNode targetNode = ref.getTargetNode();

          // TODO need to make it more efficient (maintain this data in DirParser)

          SModelReference newModelRef = null;
          String modelName = targetModelRef.getModelName();
          modelName = modelName.substring(0, modelName.indexOf('@'));
          for (SModel model : parser.getModels()) {
            if (modelName.equals(model.getModelName())) {
              newModelRef = model.getReference();
            }
          }

          if (newModelRef == null) {
            wasUnresolved = true;
            continue;
          }

          String resolveInfo = SNodeOperations.getResolveInfo(targetNode);
          SReference tempDynamicRef = new DynamicReference(role, source, newModelRef, resolveInfo);
          referencesToFix.add(tempDynamicRef);
          source.setReference(role, tempDynamicRef);

          SModel sourceModel = source.getModel();
          ((SModelBase) sourceModel).deleteModelImport(targetModelRef);
          if (!newModelRef.equals(sourceModel.getReference())) {
            // avoiding self-import
            ((SModelBase) sourceModel).addModelImport(newModelRef, false);
          }

          // better create static references right away
          // changing reference to a static reference pointing to mps node
//            SNode newTarget = stubToMpsNodes.get(stub);
//            SReference newRef = StaticReference.create(role, source, newTarget.getModel().getReference(), newTarget.getNodeId());
//            source.setReference(role, newRef);
        }

        for (SReference ref : referencesToFix) {
          SNode target = ref.getTargetNode();
          if (target == null) continue;

          SNode source = ref.getSourceNode();
          String role = ref.getRole();

          SReference finalStaticRef = StaticReference.create(role, source, target, ((DynamicReference) ref).getResolveInfo());
          source.setReference(role, finalStaticRef);
        }

        // here more complicated logic can be written
        // e.g. not delete, but rather unmark as source dir -- in case if
        // the resulting model(s) don't fall into the same directory where java was
        for (IFile file : parser.getSuccessfulFiles()) {
          file.delete();
        }

        if (wasUnresolved) {
          LOG.error("could not resolve some references");
        }

        // we want psi stub models to be up-to-date with regard to those deletions
        ReloadManager.getInstance().flush();
      }
    });

    // if the package wasn't a model before then we want our MPSTreeStructureProvider to do its work with
    // the directory node for the package
    ProjectView projectView = ProjectView.getInstance(project);
    projectView.refresh();
  }

  private boolean containsJavaThings(PsiElement[] elements) {
    for (PsiElement e : elements) {
      if (e instanceof PsiJavaFile) return true;
      if (e instanceof PsiClass && !(e instanceof MPSPsiNode)) return true;
    }
    for (PsiElement e : elements) {
      if (!(e instanceof PsiDirectory)) continue;
      if (containsJavaFiles((PsiDirectory) e)) return true;
    }
    return false;
  }

  private boolean containsJavaFiles(PsiDirectory dir) {
    for (PsiFile f : dir.getFiles()) {
      if (f instanceof PsiJavaFile) return true;
    }
    for (PsiDirectory d : dir.getSubdirectories()) {
      if (containsJavaFiles(d)) return true;
    }
    return false;
  }

  private List<IFile> toIFiles(List<? extends PsiFile> psiFiles) {
    List<IFile> result = new ArrayList<IFile>(psiFiles.size());

    for (PsiFile file : psiFiles) {
      VirtualFile vfile = file.getVirtualFile();
      IFile ifile = new IdeaFileSystem().getFile(vfile.getPath());
      result.add(ifile);
    }

    return result;
  }



  private Set<PsiClass> getPsiClasses(List<IFile> javaFiles, PsiManager psiManager) {
    Set<PsiClass> result = new HashSet<PsiClass>();
    for (IFile javaIFile : javaFiles) {
      VirtualFile vfile = VirtualFileManager.getInstance().findFileByUrl("file://" + javaIFile.getPath());
      PsiFile psiFile = psiManager.findFile(vfile);
      assert psiFile instanceof PsiJavaFile;

      result.addAll(Arrays.asList(((PsiJavaFile) psiFile).getClasses()));
    }

    return result;
  }

  private Set<SNode> getStubNodes(Set<PsiClass> psiClasses) {
    Set<SNode> result = new HashSet<SNode>();

    for (PsiClass cl : psiClasses) {
      putStubNodes(cl, result);
    }

    return result;
  }

  private void putStubNodes(PsiClass clas, Set<SNode> result) {
    putOneStubNode(clas, result);

    for (PsiClass cl : clas.getInnerClasses()) {
      putStubNodes(cl, result);
    }
    for (PsiMethod m : clas.getMethods()) {
      putOneStubNode(m, result);
    }
    for (PsiField f : clas.getFields()) {
      putOneStubNode(f, result);
    }
  }

  private void putOneStubNode(PsiElement element, Set<SNode> result) {
    NodePtr nodePtr = JavaForeignIdBuilder.computeNodePtr(element);
    if (nodePtr == null) return;
    // TODO change to project SRepository
    SNode node = nodePtr.toSNodeReference().resolve(ProjectHelper.getProjectRepository(element.getProject()));
    if (node == null) return;
    result.add(node);
  }


}
