/*
 * Copyright 2000-2014 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 com.intellij.application.options.codeStyle.arrangement.match;

import com.intellij.application.options.codeStyle.arrangement.color.ArrangementColorsProvider;
import com.intellij.application.options.codeStyle.arrangement.ui.ArrangementEditorAware;
import com.intellij.application.options.codeStyle.arrangement.ui.ArrangementRepresentationAware;
import com.intellij.application.options.codeStyle.arrangement.util.ArrangementListRowDecorator;
import com.intellij.application.options.codeStyle.arrangement.util.IntObjectMap;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementEntryMatcher;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementAtomMatchCondition;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementStandardSettingsManager;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementUiComponent;
import com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.table.JBTable;
import com.intellij.util.ui.AbstractTableCellEditor;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

/**
 * @author Denis Zhdanov
 * @since 10/31/12 1:23 PM
 */
public class ArrangementMatchingRulesControl extends JBTable {
  @NotNull private static final JLabel EMPTY_RENDERER = new JLabel(ApplicationBundle.message("arrangement.text.empty.rule"));

  @NotNull protected final IntObjectMap<ArrangementListRowDecorator> myComponents   = new IntObjectMap<>();
  @NotNull private final TIntArrayList mySelectedRows = new TIntArrayList();

  @NotNull private final ArrangementMatchNodeComponentFactory myFactory;
  @NotNull protected ArrangementMatchingRuleEditor        myEditor;
  @NotNull private final RepresentationCallback           myRepresentationCallback;
  @NotNull private final MatchingRulesRendererBase        myRenderer;

  private ArrangementMatchingRulesValidator myValidator;

  private final int myMinRowHeight;
  private int myRowUnderMouse = -1;
  private int myEditorRow     = -1;
  private boolean mySkipSelectionChange;

  public ArrangementMatchingRulesControl(@NotNull ArrangementStandardSettingsManager settingsManager,
                                         @NotNull ArrangementColorsProvider colorsProvider,
                                         @NotNull RepresentationCallback callback)
  {
    super(new ArrangementMatchingRulesModel());
    myRepresentationCallback = callback;
    myFactory = new ArrangementMatchNodeComponentFactory(settingsManager, colorsProvider, this);
    myRenderer = createRender();
    setDefaultRenderer(Object.class, myRenderer);
    getColumnModel().getColumn(0).setCellEditor(new MyEditor());
    setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
    setShowColumns(false);
    setShowGrid(false);
    setBorder(IdeBorderFactory.createBorder());
    setSurrendersFocusOnKeystroke(true);
    putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);

    ArrangementAtomMatchCondition condition = new ArrangementAtomMatchCondition(StdArrangementTokens.EntryType.CLASS);
    StdArrangementMatchRule rule = new StdArrangementMatchRule(new StdArrangementEntryMatcher(condition));
    ArrangementUiComponent component = myFactory.getComponent(condition, rule, true);
    myMinRowHeight = new ArrangementListRowDecorator(component, this).getPreferredSize().height;

    myEditor = new ArrangementMatchingRuleEditor(settingsManager, colorsProvider, this);
    addMouseMotionListener(new MouseAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        onMouseMoved(e);
      }
    });
    getSelectionModel().addListSelectionListener(new ListSelectionListener() {
      @Override
      public void valueChanged(ListSelectionEvent e) {
        onSelectionChange(e);
      }
    });
    getModel().addTableModelListener(new TableModelListener() {
      @Override
      public void tableChanged(TableModelEvent e) { onTableChange(e); }
    });
  }

  protected MatchingRulesRendererBase createRender() {
    return new MatchingRulesRendererBase();
  }

  @NotNull
  protected ArrangementMatchingRulesValidator createValidator() {
    return new ArrangementMatchingRulesValidator(getModel());
  }

  private ArrangementMatchingRulesValidator getValidator() {
    if (myValidator == null) {
      myValidator = createValidator();
    }
    return myValidator;
  }

  @NotNull
  @Override
  public ArrangementMatchingRulesModel getModel() {
    return (ArrangementMatchingRulesModel)super.getModel();
  }

  @Override
  protected void processMouseEvent(MouseEvent e) {
    int id = e.getID();
    switch (id) {
      case MouseEvent.MOUSE_ENTERED: onMouseEntered(e); break;
      case MouseEvent.MOUSE_EXITED: onMouseExited(); break;
      case MouseEvent.MOUSE_RELEASED: onMouseReleased(e); break;
      case MouseEvent.MOUSE_CLICKED: onMouseClicked(e); break;
    }
    if (!e.isConsumed()) {
      super.processMouseEvent(e);
    }
  }

  private void onMouseClicked(@NotNull MouseEvent e) {
    final int count = e.getClickCount();
    if (count != 2) {
      return;
    }

    final TIntArrayList rows = getSelectedModelRows();
    if (rows.size() != 1) {
      return;
    }

    final int row = rows.get(0);
    showEditor(row);
    scrollRowToVisible(row);
  }

  private void onMouseMoved(@NotNull MouseEvent e) {
    int i = rowAtPoint(e.getPoint());
    if (i != myRowUnderMouse) {
      onMouseExited();
    }

    if (i < 0) {
      return;
    }

    if (i != myRowUnderMouse) {
      onMouseEntered(e);
    }

    ArrangementListRowDecorator decorator = myComponents.get(i);
    if (decorator == null) {
      return;
    }

    Rectangle rectangle = decorator.onMouseMove(e);
    if (rectangle != null) {
      repaintScreenBounds(rectangle);
    }
  }

  private void repaintScreenBounds(@NotNull Rectangle bounds) {
    Point location = bounds.getLocation();
    SwingUtilities.convertPointFromScreen(location, this);
    int x = location.x;
    int width = bounds.width;
    repaint(x, location.y, width, bounds.height);
  }


  private void onMouseReleased(@NotNull MouseEvent e) {
    int i = rowAtPoint(e.getPoint());
    if (i < 0) {
      return;
    }

    ArrangementListRowDecorator decorator = myComponents.get(i);
    if (decorator != null) {
      decorator.onMouseRelease(e);
    }

    if (!e.isConsumed() && myEditorRow > 0 && myEditorRow == i + 1) {
      hideEditor();
    }
  }

  private void onMouseExited() {
    if (myRowUnderMouse < 0) {
      return;
    }

    ArrangementListRowDecorator decorator = myComponents.get(myRowUnderMouse);
    if (decorator != null) {
      decorator.onMouseExited();
      repaintRows(myRowUnderMouse, myRowUnderMouse, false);
    }
    myRowUnderMouse = -1;
  }

  private void onMouseEntered(@NotNull MouseEvent e) {
    myRowUnderMouse = rowAtPoint(e.getPoint());
    ArrangementListRowDecorator decorator = myComponents.get(myRowUnderMouse);
    if (decorator != null) {
      decorator.onMouseEntered(e);
      repaintRows(myRowUnderMouse, myRowUnderMouse, false);
    }
  }

  public void runOperationIgnoreSelectionChange(@NotNull Runnable task) {
    mySkipSelectionChange = true;
    try {
      if (isEditing()) {
        getCellEditor().stopCellEditing();
      }
      task.run();
    }
    finally {
      mySkipSelectionChange = false;
      refreshEditor();
    }
  }

  private void onSelectionChange(@NotNull ListSelectionEvent e) {
    if (mySkipSelectionChange || e.getValueIsAdjusting()) {
      return;
    }
    refreshEditor();
  }

  public void removeRow(int rowIndex) {
    if (rowIndex < myEditorRow) {
      hideEditor();
    }
    getModel().removeRow(rowIndex);
  }

  public void refreshEditor() {
    ArrangementMatchingRulesModel model = getModel();
    if (myEditorRow >= model.getSize()) {
      myEditorRow = -1;
      for (int i = 0, max = model.getSize(); i < max; i++) {
        if (model.getElementAt(i) instanceof ArrangementEditorComponent) {
          myEditorRow = i;
          break;
        }
      }
    }

    if (myEditorRow < 0) {
      return;
    }

    ListSelectionModel selectionModel = getSelectionModel();
    if (selectionModel.isSelectionEmpty()) {
      hideEditor();
      return;
    }

    int selectedRow = selectionModel.getMinSelectionIndex();
    if (selectedRow != selectionModel.getMaxSelectionIndex()) {
      // More than one row is selected.
      hideEditor();
      return;
    }

    if (selectedRow != myEditorRow && selectedRow != myEditorRow - 1) {
      hideEditor();
    }
  }

  public void hideEditor() {
    if (myEditorRow < 0) {
      return;
    }
    if (isEditing()) {
      TableCellEditor editor = getCellEditor();
      if (editor != null) {
        editor.stopCellEditing();
      }
    }

    mySkipSelectionChange = true;
    try {
      ArrangementMatchingRulesModel model = getModel();
      model.removeRow(myEditorRow);
      if (myEditorRow > 0 && model.getElementAt(myEditorRow - 1) instanceof EmptyArrangementRuleComponent) {
        model.removeRow(myEditorRow - 1);
      }
    }
    finally {
      mySkipSelectionChange = false;
    }
    myEditorRow = -1;
  }

  private void onTableChange(@NotNull TableModelEvent e) {
    final int signum;
    switch (e.getType()) {
      case TableModelEvent.INSERT:
        signum = 1;
        break;
      case TableModelEvent.DELETE:
        signum = -1;
        for (int i = e.getLastRow(); i >= e.getFirstRow(); i--) {
          myComponents.remove(i);
        }
        break;
      default:
        return;
    }
    int shift = Math.abs(e.getFirstRow() - e.getLastRow() + 1) * signum;
    myComponents.shiftKeys(e.getFirstRow(), shift);
    if (myRowUnderMouse >= e.getFirstRow()) {
      myRowUnderMouse = -1;
    }
    if (getModel().getSize() > 0) {
      repaintRows(0, getModel().getSize() - 1, false);
    }
  }

  public void showEditor(int rowToEdit) {
    showEditor(myEditor, rowToEdit);
  }

  public void scrollRowToVisible(int row) {
    final Rectangle rect = getCellRect(row, 0, false);
    if (row != getEditingRow() - 1) {
      scrollRectToVisible(rect);
    }
    else {
      final Rectangle editorRect = getCellRect(row + 1, 0, false);
      if(!rect.isEmpty() && !editorRect.isEmpty()) {
        final int height = (int)(rect.getHeight() + editorRect.getHeight());
        final Rectangle visibleRect = new Rectangle((int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), height);
        scrollRectToVisible(visibleRect);
      }
    }
  }

  public void showEditor(@NotNull ArrangementMatchingRuleEditor editor, int rowToEdit) {
    if (myEditorRow == rowToEdit + 1) {
      return;
    }
    if (myEditorRow >= 0) {
      if (myEditorRow < rowToEdit) {
        rowToEdit--;
      }
      hideEditor();
    }
    myEditorRow = rowToEdit + 1;
    ArrangementEditorComponent editorComponent = new ArrangementEditorComponent(this, myEditorRow, editor);
    int width = getBounds().width;
    JScrollPane scrollPane = JBScrollPane.findScrollPane(getParent());
    if (scrollPane != null) {
      width -= scrollPane.getVerticalScrollBar().getWidth();
    }
    editorComponent.applyAvailableWidth(width);
    editor.reset(rowToEdit);
    mySkipSelectionChange = true;
    try {
      getModel().insertRow(myEditorRow, new Object[]{editorComponent});
    }
    finally {
      mySkipSelectionChange = false;
    }

    Rectangle bounds = getRowsBounds(rowToEdit, myEditorRow);
    if (bounds != null) {
      myRepresentationCallback.ensureVisible(bounds);
    }

    // We can't just subscribe to the model modification events and update cached renderers automatically because we need to use
    // the cached renderer on atom condition removal (via click on 'close' button). The model is modified immediately then but
    // corresponding cached renderer is used for animation.
    editorComponent.expand();
    repaintRows(rowToEdit, getModel().getRowCount() - 1, false);
    editCellAt(myEditorRow, 0);
  }

  public void repaintRows(int first, int last, boolean rowStructureChanged) {
    for (int i = first; i <= last; i++) {
      if (rowStructureChanged) {
        myComponents.remove(i);
      }
      else {
        setRowHeight(i, myRenderer.getRendererComponent(i).getPreferredSize().height);
      }
    }
    getModel().fireTableRowsUpdated(first, last);
  }

  private Rectangle getRowsBounds(int first, int last) {
    Rectangle firstRect = getCellRect(first, 0, true);
    Rectangle lastRect = getCellRect(last, 0, true);
    return new Rectangle(firstRect.x, firstRect.y, lastRect.width, lastRect.y + lastRect.height - firstRect.y);
  }

  /**
   * @return    selected model rows sorted in descending order
   */
  @NotNull
  public TIntArrayList getSelectedModelRows() {
    mySelectedRows.clear();
    int min = selectionModel.getMinSelectionIndex();
    if (min >= 0) {
      for (int i = selectionModel.getMaxSelectionIndex();  i >= min; i--) {
        if ((myEditorRow >= 0 && i == myEditorRow - 1)
            || (i != myEditorRow && selectionModel.isSelectedIndex(i)))
        {
          mySelectedRows.add(i);
        }
      }
    }
    else if (myEditorRow > 0) {
      mySelectedRows.add(myEditorRow - 1);
    }
    return mySelectedRows;
  }

  public int getRowByRenderer(@NotNull ArrangementListRowDecorator renderer) {
    for (int i = 0, max = getModel().getSize(); i < max; i++) {
      if (myComponents.get(i) == renderer) {
        return i;
      }
    }
    return -1;
  }

  public int getEmptyRowHeight() {
    return myMinRowHeight;
  }

  @NotNull
  private JComponent adjustHeight(@NotNull JComponent component, int row) {
    int height = component.getPreferredSize().height;
    if (height < myMinRowHeight) {
      height = myMinRowHeight;
    }
    setRowHeight(row, height);
    return component;
  }

  protected class MatchingRulesRendererBase implements TableCellRenderer {

    public Component getRendererComponent(int row) {
      return getTableCellRendererComponent(ArrangementMatchingRulesControl.this, getModel().getElementAt(row), false, false, row, 0);
    }

    public boolean allowModifications(StdArrangementMatchRule rule) {
      return true;
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
      if (isEditing() && getEditingRow() == row) {
        return EMPTY_RENDERER;
      }
      if (value instanceof ArrangementRepresentationAware) {
        return adjustHeight(((ArrangementRepresentationAware)value).getComponent(), row);
      }

      ArrangementListRowDecorator component = myComponents.get(row);
      if (component == null) {
        if (!(value instanceof StdArrangementMatchRule)) {
          return EMPTY_RENDERER;
        }
        StdArrangementMatchRule rule = (StdArrangementMatchRule)value;
        final boolean allowModifications = allowModifications(rule);
        ArrangementUiComponent ruleComponent = myFactory.getComponent(rule.getMatcher().getCondition(), rule, allowModifications);
        component = new ArrangementListRowDecorator(ruleComponent, ArrangementMatchingRulesControl.this);
        component.setError(getValidator().validate(row));
        myComponents.set(row, component);
      }

      component.setUnderMouse(myRowUnderMouse == row);
      component.setRowIndex((myEditorRow >= 0 && row > myEditorRow) ? row : row + 1);
      component.setSelected(getSelectionModel().isSelectedIndex(row) || (myEditorRow >= 0 && row == myEditorRow - 1));
      component.setBeingEdited(myEditorRow >= 0 && myEditorRow == row + 1);
      boolean showSortIcon = value instanceof StdArrangementMatchRule
                             && StdArrangementTokens.Order.BY_NAME.equals(((StdArrangementMatchRule)value).getOrderType());
      component.setShowSortIcon(showSortIcon);
      return component.getUiComponent();
    }
  }

  @SuppressWarnings("ConstantConditions")
  private class MyEditor extends AbstractTableCellEditor {

    private int myRow;

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
      if (value instanceof ArrangementEditorAware) {
        myRow = row;
        return ((ArrangementEditorAware)value).getComponent();
      }
      return null;
    }

    @Override
    public Object getCellEditorValue() {
      return myRow < getModel().getSize() ? getModel().getElementAt(myRow) : null;
    }
  }

  public interface RepresentationCallback {
    void ensureVisible(@NotNull Rectangle r);
  }
}
