/*
 * Copyright 2003-2015 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;

import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ui.JBUI;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.EditorPanelManagerImpl;
import jetbrains.mps.nodeEditor.InspectorTool;
import jetbrains.mps.nodeEditor.MementoPersistence;
import jetbrains.mps.nodeEditor.NodeEditorComponent;
import jetbrains.mps.nodeEditor.configuration.EditorConfigurationBuilder;
import jetbrains.mps.openapi.editor.Editor;
import jetbrains.mps.openapi.editor.EditorComponentState;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.EditorState;
import jetbrains.mps.openapi.editor.extensions.EditorExtensionUtil;
import jetbrains.mps.project.Project;
import jetbrains.mps.util.EqualUtil;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import java.awt.BorderLayout;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class BaseNodeEditor implements Editor {
  private static final Logger LOG = LogManager.getLogger(BaseNodeEditor.class);

  private NodeEditorComponent myEditorComponent;
  private final JComponent myComponent = new EditorPanel();
  private final JComponent myEditorPanel = new JPanel();
  protected final Project myProject;
  private JComponent myReplace = null;
  private SNodeReference myCurrentlyEditedNode = null;
  protected final Map<TaskType, PrioritizedTask> myType2TaskMap = new HashMap<>();
  private boolean mySelected;

  public BaseNodeEditor(@NotNull Project mpsProject) {
    myProject = mpsProject;
    myEditorPanel.setLayout(new BorderLayout());
    myEditorPanel.setBorder(new EmptyBorder(JBUI.emptyInsets()));
    myComponent.add(myEditorPanel, BorderLayout.CENTER);
    showEditor();
  }

  public abstract List<Document> getAllEditedDocuments();

  public JComponent getComponent() {
    return myComponent;
  }

  protected JComponent getEditorPanel() {
    return myEditorPanel;
  }

  @Override
  public NodeEditorComponent getCurrentEditorComponent() {
    return myEditorComponent;
  }

  @Override
  public EditorContext getEditorContext() {
    return myEditorComponent == null ? null : myEditorComponent.getEditorContext();
  }

  @Override
  public SNodeReference getCurrentlyEditedNode() {
    return myCurrentlyEditedNode;
  }

  protected void editNode(@NotNull final SNodeReference nodeToEdit, @Nullable final SNodeReference nodeToSelect) {
    assert myEditorComponent != null;
    executeInEDT(new PrioritizedTask(TaskType.EDIT_NODE, myType2TaskMap) {
      @Override
      public void performTask() {
        SNode node = nodeToEdit.resolve(myProject.getRepository());
        if (node == null) {
          return;
        }
        myEditorComponent.editNode(node);
        SNode toSelect = nodeToSelect == null ? null : nodeToSelect.resolve(myProject.getRepository());
        if (toSelect != null) {
          myEditorComponent.getEditorContext().selectWRTFocusPolicy(toSelect, false); // XXX findNodeCell(, true)? to reveal even folded?
        }
      }
    });
    myCurrentlyEditedNode = nodeToEdit;
  }

  protected void executeInEDT(PrioritizedTask task) {
    // XXX I'm not sure this is the right approach (used to be ModelAccess.isInEDT()) -
    // callers expect model read, and check for EDT only is sort of an implicit knowledge about reads/writes.
    // Why not runReadInEDT always?
    if (myProject.getModelAccess().canRead() && ThreadUtils.isInEDT()) {
      task.run();
    } else {
      myProject.getModelAccess().runReadInEDT(task);
    }
  }

  @Override
  public void dispose() {
    setEditorComponent(null);
  }

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

  protected abstract Object getData(@NonNls String dataId);

  private class EditorPanel extends JPanel implements DataProvider {
    private EditorPanel() {
      setLayout(new BorderLayout());
      setBorder(new EmptyBorder(JBUI.emptyInsets()));
    }

    @Override
    @Nullable
    public Object getData(@NonNls String dataId) {
      if (dataId.equals(MPSEditorDataKeys.MPS_EDITOR.getName())) {
        return BaseNodeEditor.this;
      }
      Object data = BaseNodeEditor.this.getData(dataId);
      if (data != null) {
        return data;
      }
      NodeEditorComponent editorComponent = getCurrentEditorComponent();
      return editorComponent == null ? null : editorComponent.getData(dataId);
    }
  }

  protected void showEditor() {
    if (myReplace != null) {
      myEditorPanel.remove(myReplace);
      myReplace = null;
    }
    NodeEditorComponent editorComponent =
        new NodeEditorComponent(myProject.getRepository(), new EditorConfigurationBuilder().editorPanelManager(new EditorPanelManagerImpl(myProject)));
    EditorExtensionUtil.extendUsingProject(editorComponent, myProject);
    setEditorComponent(editorComponent);
  }

  protected void showComponent(JComponent replace) {
    setEditorComponent(null);

    if (myReplace != null) {
      myEditorPanel.remove(myReplace);
      myReplace = null;
    }

    myReplace = replace;
    myEditorPanel.add(myReplace, BorderLayout.CENTER);
    myComponent.validate();
  }

  private void pauseOrResumeHighlighter() {
    if (myEditorComponent == null) {
      return;
    }
    myEditorComponent.getHighlighter().setPaused(!mySelected);
  }

  private void setEditorComponent(NodeEditorComponent editorComponent) {
    if (myEditorComponent == editorComponent) {
      return;
    }

    if (myEditorComponent != null) {
      // Remove old editor component
      myEditorPanel.remove(myEditorComponent.getExternalComponent());
      ThreadUtils.runInUIThreadNoWait(myEditorComponent::dispose);
      myEditorComponent = null;
      myCurrentlyEditedNode = null;
    }

    myEditorComponent = editorComponent;

    if (myEditorComponent != null) {
      // Add new editor component
      myEditorComponent = editorComponent;
      JComponent externalComponent = myEditorComponent.getExternalComponent();
      //HACK to avoid strange gray border in ScrollPane after empty aspect tab
      if (externalComponent.getComponent(0) instanceof JBScrollPane) {
        ((JBScrollPane) externalComponent.getComponent(0)).setBorder(new EmptyBorder(JBUI.emptyInsets()));
        ((JBScrollPane) externalComponent.getComponent(0)).getInsets().set(0, 0, 0, 0);
      }
      myEditorPanel.add(externalComponent, BorderLayout.CENTER);
      myComponent.validate();
      pauseOrResumeHighlighter();
    }
  }

  //---state---

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

  protected void saveState(BaseEditorState state) {
    EditorContext editorContext = getEditorContext();
    if (editorContext != null) {
      state.memento = editorContext.getEditorComponentState();
      NodeEditorComponent editorComponent = getCurrentEditorComponent();
      if (editorComponent != null) {
        state.isEditorFocused = editorComponent.getFocusTracker().getEffectiveFocusState();
        EditorComponent inspector = editorComponent.getInspector();
        if (inspector != null) {
          state.inspectorMemento = inspector.getEditorContext().getEditorComponentState();
          state.isInspectorFocused = !state.isEditorFocused && inspector.getFocusTracker().getEffectiveFocusState();
        }
      }
    }
  }

  @Override
  public void loadState(@NotNull EditorState state) {
    if (!(state instanceof BaseEditorState)) {
      return;
    }

    final BaseEditorState s = (BaseEditorState) state;
    final EditorContext editorContext = getEditorContext();
    final NodeEditorComponent editorComponent = getCurrentEditorComponent();
    if (s.memento == null || editorContext == null || editorComponent == null) {
      return;
    }
    final IdeFocusManager focusManager = myProject.getComponent(IdeFocusManager.class);

    executeInEDT(new PrioritizedTask(TaskType.EDITOR_MEMENTO, myType2TaskMap) {
      @Override
      public void performTask() {
        if (editorComponent.isDisposed()) {
          return;
        }
        editorContext.restoreEditorComponentState(s.memento);

        editorComponent.getFocusTracker().setEffectiveFocusState(s.isEditorFocused);
        if (s.isEditorFocused && focusManager != null) {
          focusManager.requestFocus(editorComponent, true);
        }
      }
    });
    if (s.inspectorMemento == null) {
      return;
    }
    final EditorComponent inspectorEditorComponent = editorComponent.getInspector();
    if (inspectorEditorComponent == null) {
      LOG.error("No inspector - memento will not be restored");
      return;
    }
    final EditorContext inspectorEditorContext = inspectorEditorComponent.getEditorContext();
    executeInEDT(new PrioritizedTask(TaskType.INSPECTOR_MEMENTO, myType2TaskMap) {
      @Override
      public void performTask() {
        inspectorEditorContext.restoreEditorComponentState(s.inspectorMemento);
        inspectorEditorComponent.getFocusTracker().setEffectiveFocusState(s.isInspectorFocused);
        if (s.isInspectorFocused && focusManager != null) {
          InspectorTool inspectorTool = myProject.getComponent(InspectorTool.class);
          if (inspectorTool != null && inspectorTool.isAvailable()) {
            inspectorTool.activate();
          }
          focusManager.requestFocus(inspectorEditorComponent, true);
        }
      }
    });
  }

  @Override
  public void selectNotify() {
    setSelected(true);
  }

  @Override
  public void deselectNotify() {
    setSelected(false);
  }

  private void setSelected(boolean selected) {
    mySelected = selected;
    pauseOrResumeHighlighter();
  }

  public abstract static class PrioritizedTask implements Runnable {
    private final TaskType myType;
    private final Map<TaskType, PrioritizedTask> myType2TaskMap;

    public PrioritizedTask(TaskType type, Map<TaskType, PrioritizedTask> type2TaskMap) {
      synchronized (type2TaskMap) {
        myType = type;
        myType2TaskMap = type2TaskMap;
        myType2TaskMap.put(myType, this);
      }
    }

    @Override
    public void run() {
      synchronized (myType2TaskMap) {
        if (myType2TaskMap.get(myType) != this) {
          return;
        }
        myType2TaskMap.remove(myType);
      }
      performTask();
    }

    protected abstract void performTask();
  }

  public enum TaskType {
    EDIT_NODE,
    EDITOR_MEMENTO,
    INSPECTOR_MEMENTO,
    UPDATE_PROPERTIES
  }

  public static class BaseEditorState implements EditorState {
    private static final String MEMENTO = "memento";
    private static final String INSPECTOR_MEMENTO = "inspectorMemento";
    private static final String FOCUSED_COMPONENT_EDITOR = "editorFocused";
    private static final String FOCUSED_COMPONENT_INSPECTOR = "inspectorFocused";

    private EditorComponentState memento;
    private boolean isEditorFocused;
    private EditorComponentState inspectorMemento;
    private boolean isInspectorFocused;

    @Override
    public void save(Element e) {
      if (memento != null) {
        Element mementoElem = new Element(MEMENTO);
        MementoPersistence.saveMemento(memento, mementoElem);
        e.addContent(mementoElem);
      }
      if (inspectorMemento != null) {
        Element mementoElem = new Element(INSPECTOR_MEMENTO);
        MementoPersistence.saveMemento(inspectorMemento, mementoElem);
        e.addContent(mementoElem);
      }

      if (isEditorFocused) {
        e.setAttribute(FOCUSED_COMPONENT_EDITOR, Boolean.TRUE.toString());
      }
      if (isInspectorFocused) {
        e.setAttribute(FOCUSED_COMPONENT_EDITOR, Boolean.TRUE.toString());
      }
    }

    @Override
    public void load(Element e) {
      Element mementoElem = e.getChild(MEMENTO);
      if (mementoElem != null) {
        memento = MementoPersistence.loadMemento(mementoElem);
      }
      Element inspectorMementoElem = e.getChild(INSPECTOR_MEMENTO);
      if (inspectorMementoElem != null) {
        inspectorMemento = MementoPersistence.loadMemento(inspectorMementoElem);
      }

      isEditorFocused = Boolean.parseBoolean(e.getAttributeValue(FOCUSED_COMPONENT_EDITOR));
      isInspectorFocused = Boolean.parseBoolean(e.getAttributeValue(FOCUSED_COMPONENT_INSPECTOR));
    }

    @Override
    public void clearSessionState() {
      if (memento != null) {
        memento.clearSessionState();
      }
      if (inspectorMemento != null) {
        inspectorMemento.clearSessionState();
      }
    }

    @Override
    public int hashCode() {
      int result = memento != null ? memento.hashCode() : 0;
      result = 31 * result + (inspectorMemento != null ? inspectorMemento.hashCode() : 0);
      return result;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }

      BaseEditorState that = (BaseEditorState) o;
      return EqualUtil.equals(that.memento, memento) && EqualUtil.equals(that.inspectorMemento, inspectorMemento) &&
             that.isEditorFocused == isEditorFocused && that.isInspectorFocused == isInspectorFocused;
    }
  }
}
