/*
 * 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
 *
 *     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 org.apache.lucene.luke.app.desktop.components.fragments.search;

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.luke.app.desktop.components.ComponentOperatorRegistry;
import org.apache.lucene.luke.app.desktop.components.TableColumnInfo;
import org.apache.lucene.luke.app.desktop.components.TableModelBase;
import org.apache.lucene.luke.app.desktop.util.MessageUtils;
import org.apache.lucene.luke.app.desktop.util.TableUtils;
import org.apache.lucene.luke.models.search.QueryParserConfig;

/** Provider of the QueryParser pane (tab) */
public final class QueryParserPaneProvider implements QueryParserTabOperator {

  private final JRadioButton standardRB = new JRadioButton();

  private final JRadioButton classicRB = new JRadioButton();

  private final JComboBox<String> dfCB = new JComboBox<>();

  private final JComboBox<String> defOpCombo =
      new JComboBox<>(
          new String[] {
            QueryParserConfig.Operator.OR.name(), QueryParserConfig.Operator.AND.name()
          });

  private final JCheckBox posIncCB = new JCheckBox();

  private final JCheckBox wildCardCB = new JCheckBox();

  private final JCheckBox splitWSCB = new JCheckBox();

  private final JCheckBox genPhraseQueryCB = new JCheckBox();

  private final JCheckBox genMultiTermSynonymsPhraseQueryCB = new JCheckBox();

  private final JFormattedTextField slopFTF = new JFormattedTextField();

  private final JFormattedTextField minSimFTF = new JFormattedTextField();

  private final JFormattedTextField prefLenFTF = new JFormattedTextField();

  private final JComboBox<String> dateResCB = new JComboBox<>();

  private final JTextField locationTF = new JTextField();

  private final JTextField timezoneTF = new JTextField();

  private final JTable pointRangeQueryTable = new JTable();

  private final ListenerFunctions listeners = new ListenerFunctions();

  private final QueryParserConfig config = new QueryParserConfig.Builder().build();

  public QueryParserPaneProvider() {
    ComponentOperatorRegistry.getInstance().register(QueryParserTabOperator.class, this);
  }

  public JScrollPane get() {
    JPanel panel = new JPanel();
    panel.setOpaque(false);
    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));

    panel.add(initSelectParserPane());
    panel.add(new JSeparator(JSeparator.HORIZONTAL));
    panel.add(initParserSettingsPanel());
    panel.add(new JSeparator(JSeparator.HORIZONTAL));
    panel.add(initPhraseQuerySettingsPanel());
    panel.add(new JSeparator(JSeparator.HORIZONTAL));
    panel.add(initFuzzyQuerySettingsPanel());
    panel.add(new JSeparator(JSeparator.HORIZONTAL));
    panel.add(initDateRangeQuerySettingsPanel());
    panel.add(new JSeparator(JSeparator.HORIZONTAL));
    panel.add(initPointRangeQuerySettingsPanel());

    JScrollPane scrollPane = new JScrollPane(panel);
    scrollPane.setOpaque(false);
    scrollPane.getViewport().setOpaque(false);
    return scrollPane;
  }

  private JPanel initSelectParserPane() {
    JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEADING));
    panel.setOpaque(false);

    standardRB.setText("StandardQueryParser");
    standardRB.setSelected(true);
    standardRB.addActionListener(listeners::selectStandardQParser);
    standardRB.setOpaque(false);

    classicRB.setText("Classic QueryParser");
    classicRB.addActionListener(listeners::selectClassicQparser);
    classicRB.setOpaque(false);

    ButtonGroup group = new ButtonGroup();
    group.add(standardRB);
    group.add(classicRB);

    panel.add(standardRB);
    panel.add(classicRB);

    return panel;
  }

  private JPanel initParserSettingsPanel() {
    JPanel panel = new JPanel(new GridLayout(3, 2));
    panel.setOpaque(false);

    JPanel defField = new JPanel(new FlowLayout(FlowLayout.LEADING));
    defField.setOpaque(false);
    JLabel dfLabel = new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.df"));
    defField.add(dfLabel);
    defField.add(dfCB);
    panel.add(defField);

    JPanel defOp = new JPanel(new FlowLayout(FlowLayout.LEADING));
    defOp.setOpaque(false);
    JLabel defOpLabel = new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.dop"));
    defOp.add(defOpLabel);
    defOpCombo.setSelectedItem(config.getDefaultOperator().name());
    defOp.add(defOpCombo);
    panel.add(defOp);

    posIncCB.setText(MessageUtils.getLocalizedMessage("search_parser.checkbox.pos_incr"));
    posIncCB.setSelected(config.isEnablePositionIncrements());
    posIncCB.setOpaque(false);
    panel.add(posIncCB);

    wildCardCB.setText(MessageUtils.getLocalizedMessage("search_parser.checkbox.lead_wildcard"));
    wildCardCB.setSelected(config.isAllowLeadingWildcard());
    wildCardCB.setOpaque(false);
    panel.add(wildCardCB);

    splitWSCB.setText(MessageUtils.getLocalizedMessage("search_parser.checkbox.split_ws"));
    splitWSCB.setEnabled(config.isSplitOnWhitespace());
    splitWSCB.addActionListener(listeners::toggleSplitOnWhiteSpace);
    splitWSCB.setOpaque(false);
    panel.add(splitWSCB);

    return panel;
  }

  private JPanel initPhraseQuerySettingsPanel() {
    JPanel panel = new JPanel();
    panel.setOpaque(false);
    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));

    JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
    header.setOpaque(false);
    header.add(new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.phrase_query")));
    panel.add(header);

    JPanel genPQ = new JPanel(new FlowLayout(FlowLayout.LEADING));
    genPQ.setOpaque(false);
    genPQ.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
    genPhraseQueryCB.setText(MessageUtils.getLocalizedMessage("search_parser.checkbox.gen_pq"));
    genPhraseQueryCB.setEnabled(config.isAutoGeneratePhraseQueries());
    genPhraseQueryCB.setOpaque(false);
    genPQ.add(genPhraseQueryCB);
    panel.add(genPQ);

    JPanel genMTPQ = new JPanel(new FlowLayout(FlowLayout.LEADING));
    genMTPQ.setOpaque(false);
    genMTPQ.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
    genMultiTermSynonymsPhraseQueryCB.setText(
        MessageUtils.getLocalizedMessage("search_parser.checkbox.gen_mts"));
    genMultiTermSynonymsPhraseQueryCB.setEnabled(
        config.isAutoGenerateMultiTermSynonymsPhraseQuery());
    genMultiTermSynonymsPhraseQueryCB.setOpaque(false);
    genMTPQ.add(genMultiTermSynonymsPhraseQueryCB);
    panel.add(genMTPQ);

    JPanel slop = new JPanel(new FlowLayout(FlowLayout.LEADING));
    slop.setOpaque(false);
    slop.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
    JLabel slopLabel =
        new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.phrase_slop"));
    slop.add(slopLabel);
    slopFTF.setColumns(5);
    slopFTF.setValue(config.getPhraseSlop());
    slop.add(slopFTF);
    slop.add(new JLabel(MessageUtils.getLocalizedMessage("label.int_required")));
    panel.add(slop);

    return panel;
  }

  private JPanel initFuzzyQuerySettingsPanel() {
    JPanel panel = new JPanel();
    panel.setOpaque(false);
    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));

    JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
    header.setOpaque(false);
    header.add(new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.fuzzy_query")));
    panel.add(header);

    JPanel minSim = new JPanel(new FlowLayout(FlowLayout.LEADING));
    minSim.setOpaque(false);
    minSim.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
    JLabel minSimLabel =
        new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.fuzzy_minsim"));
    minSim.add(minSimLabel);
    minSimFTF.setColumns(5);
    minSimFTF.setValue(config.getFuzzyMinSim());
    minSim.add(minSimFTF);
    minSim.add(new JLabel(MessageUtils.getLocalizedMessage("label.float_required")));
    panel.add(minSim);

    JPanel prefLen = new JPanel(new FlowLayout(FlowLayout.LEADING));
    prefLen.setOpaque(false);
    prefLen.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
    JLabel prefLenLabel =
        new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.fuzzy_preflen"));
    prefLen.add(prefLenLabel);
    prefLenFTF.setColumns(5);
    prefLenFTF.setValue(config.getFuzzyPrefixLength());
    prefLen.add(prefLenFTF);
    prefLen.add(new JLabel(MessageUtils.getLocalizedMessage("label.int_required")));
    panel.add(prefLen);

    return panel;
  }

  private JPanel initDateRangeQuerySettingsPanel() {
    JPanel panel = new JPanel();
    panel.setOpaque(false);
    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));

    JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
    header.setOpaque(false);
    header.add(new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.daterange_query")));
    panel.add(header);

    JPanel resolution = new JPanel(new FlowLayout(FlowLayout.LEADING));
    resolution.setOpaque(false);
    resolution.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
    JLabel resLabel = new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.date_res"));
    resolution.add(resLabel);
    Arrays.stream(DateTools.Resolution.values())
        .map(DateTools.Resolution::name)
        .forEach(dateResCB::addItem);
    dateResCB.setSelectedItem(config.getDateResolution().name());
    dateResCB.setOpaque(false);
    resolution.add(dateResCB);
    panel.add(resolution);

    JPanel locale = new JPanel(new FlowLayout(FlowLayout.LEADING));
    locale.setOpaque(false);
    locale.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
    JLabel locLabel = new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.locale"));
    locale.add(locLabel);
    locationTF.setColumns(10);
    locationTF.setText(config.getLocale().toLanguageTag());
    locale.add(locationTF);
    JLabel tzLabel = new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.timezone"));
    locale.add(tzLabel);
    timezoneTF.setColumns(10);
    timezoneTF.setText(config.getTimeZone().getID());
    locale.add(timezoneTF);
    panel.add(locale);

    return panel;
  }

  private JPanel initPointRangeQuerySettingsPanel() {
    JPanel panel = new JPanel();
    panel.setOpaque(false);
    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
    panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

    JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
    header.setOpaque(false);
    header.add(
        new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.pointrange_query")));
    panel.add(header);

    JPanel headerNote = new JPanel(new FlowLayout(FlowLayout.LEADING));
    headerNote.setOpaque(false);
    headerNote.add(
        new JLabel(MessageUtils.getLocalizedMessage("search_parser.label.pointrange_hint")));
    panel.add(headerNote);

    TableUtils.setupTable(
        pointRangeQueryTable,
        ListSelectionModel.SINGLE_SELECTION,
        new PointTypesTableModel(),
        null,
        PointTypesTableModel.Column.FIELD.getColumnWidth());
    pointRangeQueryTable.setShowGrid(true);
    JScrollPane scrollPane = new JScrollPane(pointRangeQueryTable);
    panel.add(scrollPane);

    return panel;
  }

  @Override
  public void setSearchableFields(Collection<String> searchableFields) {
    dfCB.removeAllItems();
    for (String field : searchableFields) {
      dfCB.addItem(field);
    }
  }

  @Override
  public void setRangeSearchableFields(Collection<String> rangeSearchableFields) {
    pointRangeQueryTable.setModel(new PointTypesTableModel(rangeSearchableFields));
    pointRangeQueryTable.setShowGrid(true);
    String[] numTypes =
        Arrays.stream(PointTypesTableModel.NumType.values())
            .map(PointTypesTableModel.NumType::name)
            .toArray(String[]::new);
    JComboBox<String> numTypesCombo = new JComboBox<>(numTypes);
    numTypesCombo.setRenderer((list, value, index, isSelected, cellHasFocus) -> new JLabel(value));
    pointRangeQueryTable
        .getColumnModel()
        .getColumn(PointTypesTableModel.Column.TYPE.getIndex())
        .setCellEditor(new DefaultCellEditor(numTypesCombo));
    pointRangeQueryTable
        .getColumnModel()
        .getColumn(PointTypesTableModel.Column.TYPE.getIndex())
        .setCellRenderer(
            (table, value, isSelected, hasFocus, row, column) -> new JLabel((String) value));
    pointRangeQueryTable
        .getColumnModel()
        .getColumn(PointTypesTableModel.Column.FIELD.getIndex())
        .setPreferredWidth(PointTypesTableModel.Column.FIELD.getColumnWidth());
    pointRangeQueryTable.setPreferredScrollableViewportSize(
        pointRangeQueryTable.getPreferredSize());

    // set default type to Integer
    for (int i = 0; i < rangeSearchableFields.size(); i++) {
      pointRangeQueryTable.setValueAt(
          PointTypesTableModel.NumType.INT.name(), i, PointTypesTableModel.Column.TYPE.getIndex());
    }
  }

  @Override
  public QueryParserConfig getConfig() {
    int phraseSlop = (int) slopFTF.getValue();
    float fuzzyMinSimFloat = (float) minSimFTF.getValue();
    int fuzzyPrefLenInt = (int) prefLenFTF.getValue();

    Map<String, Class<? extends Number>> typeMap = new HashMap<>();
    for (int row = 0; row < pointRangeQueryTable.getModel().getRowCount(); row++) {
      String field =
          (String)
              pointRangeQueryTable.getValueAt(row, PointTypesTableModel.Column.FIELD.getIndex());
      String type =
          (String)
              pointRangeQueryTable.getValueAt(row, PointTypesTableModel.Column.TYPE.getIndex());
      switch (PointTypesTableModel.NumType.valueOf(type)) {
        case INT:
          typeMap.put(field, Integer.class);
          break;
        case LONG:
          typeMap.put(field, Long.class);
          break;
        case FLOAT:
          typeMap.put(field, Float.class);
          break;
        case DOUBLE:
          typeMap.put(field, Double.class);
          break;
        default:
          break;
      }
    }

    return new QueryParserConfig.Builder()
        .useClassicParser(classicRB.isSelected())
        .defaultOperator(QueryParserConfig.Operator.valueOf((String) defOpCombo.getSelectedItem()))
        .enablePositionIncrements(posIncCB.isSelected())
        .allowLeadingWildcard(wildCardCB.isSelected())
        .splitOnWhitespace(splitWSCB.isSelected())
        .autoGeneratePhraseQueries(genPhraseQueryCB.isSelected())
        .autoGenerateMultiTermSynonymsPhraseQuery(genMultiTermSynonymsPhraseQueryCB.isSelected())
        .phraseSlop(phraseSlop)
        .fuzzyMinSim(fuzzyMinSimFloat)
        .fuzzyPrefixLength(fuzzyPrefLenInt)
        .dateResolution(DateTools.Resolution.valueOf((String) dateResCB.getSelectedItem()))
        .locale(new Locale(locationTF.getText()))
        .timeZone(TimeZone.getTimeZone(timezoneTF.getText()))
        .typeMap(typeMap)
        .build();
  }

  @Override
  public String getDefaultField() {
    return (String) dfCB.getSelectedItem();
  }

  private class ListenerFunctions {

    void selectStandardQParser(ActionEvent e) {
      splitWSCB.setEnabled(false);
      genPhraseQueryCB.setEnabled(false);
      genMultiTermSynonymsPhraseQueryCB.setEnabled(false);
      TableUtils.setEnabled(pointRangeQueryTable, true);
    }

    void selectClassicQparser(ActionEvent e) {
      splitWSCB.setEnabled(true);
      if (splitWSCB.isSelected()) {
        genPhraseQueryCB.setEnabled(true);
      } else {
        genPhraseQueryCB.setEnabled(false);
        genPhraseQueryCB.setSelected(false);
      }
      genMultiTermSynonymsPhraseQueryCB.setEnabled(true);
      pointRangeQueryTable.setEnabled(false);
      pointRangeQueryTable.setForeground(Color.gray);
      TableUtils.setEnabled(pointRangeQueryTable, false);
    }

    void toggleSplitOnWhiteSpace(ActionEvent e) {
      if (splitWSCB.isSelected()) {
        genPhraseQueryCB.setEnabled(true);
      } else {
        genPhraseQueryCB.setEnabled(false);
        genPhraseQueryCB.setSelected(false);
      }
    }
  }

  static final class PointTypesTableModel extends TableModelBase<PointTypesTableModel.Column> {

    enum Column implements TableColumnInfo {
      FIELD("Field", 0, String.class, 300),
      TYPE("Numeric Type", 1, NumType.class, 150);

      private final String colName;
      private final int index;
      private final Class<?> type;
      private final int width;

      Column(String colName, int index, Class<?> type, int width) {
        this.colName = colName;
        this.index = index;
        this.type = type;
        this.width = width;
      }

      @Override
      public String getColName() {
        return colName;
      }

      @Override
      public int getIndex() {
        return index;
      }

      @Override
      public Class<?> getType() {
        return type;
      }

      @Override
      public int getColumnWidth() {
        return width;
      }
    }

    enum NumType {
      INT,
      LONG,
      FLOAT,
      DOUBLE
    }

    PointTypesTableModel() {
      super();
    }

    PointTypesTableModel(Collection<String> rangeSearchableFields) {
      super(rangeSearchableFields.size());
      int i = 0;
      for (String field : rangeSearchableFields) {
        data[i++][Column.FIELD.getIndex()] = field;
      }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
      return columnIndex == Column.TYPE.getIndex();
    }

    @Override
    public void setValueAt(Object value, int rowIndex, int columnIndex) {
      data[rowIndex][columnIndex] = value;
      fireTableCellUpdated(rowIndex, columnIndex);
    }

    @Override
    protected Column[] columnInfos() {
      return Column.values();
    }
  }
}
