/*
 * Copyright 2003-2011 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.selection;

import jetbrains.mps.editor.runtime.style.StyleAttributesUtil;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.FocusPolicyUtil;
import jetbrains.mps.nodeEditor.cells.EditorCell_Error;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.openapi.editor.cells.EditorCell_Label;
import jetbrains.mps.openapi.editor.selection.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionInfo;
import jetbrains.mps.openapi.editor.selection.SelectionListener;
import jetbrains.mps.openapi.editor.selection.SelectionManager;
import jetbrains.mps.openapi.editor.selection.SelectionStoreException;
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.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;

public class SelectionManagerImpl implements SelectionManager {
  private static final Logger LOG = LogManager.getLogger(SelectionManagerImpl.class);

  @NotNull
  private EditorComponent myEditorComponent;
  private Deque<Selection> mySelectionStack = new LinkedList<Selection>();
  private List<SelectionListener> mySelectionListeners = new LinkedList<SelectionListener>();

  public SelectionManagerImpl(@NotNull EditorComponent editorComponent) {
    myEditorComponent = editorComponent;
  }

  public void clearSelection() {
    if (mySelectionStack.isEmpty()) {
      return;
    }
    Selection oldSelection = mySelectionStack.getLast();
    mySelectionStack.clear();
    doChangeSelection(oldSelection, null);
  }

  public Selection getSelection() {
    return mySelectionStack.isEmpty() ? null : mySelectionStack.getLast();
  }

  public Selection getDeepestSelection() {
    return mySelectionStack.isEmpty() ? null : mySelectionStack.getFirst();
  }

  @Override
  public Selection createSelection(EditorCell editorCell) {
    Selection selection;
    if (editorCell instanceof EditorCell_Label) {
      selection = new EditorCellLabelSelection((EditorCell_Label) editorCell);
    } else if (editorCell != null) {
      selection = new EditorCellSelection(editorCell);
    } else {
      selection = null;
    }
    return selection;
  }

  @Override
  public Selection createRangeSelection(SNode firstNode, SNode lastNode) {
    return new NodeRangeSelection(myEditorComponent, firstNode, lastNode);
  }

  @Override
  public void setSelection(EditorCell editorCell) {
    setSelection(createSelection(editorCell));
  }

  @Override
  public void setSelection(EditorCell_Label label, int caretPosition) {
    label.setCaretPosition(caretPosition);
    setSelection(new EditorCellLabelSelection(label));
  }

  @Override
  public void setSelection(EditorCell_Label label, int caretPosition, int selectionStart, int selectionEnd) {
    label.setCaretPosition(caretPosition);
    label.setSelectionStart(selectionStart);
    label.setSelectionEnd(selectionEnd);
    setSelection(new EditorCellLabelSelection(label));
  }

  @Override
  public void setSelection(Selection selection) {
    Selection oldSelection = getSelection();
    if (!mySelectionStack.isEmpty()) {
      mySelectionStack.clear();
    }
    if (selection != null) {
      mySelectionStack.addLast(selection);
    }
    doChangeSelection(oldSelection, selection);
  }

  @Override
  public void pushSelection(@NotNull Selection selection) {
    Selection oldSelection = getSelection();
    mySelectionStack.addLast(selection);
    doChangeSelection(oldSelection, selection);
  }

  @Override
  public Selection popSelection() {
    if (mySelectionStack.isEmpty()) {
      return null;
    }
    Selection oldSelection = mySelectionStack.removeLast();
    doChangeSelection(oldSelection, getSelection());
    return oldSelection;
  }

  @Override
  public Iterable<Selection> getSelectionStackIterable() {
    return new ArrayList<Selection>(mySelectionStack);
  }

  @Override
  public List<SelectionInfo> getSelectionInfoStack() {
    List<SelectionInfo> result = new ArrayList<SelectionInfo>();
    try {
      for (Selection nextSelection : mySelectionStack) {
        result.add(nextSelection.getSelectionInfo());
      }
    } catch (SelectionStoreException e) {
      LOG.error(null, e);
      // unable to store selection - cleaning selection stack
      result.clear();
    }
    return result;
  }

  @Override
  public void setSelectionInfoStack(@NotNull List<SelectionInfo> selectionStack) {
    List<Selection> newSelectionStack = new ArrayList<Selection>();
    for (SelectionInfo nextSelectionInfo : selectionStack) {
      Selection selection = nextSelectionInfo.createSelection(myEditorComponent);
      if (selection != null) {
        // restoring only those selection stack elements available in the current editor, skipping those are not available now
        newSelectionStack.add(selection);
      }
    }
    if (isSameSelectionStack(newSelectionStack)) {
      return;
    }

    Selection oldSelection = getSelection();
    mySelectionStack.clear();
    mySelectionStack.addAll(newSelectionStack);
    doChangeSelection(oldSelection, getSelection());
  }

  private boolean isSameSelectionStack(List<Selection> newSelectionStack) {
    if (newSelectionStack.size() != mySelectionStack.size()) {
      return false;
    }
    int index = 0;
    for (Selection oldSelection : mySelectionStack) {
      Selection newSelection = newSelectionStack.get(index++);
      if (!oldSelection.isSame(newSelection)) {
        return false;
      }
    }
    return true;
  }

  @Override
  public int getSelectionStackSize() {
    return mySelectionStack.size();
  }

  @Override
  public void addSelectionListener(@NotNull SelectionListener listener) {
    assert !mySelectionListeners.contains(listener);
    mySelectionListeners.add(listener);
  }

  @Override
  public void removeSelectionListener(@NotNull SelectionListener listener) {
    mySelectionListeners.remove(listener);
  }

  private void doChangeSelection(Selection oldSelection, Selection newSelection) {
    boolean isSameSelection = oldSelection == newSelection;
    if (oldSelection != null && !isSameSelection) {
      oldSelection.deactivate();
    }
    if (newSelection != null && !isSameSelection) {
      newSelection.activate();
    }
    for (SelectionListener nextListener : mySelectionListeners) {
      try {
        nextListener.selectionChanged(myEditorComponent, oldSelection, newSelection);
      } catch (Exception e) {
        LOG.error(null, e);
      }
    }
  }

  public void dispose() {
    if (!mySelectionStack.isEmpty()) {
      mySelectionStack.getLast().deactivate();
    }
    mySelectionStack.clear();
  }

  @Override
  public void setSelection(SNode node) {
    setSelection(myEditorComponent.findNodeCell(node));
  }

  @Override
  public void setSelection(SNode node, @NotNull String cellId) {
    EditorCell cell = findCell(node, cellId);
    if (cell == null) {
      clearSelection();
    } else {
      setSelection(cell);
    }
  }

  @Override
  public void setSelection(SNode node, @NotNull String cellId, int caretPosition) {
    EditorCell cell = findCell(node, cellId);
    if (cell instanceof EditorCell_Collection && (caretPosition == 0 || caretPosition == -1)) {
      cell = findChildCell(cell, caretPosition == 0 ? SelectionManager.FIRST_CELL : SelectionManager.LAST_CELL);
    }
    if (cell instanceof EditorCell_Label) {
      EditorCell_Label labelCell = (EditorCell_Label) cell;
      boolean isFirstPositionRequested = caretPosition == 0;
      boolean isLastPositionRequested = caretPosition == -1 || (caretPosition != 0 && caretPosition == labelCell.getText().length());
      EditorCell_Label refinedCell = refineUsingCursorPositioningRules(labelCell, cellId, isFirstPositionRequested, isLastPositionRequested);
      if (refinedCell != null) {
        if (refinedCell != labelCell) {
          // first -> last & last -> first if prev. or next cell was selected
          if (isFirstPositionRequested) {
            isLastPositionRequested = true;
          } else if (isLastPositionRequested) {
            isLastPositionRequested = false;
            caretPosition = 0;
          }
        }
        if (isLastPositionRequested) {
          caretPosition = refinedCell.getText().length();
        }
        setSelection(refinedCell, caretPosition);
        return;
      }
    }
    clearSelection();
  }

  private EditorCell_Label refineUsingCursorPositioningRules(EditorCell_Label labelCell, String cellId, boolean isFirstPositionRequested,
      boolean isLastPositionRequested) {
    if (isFirstPositionRequested && !StyleAttributesUtil.isFirstPositionAllowed(labelCell.getStyle())) {
      return getNextApplicableCell(labelCell, false, SelectionManager.FIRST_EDITABLE_CELL.equals(cellId));
    }
    if (isLastPositionRequested && !StyleAttributesUtil.isLastPositionAllowed(labelCell.getStyle())) {
      return getNextApplicableCell(labelCell, true, SelectionManager.LAST_EDITABLE_CELL.equals(cellId));
    }
    return labelCell;
  }

  private EditorCell_Label getNextApplicableCell(EditorCell_Label startCell, boolean forwardDirection, boolean findEditableCell) {
    for (EditorCell nextCell : CellTraversalUtil.iterateTree(null, startCell, forwardDirection).skipStart()) {
      if (nextCell instanceof EditorCell_Label) {
        EditorCell_Label labelCell = (EditorCell_Label) nextCell;
        if (findEditableCell ? labelCell.isSelectable() && labelCell.isEditable() : labelCell.isSelectable()) {
          return labelCell;
        }
      }
    }
    return null;
  }

  @Override
  public void setSelection(SNode node, @NotNull String cellId, int selectionStart, int selectionEnd) {
    EditorCell cell = findCell(node, cellId);
    if (cell instanceof EditorCell_Label) {
      EditorCell_Label label = (EditorCell_Label) cell;
      if (selectionStart == -1) {
        selectionStart = label.getText().length();
      }
      if (selectionEnd == -1) {
        selectionEnd = label.getText().length();
      }

      setSelection(label, selectionEnd, selectionStart, selectionEnd);
    } else {
      clearSelection();
    }
  }

  /**
   * looking for the cell with specified id for the specified node taking into account
   * cell selector constants: SelectionManager.FIRST_CELL/SelectionManager.LAST_CELL/..
   */
  private EditorCell findCell(SNode node, String cellId) {
    EditorCell nodeCell = myEditorComponent.findNodeCell(node);
    if (nodeCell == null) {
      return null;
    }
    if (FOCUS_POLICY_CELL.equals(cellId)) {
      return FocusPolicyUtil.findFocusedCell(nodeCell);
    }
    if (isSpecifiedById(nodeCell, cellId)) {
      return nodeCell;
    }
    return findChildCell(nodeCell, cellId);
  }

  private EditorCell findChildCell(EditorCell nodeCell, String cellId) {
    boolean useForwardIterator = shouldUseForwardIterator(cellId);
    boolean ignoreChildNodes = shouldIgnoreChildNodes(cellId);
    for (EditorCell cell : CellTraversalUtil.iterateTree(nodeCell, nodeCell, useForwardIterator).skipStart()) {
      if (ignoreChildNodes && cell.getSNode() != nodeCell.getSNode()) {
        continue;
      }
      if (isSpecifiedById(cell, cellId)) {
        return cell;
      }
    }
    return null;
  }

  private boolean shouldUseForwardIterator(String cellId) {
    String[] selectorsShouldUseBackwardIterator = new String[]{LAST_CELL, LAST_EDITABLE_CELL,LAST_ERROR_CELL};
    return !Arrays.stream(selectorsShouldUseBackwardIterator).anyMatch(s -> s.equals(cellId));
  }

  private boolean shouldIgnoreChildNodes(String cellId) {
    String[] selectorsShouldNotIgnoreChildNodes = new String[]{FIRST_CELL, LAST_CELL, FIRST_EDITABLE_CELL, LAST_EDITABLE_CELL, FIRST_ERROR_CELL, LAST_ERROR_CELL};
    return !Arrays.stream(selectorsShouldNotIgnoreChildNodes).anyMatch(s -> s.equals(cellId));
  }

  private boolean isSpecifiedById(EditorCell cell, String requestedCellId) {
    String thisCellId = cell.getCellId();
    // supporting this hidden notation for selecting property cells.
    // Now property cellIDs are prefixed with editor component name.
    if (requestedCellId.startsWith("*")) {
      if (thisCellId != null && thisCellId.contains(requestedCellId.substring(1))) {
        return true;
      }
    }

    if (requestedCellId.equals(thisCellId)) {
      return true;
    }

    // processing cell selector constants
    boolean selectable = SelectionManager.FIRST_CELL.equals(requestedCellId) || SelectionManager.LAST_CELL.equals(requestedCellId);
    boolean editable = SelectionManager.FIRST_EDITABLE_CELL.equals(requestedCellId) || SelectionManager.LAST_EDITABLE_CELL.equals(requestedCellId);
    boolean error = SelectionManager.FIRST_ERROR_CELL.equals(requestedCellId) || SelectionManager.LAST_ERROR_CELL.equals(requestedCellId);

    if (cell instanceof EditorCell_Collection) {
      return false;
    }
    if (selectable) {
      return cell.isSelectable();
    } else if (editable) {
      return cell.isSelectable() && cell instanceof EditorCell_Label && ((EditorCell_Label) cell).isEditable();
    } else if (error) {
      return cell instanceof EditorCell_Error || cell.isErrorState();
    }
    return false;
  }
}
