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

import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.actionSystem.Separator;
import com.intellij.openapi.actionSystem.impl.ActionButton;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.ui.ShadowAction;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import jetbrains.mps.ide.ModelReadAction;
import jetbrains.mps.ide.editor.BaseNodeEditor;
import jetbrains.mps.ide.editor.MPSEditorDataKeys;
import jetbrains.mps.ide.editorTabs.tabfactory.NodeChangeCallback;
import jetbrains.mps.ide.editorTabs.tabfactory.TabComponentFactory;
import jetbrains.mps.ide.editorTabs.tabfactory.TabsComponent;
import jetbrains.mps.ide.editorTabs.tabfactory.tabs.AddAspectAction;
import jetbrains.mps.ide.editorTabs.tabfactory.tabs.CreateGroupsBuilder;
import jetbrains.mps.ide.editorTabs.tabfactory.tabs.CreateModeCallback;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.nodeEditor.EditorSettings;
import jetbrains.mps.nodeEditor.EditorSettingsListener;
import jetbrains.mps.nodefs.MPSNodeVirtualFile;
import jetbrains.mps.nodefs.NodeVirtualFileSystem;
import jetbrains.mps.openapi.editor.EditorState;
import jetbrains.mps.plugins.relations.RelationDescriptor;
import jetbrains.mps.project.Project;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.util.EqualUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.event.SPropertyChangeEvent;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeChangeListenerAdapter;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModule;

import javax.swing.JComponent;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public class TabbedEditor extends BaseNodeEditor {
  private TabsComponent myTabsComponent;
  private final MyNameListener myNameListener = new MyNameListener();
  private final SNodeReference myBaseNode;
  private final Set<RelationDescriptor> myPossibleTabs;
  private final ShadowAction myNextTabAction, myPrevTabAction;
  // UI container to hold tab UI components plus auxiliary controls like 'Add aspect' action and alike.
  private final JPanel myTabsPanel;
  private final RepoChangeListener myRepoChangeListener = new RepoChangeListener();
  private final FileStatusChangeListener myFileStatusListener = new FileStatusChangeListener();

  private final EditorSettingsListener mySettingsListener = new EditorSettingsListener() {
    @Override
    public void settingsChanged() {
      final SNodeReference node = getCurrentlyEditedNode();
      JComponent comp = myTabsComponent.getComponent();
      if (comp != null) {
        myTabsPanel.remove(comp);
      }
      myProject.getModelAccess().runReadInEDT(new Runnable() {
        @Override
        public void run() {
          if (myDisposed) {
            return;
          }
          installTabsComponent();
          if (node != null) {
            myTabsComponent.updateTabs();
            myTabsComponent.editNode(node);
          }
        }
      });
    }
  };

  private final MPSNodeVirtualFile myVirtualFile;
  private boolean myDisposed;

  public TabbedEditor(SNodeReference baseNode, Set<RelationDescriptor> possibleTabs, @NotNull Project mpsProject) {
    super(mpsProject);
    myBaseNode = baseNode;
    myPossibleTabs = possibleTabs;

    myVirtualFile = NodeVirtualFileSystem.getInstance().getFileFor(mpsProject.getRepository(), myBaseNode);

    myTabsPanel = new JPanel(new BorderLayout());
    // bloody BaseNodeEditor makes us know about layout used there
    getComponent().add(myTabsPanel, BorderLayout.SOUTH);

    installTabsComponent();

    showNode(myBaseNode.resolve(myProject.getRepository()), false);

    myNextTabAction = new ShadowAction(new BaseNavigationAction(() -> myTabsComponent.nextTab()),
        ActionManager.getInstance().getAction(IdeActions.ACTION_NEXT_EDITOR_TAB), getComponent());
    myPrevTabAction = new ShadowAction(new BaseNavigationAction(() -> myTabsComponent.prevTab()),
        ActionManager.getInstance().getAction(IdeActions.ACTION_PREVIOUS_EDITOR_TAB), getComponent());

    final AnAction addAction = new AddAspectAction(mpsProject, myBaseNode, myPossibleTabs, new SetTabsComponentNode()) {
      @Override
      protected RelationDescriptor getCurrentAspect() {
        return myTabsComponent.getCurrentTabAspect();
      }
    };
    ActionButton btn = new ActionButton(addAction, addAction.getTemplatePresentation(), ActionPlaces.UNKNOWN, new Dimension(23, 23));
    myTabsPanel.add(btn, BorderLayout.WEST);

    EditorSettings.getInstance().addEditorSettingsListener(mySettingsListener);
    myRepoChangeListener.subscribeTo(myProject.getRepository());
    myFileStatusListener.attach(myProject);
  }

  private void installTabsComponent() {
    if (myTabsComponent != null) {
      myTabsComponent.dispose();
    }
    final NodeChangeCallback nodeChangeCallback = newNode -> showNodeInternal(newNode);
    final CreateModeCallback createAspectCallback = new CreateModeCallback() {
      @Override
      public void create(RelationDescriptor tab) {
        // FIXME what if we create two+ aspects in a row, who's responsible to dispose inactive CreatePanel instances?
        final CreatePanel cp = new CreatePanel(myProject, myBaseNode, new SetTabsComponentNode(), tab);
        showComponent(cp);
        final IdeFocusManager fm = IdeFocusManager.getInstance(ProjectHelper.toIdeaProject(myProject));
        fm.doWhenFocusSettlesDown(() -> fm.requestFocus(cp, false));
      }
    };
    myTabsComponent = TabComponentFactory.createTabsComponent(myBaseNode, myPossibleTabs, getEditorPanel(), nodeChangeCallback, createAspectCallback,
        ProjectHelper.toIdeaProject(myProject));

    myRepoChangeListener.setTabController(myTabsComponent);
    myFileStatusListener.setTabController(myTabsComponent, myBaseNode);

    JComponent c = myTabsComponent.getComponent();
    if (c != null) {
      myTabsPanel.add(c, BorderLayout.CENTER);
    }
  }

  @Override
  public void dispose() {
    myDisposed = true;
    myFileStatusListener.detach();
    EditorSettings.getInstance().removeEditorSettingsListener(mySettingsListener);

    myNextTabAction.dispose();
    myPrevTabAction.dispose();
    myProject.getModelAccess().runReadAction(() -> {
      myRepoChangeListener.unsubscribeFrom(myProject.getRepository());
      myNameListener.detach();
    });
    myTabsComponent.dispose();
    super.dispose();
  }

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

  @Override
  public List<Document> getAllEditedDocuments() {
    return myTabsComponent.getAllEditedDocuments();
  }

  @Override
  public void showNode(SNode node, boolean select) {
    SNodeReference currentNodeReference = getCurrentlyEditedNode();
    SNodeReference newNodeReference = node.getReference();
    if (currentNodeReference != null && currentNodeReference.equals(newNodeReference)) {
      return;
    }

    if (currentNodeReference == null) {
      showEditor();
    }
    myTabsComponent.updateTabs();
    myTabsComponent.editNode(newNodeReference);
  }

  private void showNodeInternal(SNodeReference nodeRef) {
    if (getCurrentEditorComponent() == null) {
      showEditor();
    }
    myNameListener.detach();

    if (nodeRef == null) {
      // Null means that it is empty tab - just update tab header
      executeInEDT(new PrioritizedTask(TaskType.UPDATE_PROPERTIES, myType2TaskMap) {
        @Override
        public void performTask() {
          updateProperties();
        }
      });
      return;
    }

    SNode node = nodeRef.resolve(myProject.getRepository());

    if (node == null || node.getModel() == null) {
      // FIXME suggest create new? Use CreatePanel?
      return;
    }

    SModel md = node.getModel();
    SModule module = md.getModule();
    assert module != null : md.getReference().toString() + "; node is disposed = " + !org.jetbrains.mps.openapi.model.SNodeUtil.isAccessible(node,
        myProject.getRepository());
    SNodeReference selection = nodeRef;
    if (myTabsComponent.getCurrentTabAspect() != null) {
      Collection<SNodeReference> a = myTabsComponent.getSelectionFor(myTabsComponent.getCurrentTabAspect(), nodeRef);
      selection = a.isEmpty() ? selection : a.iterator().next();
    }
    editNode(nodeRef, selection);

    myNameListener.attach(md);

    executeInEDT(new PrioritizedTask(TaskType.UPDATE_PROPERTIES, myType2TaskMap) {
      @Override
      public void performTask() {
        updateProperties();
      }
    });
  }

  /*package*/ void updateProperties() {
    final com.intellij.openapi.project.Project project = ProjectHelper.toIdeaProject(myProject);
    FileEditorManagerEx manager = FileEditorManagerEx.getInstanceEx(project);
    VirtualFile virtualFile = manager.getCurrentFile();
    if (virtualFile != null) {
      FileStatusManager.getInstance(project).fileStatusChanged(virtualFile);
      manager.updateFilePresentation(virtualFile);
    }
  }


  @Override
  public Object getData(@NonNls String dataId) {
    if (MPSEditorDataKeys.EDITOR_CREATE_GROUP.getName().equals(dataId)) {
      return getCreateGroup();
    }
    if (dataId.equals(LangDataKeys.VIRTUAL_FILE.getName())) {
      return myVirtualFile;
    }

    return null;
  }

  private AnAction getCreateGroup() {
    DefaultActionGroup result = new DefaultActionGroup();

    List<DefaultActionGroup> groups =
        new CreateGroupsBuilder(myProject, myBaseNode, new SetTabsComponentNode()).getCreateGroups(myPossibleTabs, myTabsComponent.getCurrentTabAspect());
    for (DefaultActionGroup group : groups) {
      group.setPopup(false);
      result.add(group);
      result.add(new Separator());
    }

    return result;
  }

  private class MyNameListener extends SNodeChangeListenerAdapter {
    private SModel myLastAttachModel;

    synchronized void attach(@Nullable SModel model) {
      detach();
      myLastAttachModel = model;
      if (model != null) {
        model.addChangeListener(this);
      }
    }

    synchronized void detach() {
      if (myLastAttachModel != null) {
        myLastAttachModel.removeChangeListener(this);
        myLastAttachModel = null;
      }
    }

    @Override
    public void propertyChanged(@NotNull SPropertyChangeEvent event) {
      if (SNodeUtil.property_INamedConcept_name.equals(event.getProperty()) && event.getNode().getReference().equals(getCurrentlyEditedNode())) {
        updateProperties();
      }
    }
  }

  @Override
  public EditorState saveState() {
    TabbedEditorState state = new TabbedEditorState();
    saveState(state);
    return state;
  }

  protected void saveState(TabbedEditorState state) {
    super.saveState(state);
    state.setNode(getCurrentlyEditedNode());
  }

  @Override
  public void loadState(@NotNull final EditorState state) {
    myProject.getModelAccess().runReadAction(() -> {
      if (state instanceof TabbedEditorState) {
        SNodeReference nodePointer = ((TabbedEditorState) state).getNode();
        SNode node = nodePointer == null ? null : nodePointer.resolve(myProject.getRepository());
        if (node != null) {
          showNode(node, false);
        }
      } else {
        //regular editor was shown for that node last time
        showNode(myBaseNode.resolve(myProject.getRepository()), false);
      }
    });
    super.loadState(state);
  }

  public final static class TabbedEditorState extends BaseEditorState implements EditorState {
    private static final String NODE = "node";
    private static final String NODE_REF = "node_ref";

    private SNodeReference myCurrentNode;

    /*package*/ void setNode(@Nullable SNodeReference ref) {
      myCurrentNode = ref;
    }

    @Nullable
    /*package*/ SNodeReference getNode() {
      return myCurrentNode;
    }

    @Override
    public void save(Element e) {
      super.save(e);
      boolean createNewElement = (e.getChild(NODE) == null);
      Element node = createNewElement ? new Element(NODE) : e.getChild(NODE);
      if (myCurrentNode != null) {
        node.setAttribute(NODE_REF, jetbrains.mps.smodel.SNodePointer.serialize(myCurrentNode));
      }
      if (createNewElement) {
        e.addContent(node);
      }
    }

    @Override
    public void load(Element e) {
      super.load(e);
      Element nodeElem = e.getChild(NODE);
      String val = nodeElem.getAttributeValue(NODE_REF);
      if (val != null) {
        myCurrentNode = jetbrains.mps.smodel.SNodePointer.deserialize(val);
      }
    }

    public int hashCode() {
      return super.hashCode() * 13 + (myCurrentNode == null ? 0 : myCurrentNode.hashCode());
    }

    public boolean equals(Object obj) {
      return obj instanceof TabbedEditorState && super.equals(obj) && EqualUtil.equals(myCurrentNode, ((TabbedEditorState) obj).myCurrentNode);
    }
  }

  private static class BaseNavigationAction extends ModelReadAction {
    public BaseNavigationAction(Runnable delegate) {
      super(null, delegate);
      setEnabledInModalContext(true);
    }
  }

  private class SetTabsComponentNode implements NodeChangeCallback {
    @Override
    public void changeNode(SNodeReference newNode) {
      myTabsComponent.updateTabs();
      myTabsComponent.editNode(newNode);
    }
  }
}
