content: /*****************************************************************
   *   Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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
   *
   *    https://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 org.apache.cayenne.modeler.editor;
  
  import org.apache.cayenne.configuration.event.QueryEvent;
  import org.apache.cayenne.map.Entity;
  import org.apache.cayenne.map.QueryDescriptor;
  import org.apache.cayenne.map.SelectQueryDescriptor;
  import org.apache.cayenne.modeler.Application;
  import org.apache.cayenne.modeler.ProjectController;
  import org.apache.cayenne.modeler.util.CayenneAction;
  import org.apache.cayenne.modeler.util.EntityTreeModel;
  import org.apache.cayenne.modeler.util.ModelerUtil;
  import org.apache.cayenne.modeler.util.MultiColumnBrowser;
  import org.apache.cayenne.modeler.util.UIUtil;
  import org.apache.cayenne.query.Ordering;
  import org.apache.cayenne.query.SortOrder;
  import org.apache.cayenne.swing.components.image.FilteredIconFactory;
  import org.apache.cayenne.util.CayenneMapEntry;
  
  import javax.swing.BorderFactory;
  import javax.swing.Icon;
  import javax.swing.JButton;
  import javax.swing.JComponent;
  import javax.swing.JLabel;
  import javax.swing.JPanel;
  import javax.swing.JScrollPane;
  import javax.swing.JSplitPane;
  import javax.swing.JTable;
  import javax.swing.JToolBar;
  import javax.swing.ListSelectionModel;
  import javax.swing.table.AbstractTableModel;
  import javax.swing.table.TableModel;
  import javax.swing.tree.TreeModel;
  import java.awt.BorderLayout;
  import java.awt.CardLayout;
  import java.awt.Dimension;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
  import java.util.prefs.Preferences;
  
  /**
   * A panel for picking SelectQuery orderings.
   * 
   */
  public class SelectQueryOrderingTab extends JPanel implements PropertyChangeListener {
  
  
      // property for split pane divider size
      private static final String SPLIT_DIVIDER_LOCATION_PROPERTY = "query.orderings.divider.location";
  
      static final Dimension BROWSER_CELL_DIM = new Dimension(150, 100);
      static final Dimension TABLE_DIM = new Dimension(460, 60);
  
      static final String PATH_HEADER = "Path";
      static final String ASCENDING_HEADER = "Ascending";
      static final String IGNORE_CASE_HEADER = "Ignore Case";
  
      static final String REAL_PANEL = "real";
      static final String PLACEHOLDER_PANEL = "placeholder";
  
      protected ProjectController mediator;
      protected SelectQueryDescriptor selectQuery;
  
      protected MultiColumnBrowser browser;
      protected JTable table;
  
      protected CardLayout cardLayout;
      protected JPanel messagePanel;
  
      public SelectQueryOrderingTab(ProjectController mediator) {
          this.mediator = mediator;
  
          initView();
          initController();
      }
  
      protected void initView() {
  
          messagePanel = new JPanel(new BorderLayout());
          cardLayout = new CardLayout();
  
          Preferences detail = Application.getInstance().getPreferencesNode(this.getClass(), "");
  
          int defLocation = Application.getFrame().getHeight() / 2;
          int location = detail != null ? detail.getInt(
                  getDividerLocationProperty(),
                  defLocation) : defLocation;
  
          //As of CAY-888 #3 main pane is now a JSplitPane. Top component is a bit larger.
          JSplitPane mainPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
          mainPanel.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
          mainPanel.setDividerLocation(location);
  
          mainPanel.setTopComponent(createEditorPanel());
          mainPanel.setBottomComponent(createSelectorPanel());
  
          setLayout(cardLayout);
          add(mainPanel, REAL_PANEL);
          add(messagePanel, PLACEHOLDER_PANEL);
      }
  
      protected void initController() {
  
          // scroll to selected row whenever a selection even occurs
          table.getSelectionModel().addListSelectionListener(e -> {
              if (!e.getValueIsAdjusting()) {
                  UIUtil.scrollToSelectedRow(table);
              }
          });
      }
  
      protected void initFromModel() {
          QueryDescriptor query = mediator.getCurrentQuery();
  
          if (query == null || !QueryDescriptor.SELECT_QUERY.equals(query.getType())) {
              processInvalidModel("Unknown query.");
              return;
          }
  
          if (!(query.getRoot() instanceof Entity)) {
              processInvalidModel("SelectQuery has no root set.");
              return;
          }
  
          this.selectQuery = (SelectQueryDescriptor) query;
          browser.setModel(createBrowserModel((Entity<?,?,?>) selectQuery.getRoot()));
          table.setModel(createTableModel());
  
          // init column sizes
          table.getColumnModel().getColumn(0).setPreferredWidth(250);
  
          cardLayout.show(this, REAL_PANEL);
      }
  
      protected void processInvalidModel(String message) {
          JLabel messageLabel = new JLabel(message, JLabel.CENTER);
  
          messagePanel.removeAll();
          messagePanel.add(messageLabel, BorderLayout.CENTER);
          cardLayout.show(this, PLACEHOLDER_PANEL);
      }
  
      protected JPanel createEditorPanel() {
  
          table = new JTable();
          table.setRowHeight(25);
          table.setRowMargin(3);
          table.setPreferredScrollableViewportSize(TABLE_DIM);
          table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  
          JPanel panel = new JPanel(new BorderLayout());
          panel.add(new JScrollPane(table), BorderLayout.CENTER);
  
          return panel;
      }
  
      protected JPanel createSelectorPanel() {
          browser = new MultiColumnBrowser();
          browser.setPreferredColumnSize(BROWSER_CELL_DIM);
          browser.setDefaultRenderer();
  
          JPanel panel = new JPanel(new BorderLayout());
          panel.add(createToolbar(), BorderLayout.NORTH);
          panel.add(new JScrollPane(
                  browser,
                  JScrollPane.VERTICAL_SCROLLBAR_NEVER,
                  JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), BorderLayout.CENTER);
  
          // setting minimal size, otherwise scrolling looks awful, because of
          // VERTICAL_SCROLLBAR_NEVER strategy
          panel.setMinimumSize(panel.getPreferredSize());
  
          return panel;
      }
  
      protected JComponent createToolbar() {
  
          JButton add = new CayenneAction.CayenneToolbarButton(null, 1);
          add.setText("Add Ordering");
          Icon addIcon = ModelerUtil.buildIcon("icon-plus.png");
          add.setIcon(addIcon);
          add.setDisabledIcon(FilteredIconFactory.createDisabledIcon(addIcon));
          add.addActionListener(e -> addOrdering());
  
          JButton remove = new CayenneAction.CayenneToolbarButton(null, 3);
          remove.setText("Remove Ordering");
          Icon removeIcon = ModelerUtil.buildIcon("icon-trash.png");
          remove.setIcon(removeIcon);
          remove.setDisabledIcon(FilteredIconFactory.createDisabledIcon(removeIcon));
          remove.addActionListener(e -> removeOrdering());
  
          JToolBar toolBar = new JToolBar();
          toolBar.setBorder(BorderFactory.createEmptyBorder());
          toolBar.setFloatable(false);
          toolBar.add(add);
          toolBar.add(remove);
          return toolBar;
      }
  
      protected TreeModel createBrowserModel(Entity<?,?,?> entity) {
          return new EntityTreeModel(entity);
      }
  
      protected TableModel createTableModel() {
          return new OrderingModel();
      }
  
      protected String getSelectedPath() {
          Object[] path = browser.getSelectionPath().getPath();
  
          // first item in the path is Entity, so we must have
          // at least two elements to constitute a valid ordering path
          if (path.length < 2) {
              return null;
          }
  
          StringBuilder buffer = new StringBuilder();
  
          // attribute or relationships
          CayenneMapEntry first = (CayenneMapEntry) path[1];
          buffer.append(first.getName());
  
          for (int i = 2; i < path.length; i++) {
              CayenneMapEntry pathEntry = (CayenneMapEntry) path[i];
              buffer.append(".").append(pathEntry.getName());
          }
  
          return buffer.toString();
      }
  
      void addOrdering() {
          String orderingPath = getSelectedPath();
          if (orderingPath == null) {
              return;
          }
  
          // check if such ordering already exists
          for (Ordering ord : selectQuery.getOrderings()) {
              if (orderingPath.equals(ord.getSortSpecString())) {
                  return;
              }
          }
  
          selectQuery.addOrdering(new Ordering(orderingPath, SortOrder.ASCENDING));
          int index = selectQuery.getOrderings().size() - 1;
  
          OrderingModel model = (OrderingModel) table.getModel();
          model.fireTableRowsInserted(index, index);
          mediator.fireQueryEvent(new QueryEvent(SelectQueryOrderingTab.this, selectQuery));
      }
  
      void removeOrdering() {
          int selection = table.getSelectedRow();
          if (selection < 0) {
              return;
          }
  
          OrderingModel model = (OrderingModel) table.getModel();
          Ordering ordering = model.getOrdering(selection);
          selectQuery.removeOrdering(ordering);
  
          model.fireTableRowsDeleted(selection, selection);
          mediator.fireQueryEvent(new QueryEvent(SelectQueryOrderingTab.this, selectQuery));
      }
  
      /**
       * A table model for the Ordering editing table.
       */
      final class OrderingModel extends AbstractTableModel {
  
          Ordering getOrdering(int row) {
              return selectQuery.getOrderings().get(row);
          }
  
          public int getColumnCount() {
              return 3;
          }
  
          public int getRowCount() {
              return (selectQuery != null) ? selectQuery.getOrderings().size() : 0;
          }
  
          public Object getValueAt(int row, int column) {
              Ordering ordering = getOrdering(row);
  
              switch (column) {
                  case 0:
                      return ordering.getSortSpecString();
                  case 1:
                      return ordering.isAscending() ? Boolean.TRUE : Boolean.FALSE;
                  case 2:
                      return ordering.isCaseInsensitive() ? Boolean.TRUE : Boolean.FALSE;
                  default:
                      throw new IndexOutOfBoundsException("Invalid column: " + column);
              }
          }
  
          @Override
          public Class<?> getColumnClass(int column) {
              switch (column) {
                  case 0:
                      return String.class;
                  case 1:
                  case 2:
                      return Boolean.class;
                  default:
                      throw new IndexOutOfBoundsException("Invalid column: " + column);
              }
          }
  
          @Override
          public String getColumnName(int column) {
              switch (column) {
                  case 0:
                      return PATH_HEADER;
                  case 1:
                      return ASCENDING_HEADER;
                  case 2:
                      return IGNORE_CASE_HEADER;
                  default:
                      throw new IndexOutOfBoundsException("Invalid column: " + column);
              }
          }
  
          @Override
          public boolean isCellEditable(int row, int column) {
              return column == 1 || column == 2;
          }
  
          @Override
          public void setValueAt(Object value, int row, int column) {
              Ordering ordering = getOrdering(row);
  
              switch (column) {
                  case 1:
                      if ((Boolean) value) {
                          ordering.setAscending();
                      } else {
                          ordering.setDescending();
                      }
                      break;
                  case 2:
                      if ((Boolean) value) {
                          ordering.setCaseInsensitive();
                      } else {
                          ordering.setCaseSensitive();
                      }
                      break;
                  default:
                      throw new IndexOutOfBoundsException("Invalid editable column: " + column);
              }
  
              mediator.fireQueryEvent(new QueryEvent(SelectQueryOrderingTab.this, selectQuery));
          }
      }
  
      /**
       * Updates split pane divider location in properties
       */
      public void propertyChange(PropertyChangeEvent evt) {
          if (JSplitPane.DIVIDER_LOCATION_PROPERTY.equals(evt.getPropertyName())) {
              int value = (Integer) evt.getNewValue();
  
              Preferences detail = Application.getInstance().getPreferencesNode(this.getClass(), "");
              detail.putInt(getDividerLocationProperty(), value);
          }
      }
  
      /**
       * Returns name of a property for divider location.
       */
      protected String getDividerLocationProperty() {
          return SPLIT_DIVIDER_LOCATION_PROPERTY;
      }
  }
  