/*
 * Copyright 2003-2012 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.debugger.breakpoints;

import com.intellij.debugger.DebuggerManagerEx;
import com.intellij.debugger.ui.breakpoints.Breakpoint;
import com.intellij.debugger.ui.breakpoints.BreakpointWithHighlighter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.xdebugger.XDebuggerManager;
import com.intellij.xdebugger.XDebuggerUtil;
import com.intellij.xdebugger.breakpoints.XBreakpoint;
import com.intellij.xdebugger.breakpoints.XBreakpointListener;
import jetbrains.mps.debugger.core.breakpoints.BreakpointsUiComponentEx;
import jetbrains.mps.ide.editor.util.EditorComponentUtil;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.idea.java.trace.GeneratedSourcePosition;
import jetbrains.mps.nodeEditor.AdditionalPainter;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.util.Computable;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class IdeaBreakpointsUiComponent extends BreakpointsUiComponentEx<Breakpoint, BreakpointWithHighlighter> implements ProjectComponent {
  private static final Logger LOG = LogManager.getLogger(IdeaBreakpointsUiComponent.class);
  private DebuggerManagerEx myDebuggerManager;
  private final XBreakpointListener myBreakpointListener = new MyBreakpointListener();

  public IdeaBreakpointsUiComponent(Project project, FileEditorManager manager) {
    super(project, manager);
  }

  @Override
  public void projectOpened() {
  }

  @Override
  public void projectClosed() {
  }

  @Override
  public void initComponent() {
    super.init();
    myDebuggerManager = DebuggerManagerEx.getInstanceEx(myProject);
    XDebuggerManager.getInstance(myProject).getBreakpointManager().addBreakpointListener(myBreakpointListener);
  }

  @Override
  public void disposeComponent() {
    super.dispose();
    XDebuggerManager.getInstance(myProject).getBreakpointManager().removeBreakpointListener(myBreakpointListener);
    myDebuggerManager = null;
  }

  @NotNull
  @Override
  public String getComponentName() {
    return "Idea Breakpoints In MPS Component";
  }

  @Override
  protected Set<BreakpointWithHighlighter> getBreakpointsForComponent(@NotNull final EditorComponent component) {
    final Set<BreakpointWithHighlighter> result = new HashSet<BreakpointWithHighlighter>();
    final List<Breakpoint> breakpoints = myDebuggerManager.getBreakpointManager().getBreakpoints(); //XDebuggerManager.getInstance(myProject).getBreakpointManager()
    ProjectHelper.getModelAccess(myProject).runReadAction(new Runnable() {
      @Override
      public void run() {
        for (Breakpoint breakpoint : breakpoints) {
          if (breakpoint instanceof BreakpointWithHighlighter) {
            BreakpointWithHighlighter breakpointWithHighlighter = (BreakpointWithHighlighter) breakpoint;
            SNode node = BreakpointPainter.getNodeForBreakpoint(breakpointWithHighlighter);
            if (node != null && EditorComponentUtil.isNodeShownInTheComponent(component, node)) {
              result.add(breakpointWithHighlighter);
            }
          }
        }
      }
    });
    return result;
  }

  @Override
  @NotNull
  protected List<EditorComponent> getComponentsForBreakpoint(@NotNull final BreakpointWithHighlighter breakpoint) {
    return new ModelAccessHelper(ProjectHelper.getModelAccess(myProject)).runReadAction(new Computable<List<EditorComponent>>() {
      public List<EditorComponent> compute() {
        SNode node = BreakpointPainter.getNodeForBreakpoint(breakpoint);
        if (node != null) {
          return EditorComponentUtil.findComponentForNode(node, myFileEditorManager);
        }
        return Collections.emptyList();
      }
    });
  }


  @Override
  protected BreakpointPainter createPainter(BreakpointWithHighlighter breakpoint) {
    return new BreakpointPainter(breakpoint);
  }

  @Override
  protected BreakpointIconRenderrer createRenderrer(BreakpointWithHighlighter breakpoint, EditorComponent component) {
    return new BreakpointIconRenderrer(breakpoint, component);
  }

  @Override
  protected void toggleBreakpoint(final SNode node) {
    boolean breakpointWasSet = false;
    GeneratedSourcePosition sourcePosition = GeneratedSourcePosition.fromNode(node);
    if (sourcePosition != null) {
      PsiFile psiFile = sourcePosition.getPsiFile(myProject);
      if (psiFile != null) {
        Document document = PsiDocumentManager.getInstance(myProject).getDocument(psiFile);
        if (document != null) {
          if (!hasMethodOrFieldsAtLine(document, psiFile, sourcePosition.getLineNumber())) {
            breakpointWasSet = toggleAtLine(psiFile, sourcePosition.getLineNumber());
          }
        }
      }
    }
    if (!breakpointWasSet) {
      LOG.debug("Failed to create a breakpoint at this location");
    }
  }

  private boolean toggleAtLine(PsiFile psiFile, int lineNumber) {
    XDebuggerUtil debuggerUtil = XDebuggerUtil.getInstance();
    if (debuggerUtil.canPutBreakpointAt(myProject, psiFile.getVirtualFile(), lineNumber)) {
      debuggerUtil.toggleLineBreakpoint(myProject, psiFile.getVirtualFile(), lineNumber);
      return true;
    }
    return false;
  }

  private boolean hasMethodOrFieldsAtLine(@NotNull Document document, @NotNull PsiFile file, int lineNumber) {
    int lineStartOffset = document.getLineStartOffset(lineNumber);
    int lineEndOffset = document.getLineEndOffset(lineNumber);
    Class<? extends PsiElement>[] elementsLookingFor = new Class[]{PsiMethod.class, PsiField.class};

    for (int i = lineStartOffset; i < lineEndOffset; ++i) {
      for (Class<? extends PsiElement> elementClass : elementsLookingFor) {
        PsiElement psiElement = PsiTreeUtil.findElementOfClassAtOffset(file, i, elementClass, true);
        if (psiElement != null && document.getLineNumber(psiElement.getTextOffset()) == lineNumber) {
          return true;
        }
      }
    }
    return false;
  }

  @Override
  protected EditorCell findDebuggableOrTraceableCell(EditorCell cell) {
    return findTraceableCell(cell);
  }

  private void clearAllEditors() {
    List<EditorComponent> allEditorComponents = EditorComponentUtil.getAllEditorComponents(myFileEditorManager, true);
    for (EditorComponent component : allEditorComponents) {
      component.getLeftEditorHighlighter().removeAllIconRenderers(BreakpointIconRenderrer.TYPE);
      List<AdditionalPainter> additionalPainters = component.getAdditionalPainters();
      for (AdditionalPainter painter : additionalPainters) {
        if (painter instanceof BreakpointPainter) {
          component.removeAdditionalPainter(painter);
        }
      }
    }
  }

  private class MyBreakpointListener implements XBreakpointListener {

    @Override
    public void breakpointAdded(@NotNull XBreakpoint breakpoint) {
      breakpointChanged(breakpoint);
    }

    @Override
    public void breakpointRemoved(@NotNull XBreakpoint breakpoint) {
      breakpointChanged(breakpoint);
    }

    @Override
    public void breakpointChanged(@NotNull XBreakpoint breakpointNoUse) {
      clearAllEditors();

      // XXX why do we remove/re-add all BPs here instead of managing them individually like BreakpointsUiComponent does?
      final List<Breakpoint> breakpoints = myDebuggerManager.getBreakpointManager().getBreakpoints();
      for (Breakpoint breakpoint : breakpoints) {
        if (breakpoint instanceof BreakpointWithHighlighter) {
          BreakpointWithHighlighter breakpointWithHighlighter = (BreakpointWithHighlighter) breakpoint;
          addLocationBreakpoint(breakpointWithHighlighter);
        }
      }

      repaintBreakpoints();
      ApplicationManager.getApplication().invokeLater(new Runnable() {
        @Override
        public void run() {
          List<EditorComponent> allEditorComponents = EditorComponentUtil.getAllEditorComponents(myFileEditorManager, true);
          for (EditorComponent component : allEditorComponents) {
            component.repaint();
          }
        }
      });
    }
  }
}
