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

import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.search.AbstractSearchPanel;
import jetbrains.mps.ide.search.SearchHistoryStorage;
import jetbrains.mps.nodeEditor.cellLayout.PunctuationUtil;
import jetbrains.mps.nodeEditor.cells.EditorCell_Collection;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.text.TextRenderUtil;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;
import jetbrains.mps.project.MPSProject;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.util.CollectionUtil;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;

import java.awt.FontMetrics;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SearchPanel extends AbstractSearchPanel {
  private EditorComponent myEditor;
  private List<SearchEntry> mySearchEntries = new ArrayList<>();
  private NodeHighlightManager myHighlightManager;
  private EditorMessageOwner myOwner;
  private SearchHistoryStorage mySearchHistory;

  public SearchPanel(EditorComponent editor) {
    super();
    myEditor = editor;
  }

  @Override
  protected SearchHistoryStorage getSearchHistory() {
    if (mySearchHistory == null) {
      final MPSProject p = MPSCommonDataKeys.MPS_PROJECT.getData(DataManager.getInstance().getDataContext(myEditor));
      if (p != null) {
        mySearchHistory = p.getComponent(SearchHistoryComponent.class);
      }
      if (mySearchHistory == null) {
        mySearchHistory = new SearchHistoryComponent();
      }
    }
    return mySearchHistory;
  }

  private Pair<List<EditorCell_Label>, String> allCellsAndContent() {
    StringBuilder sb = new StringBuilder();
    List<EditorCell_Label> cells = new ArrayList<>();
    EditorCell rootCell = myEditor.getRootCell();
    if (rootCell instanceof EditorCell_Label) {
      EditorCell_Label cell_label = (EditorCell_Label) rootCell;
      cells.add(cell_label);
      sb.append(cell_label.getRenderedText());
    }
    if (rootCell instanceof EditorCell_Collection) {
      EditorCell_Collection collection = (EditorCell_Collection) rootCell;
      List<EditorCell_Label> editorCell_labelList = CollectionUtil.filter(EditorCell_Label.class, collection.dfsCells());
      for (EditorCell_Label label : editorCell_labelList) {
        if (PunctuationUtil.hasLeftGap(label)) {
          sb.append(" ");
        }
        sb.append(label.getRenderedText());
      }
      cells.addAll(editorCell_labelList);
    }
    return new Pair<>(cells, sb.toString());
  }

  @Override
  protected boolean showExportToFindTool() {
    return true;
  }

  @Override
  protected boolean canExportToFindTool() {
    return !getMessages().isEmpty();
  }

  @Override
  public void goToPrevious() {
    if (mySearchEntries.size() == 0) {
      return;
    }
    addToHistory();
    EditorCell selectedCell = myEditor.getDeepestSelectedCell();
    int selectionStart = 0;
    boolean isEmpty = false;
    if (selectedCell instanceof EditorCell_Label) {
      EditorCell_Label labelCell = (EditorCell_Label) selectedCell;
      selectionStart = labelCell.getSelectionStart();
      isEmpty = labelCell.getText().isEmpty();
    }
    SearchEntry entryToSelect = null;
    for (ListIterator<SearchEntry> it = mySearchEntries.listIterator(mySearchEntries.size()); it.hasPrevious() && entryToSelect == null; ) {
      SearchEntry currentEntry = it.previous();
      if (CellTraversalUtil.compare(selectedCell, currentEntry.getStartLabel()) >= 0) {
        while (entryToSelect == null) {
          if (!currentEntry.getStartLabel().equals(selectedCell) || (selectionStart >= currentEntry.getFirstRange().getEndPosition() && !isEmpty)) {
            entryToSelect = currentEntry;
          }
          if (it.hasPrevious()) {
            currentEntry = it.previous();
          } else {
            break;
          }
        }
      }
    }
    if (entryToSelect == null) {
      entryToSelect = mySearchEntries.get(mySearchEntries.size() - 1);
    }
    entryToSelect.select();
  }

  @Override
  public void goToNext() {
    if (mySearchEntries.size() == 0) {
      return;
    }
    addToHistory();
    EditorCell selectedCell = myEditor.getDeepestSelectedCell();
    int selectionEnd = -1;
    boolean isEmpty = false;
    if (selectedCell instanceof EditorCell_Label) {
      EditorCell_Label labelCell = (EditorCell_Label) selectedCell;
      selectionEnd = labelCell.getSelectionEnd();
      isEmpty = labelCell.getText().isEmpty();
    }
    SearchEntry entryToSelect = null;
    for (ListIterator<SearchEntry> it = mySearchEntries.listIterator(); it.hasNext() && entryToSelect == null; ) {
      SearchEntry currentEntry = it.next();
      if (CellTraversalUtil.compare(selectedCell, currentEntry.getStartLabel()) <= 0) {
        while (entryToSelect == null) {
          if (!currentEntry.getStartLabel().equals(selectedCell) || (selectionEnd <= currentEntry.getFirstRange().getStartPosition() && !isEmpty)) {
            entryToSelect = currentEntry;
          }
          if (it.hasNext()) {
            currentEntry = it.next();
          } else {
            break;
          }
        }
      }
    }
    if (entryToSelect == null) {
      entryToSelect = mySearchEntries.get(0);
    }
    entryToSelect.select();
  }

  private void clearHighlight() {
    if (myOwner != null && myHighlightManager != null && mySearchEntries.size() <= 100) {
      myHighlightManager.clearForOwner(myOwner);
    }
  }

  @Override
  protected void search() {
    search(true);
  }

  protected void search(boolean requestFocus) {
    clearHighlight();
    mySearchEntries.clear();
    if (myText.getText().length() == 0) {
      myFindResult.setText("");
      myText.setBackground(myDefaultBackground);
      if (requestFocus) {
        myText.requestFocus();
        myEditor.repaintExternalComponent();
      }
      return;
    }
    selectCell(requestFocus);
    updateSearchReport(mySearchEntries.size());
  }

  public void update(AnActionEvent e) {

  }

  private void selectCell(boolean requestFocus) {
    Pair<List<EditorCell_Label>, String> pair = allCellsAndContent();
    final List<EditorCell_Label> cells = pair.o1;
    List<Integer> startCellPosition = new ArrayList<>();
    List<Integer> endCellPosition = new ArrayList<>();
    String content = pair.o2;
    int current = 0;
    List<EditorCell> emptyCells = new ArrayList<>();
    for (EditorCell_Label cell : cells) {
      if (cell.getRenderedText().isEmpty()) {
        emptyCells.add(cell);
      }
    }
    cells.removeAll(emptyCells);
    for (EditorCell_Label cell : cells) {
      if (current >= content.length()) {
        break;
      }
      String contentPart = content.substring(current);
      int start = contentPart.indexOf(cell.getRenderedText()) + current;
      startCellPosition.add(start);
      current = start + cell.getRenderedText().length();
      endCellPosition.add(current);
    }

    Pattern pattern = getPattern();
    if (pattern == null) {
      setErrorMessage("Incorrect regular expression");
      return;
    }
    setErrorMessage(null);
    Matcher matcher = pattern.matcher(content);
    int index = 0;
    SearchEntry searchEntryToSelect = null;
    while (matcher.find()) {
      while (index < endCellPosition.size() && endCellPosition.get(index) <= matcher.start()) {
        index++;
      }
      if (index >= startCellPosition.size()) {
        break;
      }
      if (startCellPosition.get(index) > matcher.start()) {
        // found text does not belong to any cell. Looking for next entry.
        continue;
      }

      EditorCell_Label startCell = cells.get(index);
      assert startCell != null;

      List<TextRange> textRanges = new ArrayList<>();
      for (int rangeIndex = index; rangeIndex < startCellPosition.size() && startCellPosition.get(rangeIndex) < matcher.end(); rangeIndex++) {
        int startPosition = Math.max(0, matcher.start() - startCellPosition.get(rangeIndex));
        int endPosition = Math.min(matcher.end(), endCellPosition.get(rangeIndex)) - startCellPosition.get(rangeIndex);
        EditorCell_Label nextCell = cells.get(rangeIndex);
        assert nextCell != null;
        textRanges.add(new TextRange(nextCell, startPosition, endPosition));
      }
      SearchEntry searchEntry = new SearchEntry(startCell, textRanges);
      mySearchEntries.add(searchEntry);

      //noinspection SuspiciousMethodCalls
      if (requestFocus && searchEntryToSelect == null && index >= cells.indexOf(myEditor.getSelectedCell())) {
        searchEntryToSelect = searchEntry;
      }
    }
    myOwner = new EditorMessageOwner() {
    };
    if (!mySearchEntries.isEmpty() && mySearchEntries.size() <= 100) {
      highlight(mySearchEntries);
    }
    if (searchEntryToSelect != null) {
      searchEntryToSelect.select();
    }
  }

  private void highlight(final List<SearchEntry> searchEntries) {
    ModelAccess.instance().runReadAction(() -> {
      myHighlightManager = myEditor.getHighlightManager();
      List<EditorMessage> messages = new ArrayList<>();
      Map<EditorCell_Label, List<Pair>> cellToPositions = new LinkedHashMap<>();
      for (SearchEntry searchEntry : searchEntries) {
        for (TextRange range : searchEntry.getRangesIterator()) {
          if (!cellToPositions.containsKey(range.getLabel())) {
            cellToPositions.put(range.getLabel(), new ArrayList<>());
          }
          cellToPositions.get(range.getLabel()).add(new Pair(range.getStartPosition(), range.getEndPosition()));
        }
      }
      for (EditorCell_Label cell : cellToPositions.keySet()) {
        messages.add(new SearchPanelEditorMessage(cell, cellToPositions.get(cell)));
      }
      myHighlightManager.mark(messages);
    });
  }

  private List<SearchPanelEditorMessage> getMessages() {
    final List<SearchPanelEditorMessage> searchMessages = new ArrayList<SearchPanelEditorMessage>();
    if (myEditor == null) {
      return searchMessages;
    }
    for (SimpleEditorMessage candidate : myEditor.getMessages()) {
      if (candidate instanceof SearchPanelEditorMessage) {
        searchMessages.add((SearchPanelEditorMessage) candidate);
      }
    }
    return searchMessages;
  }

  @Override
  public void exportToFindTool() {
    final List<SearchPanelEditorMessage> searchMessages = getMessages();
    final List<EditorCell_Label> editorLabels = allCellsAndContent().o1;
    Collections.sort(searchMessages, (o1, o2) -> {
      Integer i1 = editorLabels.indexOf(o1.getCell(myEditor));
      Integer i2 = editorLabels.indexOf(o2.getCell(myEditor));
      return i1.compareTo(i2);
    });
    // TODO FIXME
//    UsagesViewTool usagesViewTool = new UsagesViewTool(ProjectHelper.toIdeaProject(myEditor.getOperationContext().getProject()));
//    BaseNode baseNode = new BaseNode() {
//      public SearchResults doGetResults(SearchQuery query, @NotNull ProgressMonitor monitor) {
//        monitor.start("", 1);
//        SearchResults<SNode> searchResults = new SearchResults<SNode>();
//        for (SearchPanelEditorMessage message : searchMessages) {
//          EditorCell cell = message.getCell(myEditor);
//          if (cell == null) continue;
//          SNode node = cell.getSNode();
//          searchResults.getSearchResults().add(new SearchResult<SNode>(node, "Search Panel"));
//        }
//        monitor.done();
//        return searchResults;
//      }
//    };
//    SearchQuery searchQuery = new SearchQuery(null) {
//      @NotNull
//      public String getCaption() {
//        return "Occurrences of '" + myText.getText() + "'";
//      }
//    };
//    usagesViewTool.findUsages(baseNode, searchQuery, false, false, false, null);
  }

  boolean isTextFieldFocused() {
    return myText.isFocusOwner();
  }

  @Override
  public void deactivate() {
    setVisible(false);
    clearHighlight();
    if (!mySearchEntries.isEmpty()) {
      mySearchEntries.clear();
    }
    myFindResult.setText("");
    myText.setText("");
    myText.setBackground(myDefaultBackground);
    revalidate();
    myEditor.removeUpperComponent(this);
    myEditor.requestFocus();
  }

  @Override
  public void activate() {
    String initValue = "";
    if (myEditor.getDeepestSelectedCell() instanceof EditorCell_Label) {
      EditorCell_Label cell_label = (EditorCell_Label) myEditor.getDeepestSelectedCell();
      if (cell_label.getSelectionStart() != cell_label.getSelectionEnd()) {
        initValue = TextRenderUtil.getTextBuilderForSelectedCellsOfEditor(myEditor).getText();
      }
    }
    setInitialText(initValue);
    myEditor.addUpperComponent(this);
    super.activate();
  }

  private class SearchPanelEditorMessage extends DefaultEditorMessage {
    @NotNull
    private final List<Pair> myPositions;
    /**
     * Using cell instead of CellInfo here because SearchPanel itself depends on EditorCells
     * and re-execute search query/re-create EditorMessages on each underlying editor relayout
     */
    @NotNull
    private EditorCell_Label myCell;

    public SearchPanelEditorMessage(@NotNull EditorCell_Label cell, @NotNull List<Pair> positions) {
      super(cell.getSNode(),
          EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES).getBackgroundColor(),
          "", SearchPanel.this.myOwner);
      myCell = cell;
      myPositions = positions;
    }

    @Override
    public EditorCell getCell(EditorComponent editor) {
      return myCell;
    }

    @Override
    public boolean acceptCell(EditorCell cell, EditorComponent editor) {
      return myCell == cell;
    }

    @Override
    public void paint(Graphics g, EditorComponent editorComponent, EditorCell cell) {
      if (cell == null || !(cell instanceof EditorCell_Label)) {
        return;
      }
      EditorCell_Label editorCell = (EditorCell_Label) cell;
      for (Pair position : myPositions) {
        int startPosition = (Integer) position.o1;
        int endPosition = (Integer) position.o2;
        if (editorCell.getRenderedText().length() >= endPosition) {
          FontMetrics metrics = g.getFontMetrics();
          String text = editorCell.getRenderedText().substring(startPosition, endPosition);
          int prevStringWidth = metrics.stringWidth(editorCell.getRenderedText().
              substring(0, startPosition));
          int x = editorCell.getX() + editorCell.getLeftInset()
              + prevStringWidth;
          int y = editorCell.getY();
          int height = editorCell.getHeight();
          int width = metrics.stringWidth(text);

          g.setColor(getColor());
          // Filling smaller rectangle to not cover frames created by other nessages
          g.fillRect(x + 1, y + 1, width - 2, height - 2);
        }
      }
    }

    @Override
    public boolean sameAs(SimpleEditorMessage message) {
      return super.sameAs(message) && this.equals(message);
    }

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

    @Override
    public int getPriority() {
      return 10;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof SearchPanelEditorMessage)) {
        return false;
      }

      SearchPanelEditorMessage that = (SearchPanelEditorMessage) o;

      if (!myCell.equals(that.myCell)) {
        return false;
      }
      if (!myPositions.equals(that.myPositions)) {
        return false;
      }

      return true;
    }

    @Override
    public int hashCode() {
      int result = myPositions.hashCode();
      result = 31 * result + myCell.hashCode();
      return result;
    }
  }

  private class SearchEntry {
    private EditorCell_Label myStartLabel;
    private List<TextRange> myTextRanges;

    public SearchEntry(@NotNull EditorCell_Label startLabel, @NotNull List<TextRange> textRanges) {
      myStartLabel = startLabel;
      assert textRanges.size() > 0;
      myTextRanges = textRanges;
    }

    @NotNull
    public Iterable<TextRange> getRangesIterator() {
      return myTextRanges;
    }

    @NotNull
    public TextRange getFirstRange() {
      return myTextRanges.get(0);
    }

    @NotNull
    public EditorCell_Label getStartLabel() {
      return myStartLabel;
    }

    public void select() {
      TextRange range = getFirstRange();
      myEditor.changeSelection(range.getLabel());
      boolean canSetCaretStart = range.getLabel().isCaretPositionAllowed(range.getStartPosition());
      if (canSetCaretStart) {
        range.getLabel().setCaretPosition(range.getStartPosition());
      }
      boolean canSetCaretEnd = range.getLabel().isCaretPositionAllowed(range.getEndPosition());
      if (canSetCaretEnd) {
        range.getLabel().setCaretPosition(range.getEndPosition(), canSetCaretStart);
      }

      if (!(canSetCaretStart && canSetCaretEnd)) {
        range.getLabel().setSelectionStart(range.getStartPosition());
        range.getLabel().setSelectionEnd(range.getEndPosition());
      }

    }
  }

  private class TextRange {
    private EditorCell_Label myLabel;
    private int myStartPosition;
    private int myEndPosition;

    public TextRange(@NotNull EditorCell_Label firstLabel, int startPosition, int endPosition) {
      myLabel = firstLabel;
      myStartPosition = startPosition;
      myEndPosition = endPosition;
    }

    @NotNull
    public EditorCell_Label getLabel() {
      return myLabel;
    }

    public int getStartPosition() {
      return myStartPosition;
    }

    public int getEndPosition() {
      return myEndPosition;
    }
  }
}
