/*
 * 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.ide.projectPane.logicalview.highlighting.visitor;

import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.DumbService;
import jetbrains.mps.extapi.model.GeneratableSModel;
import jetbrains.mps.generator.ModelGenerationStatusManager;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.projectPane.logicalview.highlighting.visitor.updates.AdditionalTextNodeUpdate;
import jetbrains.mps.ide.ui.tree.module.NamespaceTextNode;
import jetbrains.mps.ide.ui.tree.module.ProjectModuleTreeNode;
import jetbrains.mps.ide.ui.tree.smodel.SModelTreeNode;
import jetbrains.mps.make.IMakeService;
import jetbrains.mps.project.Project;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.Language;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;

import javax.swing.tree.TreeNode;

public class GenStatusUpdater extends TreeUpdateVisitor {

  public GenStatusUpdater(Project mpsProject) {
    super(mpsProject);
  }

  private ProjectModuleTreeNode getContainingModuleNode(TreeNode node) {
    do {
      node = node.getParent();
      if (node == null) return null;
    } while (!(node instanceof ProjectModuleTreeNode));
    return (ProjectModuleTreeNode) node;
  }

  private boolean isTimeToRelax() {
    if (IMakeService.INSTANCE.isSessionActive()) {
      return true;
    }

    Application application = ApplicationManager.getApplication();
    return (application.isDisposed() || application.isDisposeInProgress() || myProject.isDisposed());
  }

  @Override
  public void visitModuleNode(@NotNull final ProjectModuleTreeNode node) {
    // XXX might be fruitful to have pre/post visit notifications, so that we can get rid of propagateStatusToNamespaceNodes (do it from post visit)
    if (node.isInitialized()) {
      // we've got children (SModelTreeNodes) and there's update for them in #visitModelNode(), below
      return;
    }
    scheduleModelRead(node, new Runnable() {
      @Override
      public void run() {
        if (node.getModule().isReadOnly()) {
          new StatusUpdate(node).update(GenerationStatus.READONLY);
          return;
        }
        final com.intellij.openapi.project.Project project = ProjectHelper.toIdeaProject(myProject);
        if (project != null && DumbService.getInstance(project).isDumb()) {
          // see visitModelNode for explanation
          propagateStatusToNamespaceNodes(node, GenerationStatus.UPDATING);
          return;
        }
        GenerationStatus s = new StatusUpdate(node).update();
        // no need to check for generator and language here as #visitModelNode does, as now
        // we can face generator module only as sibling to language's models (i.e. SModelTreeNodes)
        propagateStatusToNamespaceNodes(node, s);
      }
    });
  }

  @Override
  public void visitModelNode(@NotNull final SModelTreeNode modelNode) {
    scheduleModelRead(modelNode, new Runnable() {
      @Override
      public void run() {
        if (isTimeToRelax()) {
          return;
        }

        SModel md = modelNode.getModel();
        if (!(md instanceof EditableSModel)) {
          return;
        }
        if (!(md instanceof GeneratableSModel)) {
          return;
        }
        if (md.getModule() == null) return;

        boolean wasChanged = ((EditableSModel) md).isChanged();

        if (!wasChanged && !((GeneratableSModel) md).isGeneratable()) {
          // changing doNotGenerate := true immediately renders the model notGeneratable
          // while GenStatusUpdater needs to update its status
          return;
        }

        final ProjectModuleTreeNode moduleNode = getContainingModuleNode(modelNode);
        if (moduleNode == null) {
          return;
        }
        if (moduleNode.getModule().isReadOnly()) {
          new StatusUpdate(modelNode).update(GenerationStatus.READONLY);
          new StatusUpdate(moduleNode).update(GenerationStatus.READONLY);
          return;
        }

        final com.intellij.openapi.project.Project project = ProjectHelper.toIdeaProject(myProject);
        if (project != null && DumbService.getInstance(project).isDumb()) {
          // while idea updates its index, we can't use index to check model hashes.
          // of course, we can calculate hash again (i.e. if none in index found),
          // however, as long as we use index for hashes, seems reasonable to wait for end of dumb mode
          // and to update status again then (PPTH.dumbUpdate does that).
          // Here, I don't care to set status of individual models and modules - status for a group seems to be enough
          propagateStatusToNamespaceNodes(moduleNode, GenerationStatus.UPDATING);
          return;
        }

        new StatusUpdate(modelNode).update();
        GenerationStatus s = new StatusUpdate(moduleNode).update();
        if (moduleNode.getModule() instanceof Generator) {
          final ProjectModuleTreeNode languageNode = getContainingModuleNode(moduleNode);
          if (languageNode != null) {
            new StatusUpdate(languageNode).update(s);
          }
        }
        propagateStatusToNamespaceNodes(moduleNode, s);
      }
    });
  }

  private void propagateStatusToNamespaceNodes(ProjectModuleTreeNode node, GenerationStatus status) {
    final AdditionalTextNodeUpdate r = new AdditionalTextNodeUpdate(status.getMessage());
    for (TreeNode n = node; n != null; n = n.getParent()) {
      if (n instanceof NamespaceTextNode) {
        addUpdate((NamespaceTextNode) n, r);
      }
    }
  }

  private class StatusUpdate {
    private final SModelTreeNode myModelNode;
    private final ProjectModuleTreeNode myModuleNode;

    StatusUpdate(ProjectModuleTreeNode moduleNode) {
      myModuleNode = moduleNode;
      myModelNode = null;
    }
    StatusUpdate(SModelTreeNode modelNode) {
      myModuleNode = null;
      myModelNode = modelNode;
    }
    public GenerationStatus update() {
      if (myModuleNode == null && myModelNode == null) {
        return null;
      }
      // FIXME update is inside model read already, no need to wrap once again
      GenerationStatus status = compute();
      update(status);
      return status;
    }
    public void update(GenerationStatus status) {
      if (myModelNode != null) {
        addUpdate(myModelNode, new AdditionalTextNodeUpdate(status.getMessage()));
      }
      if (myModuleNode != null) {
        addUpdate(myModuleNode, new AdditionalTextNodeUpdate(status.getMessage()));
      }
    }

    private GenerationStatus compute() {
      if (myModelNode != null) {
        // extra check before read action
        if (myModelNode.getModel().getModule() == null) {
          return GenerationStatus.NOT_REQUIRED;
        }
        return getGenerationStatus(myModelNode);
      }
      if (myModuleNode != null) {
        return getGenerationStatus(myModuleNode);
      }
      throw new IllegalStateException();
    }
  }

  private static boolean generationRequired(SModule module) {
    for (SModel md : module.getModels()) {
      if (ModelGenerationStatusManager.getInstance().generationRequired(md)) {
        return true;
      }
    }
    return false;
  }

  static GenerationStatus getGenerationStatus(ProjectModuleTreeNode node) {
    SModule module = node.getModule();
    if (module.isReadOnly()) {
      return GenerationStatus.READONLY;
    }
    if (generationRequired(module)) return GenerationStatus.REQUIRED;
    if (module instanceof Language) {
      for (Generator generator : ((Language) module).getGenerators()) {
        if (generationRequired(generator)) return GenerationStatus.REQUIRED;
      }
    }
    return GenerationStatus.NOT_REQUIRED;
  }

  private static GenerationStatus getGenerationStatus(SModelTreeNode node) {
    if (node.getModel() == null) return GenerationStatus.NOT_REQUIRED;
    if (isPackaged(node)) return GenerationStatus.READONLY;
    if (isDoNotGenerate(node)) return GenerationStatus.DO_NOT_GENERATE;

    boolean required = ModelGenerationStatusManager.getInstance().generationRequired(node.getModel());
    return required ? GenerationStatus.REQUIRED : GenerationStatus.NOT_REQUIRED;
  }

  private static boolean isPackaged(SModelTreeNode node) {
    SModel md = node.getModel();
    if (!(md instanceof EditableSModel)) return false;
    return md.isReadOnly();
  }

  private static boolean isDoNotGenerate(SModelTreeNode node) {
    SModel md = node.getModel();
    if (!(md instanceof GeneratableSModel)) return false;
    return ((GeneratableSModel) md).isDoNotGenerate();
  }

  public static enum GenerationStatus {
    READONLY("read only"),
    DO_NOT_GENERATE("do not generate"),
    UPDATING("updating..."),
    REQUIRED("generation required"),
    NOT_REQUIRED(null);

    private String myMessage;

    GenerationStatus(String message) {
      myMessage = message;
    }

    @Nullable
    public String getMessage() {
      return myMessage;
    }
  }
}
