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

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListModel;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.ListSelectionModel;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.luke.app.DirectoryHandler;
import org.apache.lucene.luke.app.DirectoryObserver;
import org.apache.lucene.luke.app.IndexHandler;
import org.apache.lucene.luke.app.IndexObserver;
import org.apache.lucene.luke.app.LukeState;
import org.apache.lucene.luke.app.desktop.util.MessageUtils;
import org.apache.lucene.luke.app.desktop.util.TableUtils;
import org.apache.lucene.luke.models.commits.Commit;
import org.apache.lucene.luke.models.commits.Commits;
import org.apache.lucene.luke.models.commits.CommitsFactory;
import org.apache.lucene.luke.models.commits.File;
import org.apache.lucene.luke.models.commits.Segment;

/** Provider of the Commits panel */
public final class CommitsPanelProvider {

  private final CommitsFactory commitsFactory = new CommitsFactory();

  private final JComboBox<Long> commitGenCombo = new JComboBox<>();

  private final JLabel deletedLbl = new JLabel();

  private final JLabel segCntLbl = new JLabel();

  private final JTextArea userDataTA = new JTextArea();

  private final JTable filesTable = new JTable();

  private final JTable segmentsTable = new JTable();

  private final JRadioButton diagRB = new JRadioButton();

  private final JRadioButton attrRB = new JRadioButton();

  private final JRadioButton codecRB = new JRadioButton();

  private final ButtonGroup rbGroup = new ButtonGroup();

  private final JList<String> segDetailList = new JList<>();

  private ListenerFunctions listeners = new ListenerFunctions();

  private Commits commitsModel;

  public CommitsPanelProvider() {
    IndexHandler.getInstance().addObserver(new Observer());
    DirectoryHandler.getInstance().addObserver(new Observer());
  }

  public JPanel get() {
    JPanel panel = new JPanel(new GridLayout(1, 1));
    panel.setOpaque(false);
    panel.setBorder(BorderFactory.createLineBorder(Color.gray));

    JSplitPane splitPane =
        new JSplitPane(JSplitPane.VERTICAL_SPLIT, initUpperPanel(), initLowerPanel());
    splitPane.setOpaque(false);
    splitPane.setBorder(BorderFactory.createEmptyBorder());
    splitPane.setDividerLocation(120);
    panel.add(splitPane);

    return panel;
  }

  private JPanel initUpperPanel() {
    JPanel panel = new JPanel(new BorderLayout(20, 0));
    panel.setOpaque(false);
    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));

    JPanel left = new JPanel(new FlowLayout(FlowLayout.LEADING));
    left.setOpaque(false);
    left.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.select_gen")));
    commitGenCombo.addActionListener(listeners::selectGeneration);
    left.add(commitGenCombo);
    panel.add(left, BorderLayout.LINE_START);

    JPanel right = new JPanel(new GridBagLayout());
    right.setOpaque(false);
    GridBagConstraints c1 = new GridBagConstraints();
    c1.ipadx = 5;
    c1.ipady = 5;

    c1.gridx = 0;
    c1.gridy = 0;
    c1.weightx = 0.2;
    c1.anchor = GridBagConstraints.EAST;
    right.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.deleted")), c1);

    c1.gridx = 1;
    c1.gridy = 0;
    c1.weightx = 0.5;
    c1.anchor = GridBagConstraints.WEST;
    right.add(deletedLbl, c1);

    c1.gridx = 0;
    c1.gridy = 1;
    c1.weightx = 0.2;
    c1.anchor = GridBagConstraints.EAST;
    right.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.segcount")), c1);

    c1.gridx = 1;
    c1.gridy = 1;
    c1.weightx = 0.5;
    c1.anchor = GridBagConstraints.WEST;
    right.add(segCntLbl, c1);

    c1.gridx = 0;
    c1.gridy = 2;
    c1.weightx = 0.2;
    c1.anchor = GridBagConstraints.EAST;
    right.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.userdata")), c1);

    userDataTA.setRows(3);
    userDataTA.setColumns(30);
    userDataTA.setLineWrap(true);
    userDataTA.setWrapStyleWord(true);
    userDataTA.setEditable(false);
    JScrollPane userDataScroll =
        new JScrollPane(
            userDataTA,
            JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
            JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    c1.gridx = 1;
    c1.gridy = 2;
    c1.weightx = 0.5;
    c1.anchor = GridBagConstraints.WEST;
    right.add(userDataScroll, c1);

    panel.add(right, BorderLayout.CENTER);

    return panel;
  }

  private JPanel initLowerPanel() {
    JPanel panel = new JPanel(new GridLayout(1, 1));
    panel.setOpaque(false);
    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));

    JSplitPane splitPane =
        new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, initFilesPanel(), initSegmentsPanel());
    splitPane.setOpaque(false);
    splitPane.setBorder(BorderFactory.createEmptyBorder());
    splitPane.setDividerLocation(300);
    panel.add(splitPane);
    return panel;
  }

  private JPanel initFilesPanel() {
    JPanel panel = new JPanel(new BorderLayout());
    panel.setOpaque(false);
    panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));

    JPanel header = new JPanel(new FlowLayout(FlowLayout.LEADING));
    header.setOpaque(false);
    header.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.files")));
    panel.add(header, BorderLayout.PAGE_START);

    TableUtils.setupTable(
        filesTable,
        ListSelectionModel.SINGLE_SELECTION,
        new FilesTableModel(),
        null,
        FilesTableModel.Column.FILENAME.getColumnWidth());
    panel.add(new JScrollPane(filesTable), BorderLayout.CENTER);

    return panel;
  }

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

    JPanel segments = new JPanel(new FlowLayout(FlowLayout.LEADING));
    segments.setOpaque(false);
    segments.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.segments")));
    panel.add(segments);

    TableUtils.setupTable(
        segmentsTable,
        ListSelectionModel.SINGLE_SELECTION,
        new SegmentsTableModel(),
        new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            listeners.showSegmentDetails(e);
          }
        },
        SegmentsTableModel.Column.NAME.getColumnWidth(),
        SegmentsTableModel.Column.MAXDOCS.getColumnWidth(),
        SegmentsTableModel.Column.DELS.getColumnWidth(),
        SegmentsTableModel.Column.DELGEN.getColumnWidth(),
        SegmentsTableModel.Column.VERSION.getColumnWidth(),
        SegmentsTableModel.Column.CODEC.getColumnWidth());
    panel.add(new JScrollPane(segmentsTable));

    JPanel segDetails = new JPanel(new FlowLayout(FlowLayout.LEADING));
    segDetails.setOpaque(false);
    segDetails.add(new JLabel(MessageUtils.getLocalizedMessage("commits.label.segdetails")));
    panel.add(segDetails);

    JPanel buttons = new JPanel(new FlowLayout(FlowLayout.LEADING));
    buttons.setOpaque(false);

    diagRB.setText("Diagnostics");
    diagRB.setActionCommand(ActionCommand.DIAGNOSTICS.name());
    diagRB.setSelected(true);
    diagRB.setEnabled(false);
    diagRB.setOpaque(false);
    diagRB.addMouseListener(
        new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            listeners.showSegmentDetails(e);
          }
        });
    buttons.add(diagRB);

    attrRB.setText("Attributes");
    attrRB.setActionCommand(ActionCommand.ATTRIBUTES.name());
    attrRB.setSelected(false);
    attrRB.setEnabled(false);
    attrRB.setOpaque(false);
    attrRB.addMouseListener(
        new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            listeners.showSegmentDetails(e);
          }
        });
    buttons.add(attrRB);

    codecRB.setText("Codec");
    codecRB.setActionCommand(ActionCommand.CODEC.name());
    codecRB.setSelected(false);
    codecRB.setEnabled(false);
    codecRB.setOpaque(false);
    codecRB.addMouseListener(
        new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            listeners.showSegmentDetails(e);
          }
        });
    buttons.add(codecRB);

    rbGroup.add(diagRB);
    rbGroup.add(attrRB);
    rbGroup.add(codecRB);

    panel.add(buttons);

    segDetailList.setVisibleRowCount(10);
    panel.add(new JScrollPane(segDetailList));

    return panel;
  }

  // control methods

  private void selectGeneration() {
    diagRB.setEnabled(false);
    attrRB.setEnabled(false);
    codecRB.setEnabled(false);
    segDetailList.setModel(new DefaultListModel<>());

    long commitGen = (long) commitGenCombo.getSelectedItem();
    commitsModel
        .getCommit(commitGen)
        .ifPresent(
            commit -> {
              deletedLbl.setText(String.valueOf(commit.isDeleted()));
              segCntLbl.setText(String.valueOf(commit.getSegCount()));
              userDataTA.setText(commit.getUserData());
            });

    filesTable.setModel(new FilesTableModel(commitsModel.getFiles(commitGen)));
    filesTable.setShowGrid(true);
    filesTable
        .getColumnModel()
        .getColumn(FilesTableModel.Column.FILENAME.getIndex())
        .setPreferredWidth(FilesTableModel.Column.FILENAME.getColumnWidth());

    segmentsTable.setModel(new SegmentsTableModel(commitsModel.getSegments(commitGen)));
    segmentsTable.setShowGrid(true);
    segmentsTable
        .getColumnModel()
        .getColumn(SegmentsTableModel.Column.NAME.getIndex())
        .setPreferredWidth(SegmentsTableModel.Column.NAME.getColumnWidth());
    segmentsTable
        .getColumnModel()
        .getColumn(SegmentsTableModel.Column.MAXDOCS.getIndex())
        .setPreferredWidth(SegmentsTableModel.Column.MAXDOCS.getColumnWidth());
    segmentsTable
        .getColumnModel()
        .getColumn(SegmentsTableModel.Column.DELS.getIndex())
        .setPreferredWidth(SegmentsTableModel.Column.DELS.getColumnWidth());
    segmentsTable
        .getColumnModel()
        .getColumn(SegmentsTableModel.Column.DELGEN.getIndex())
        .setPreferredWidth(SegmentsTableModel.Column.DELGEN.getColumnWidth());
    segmentsTable
        .getColumnModel()
        .getColumn(SegmentsTableModel.Column.VERSION.getIndex())
        .setPreferredWidth(SegmentsTableModel.Column.VERSION.getColumnWidth());
    segmentsTable
        .getColumnModel()
        .getColumn(SegmentsTableModel.Column.CODEC.getIndex())
        .setPreferredWidth(SegmentsTableModel.Column.CODEC.getColumnWidth());
  }

  private void showSegmentDetails() {
    int selectedRow = segmentsTable.getSelectedRow();
    if (commitGenCombo.getSelectedItem() == null
        || selectedRow < 0
        || selectedRow >= segmentsTable.getRowCount()) {
      return;
    }

    diagRB.setEnabled(true);
    attrRB.setEnabled(true);
    codecRB.setEnabled(true);

    long commitGen = (long) commitGenCombo.getSelectedItem();
    String segName =
        (String) segmentsTable.getValueAt(selectedRow, SegmentsTableModel.Column.NAME.getIndex());
    ActionCommand command = ActionCommand.valueOf(rbGroup.getSelection().getActionCommand());

    final DefaultListModel<String> detailsModel = new DefaultListModel<>();
    switch (command) {
      case DIAGNOSTICS:
        commitsModel.getSegmentDiagnostics(commitGen, segName).entrySet().stream()
            .map(entry -> entry.getKey() + " = " + entry.getValue())
            .forEach(detailsModel::addElement);
        break;
      case ATTRIBUTES:
        commitsModel.getSegmentAttributes(commitGen, segName).entrySet().stream()
            .map(entry -> entry.getKey() + " = " + entry.getValue())
            .forEach(detailsModel::addElement);
        break;
      case CODEC:
        commitsModel
            .getSegmentCodec(commitGen, segName)
            .ifPresent(
                codec -> {
                  Map<String, String> map = new HashMap<>();
                  map.put("Codec name", codec.getName());
                  map.put("Codec class name", codec.getClass().getName());
                  map.put("Compound format", codec.compoundFormat().getClass().getName());
                  map.put("DocValues format", codec.docValuesFormat().getClass().getName());
                  map.put("FieldInfos format", codec.fieldInfosFormat().getClass().getName());
                  map.put("LiveDocs format", codec.liveDocsFormat().getClass().getName());
                  map.put("Norms format", codec.normsFormat().getClass().getName());
                  map.put("Points format", codec.pointsFormat().getClass().getName());
                  map.put("Postings format", codec.postingsFormat().getClass().getName());
                  map.put("SegmentInfo format", codec.segmentInfoFormat().getClass().getName());
                  map.put("StoredFields format", codec.storedFieldsFormat().getClass().getName());
                  map.put("TermVectors format", codec.termVectorsFormat().getClass().getName());
                  map.entrySet().stream()
                      .map(entry -> entry.getKey() + " = " + entry.getValue())
                      .forEach(detailsModel::addElement);
                });
        break;
    }
    segDetailList.setModel(detailsModel);
  }

  private class ListenerFunctions {

    void selectGeneration(ActionEvent e) {
      CommitsPanelProvider.this.selectGeneration();
    }

    void showSegmentDetails(MouseEvent e) {
      CommitsPanelProvider.this.showSegmentDetails();
    }
  }

  private class Observer implements IndexObserver, DirectoryObserver {

    @Override
    public void openDirectory(LukeState state) {
      commitsModel = commitsFactory.newInstance(state.getDirectory(), state.getIndexPath());
      populateCommitGenerations();
    }

    @Override
    public void closeDirectory() {
      close();
    }

    @Override
    public void openIndex(LukeState state) {
      if (state.hasDirectoryReader()) {
        DirectoryReader dr = (DirectoryReader) state.getIndexReader();
        commitsModel = commitsFactory.newInstance(dr, state.getIndexPath());
        populateCommitGenerations();
      }
    }

    @Override
    public void closeIndex() {
      close();
    }

    private void populateCommitGenerations() {
      DefaultComboBoxModel<Long> segGenList = new DefaultComboBoxModel<>();
      for (Commit commit : commitsModel.listCommits()) {
        segGenList.addElement(commit.getGeneration());
      }
      commitGenCombo.setModel(segGenList);

      if (segGenList.getSize() > 0) {
        commitGenCombo.setSelectedIndex(0);
      }
    }

    private void close() {
      commitsModel = null;

      commitGenCombo.setModel(new DefaultComboBoxModel<>());
      deletedLbl.setText("");
      segCntLbl.setText("");
      userDataTA.setText("");
      TableUtils.setupTable(
          filesTable,
          ListSelectionModel.SINGLE_SELECTION,
          new FilesTableModel(),
          null,
          FilesTableModel.Column.FILENAME.getColumnWidth());
      TableUtils.setupTable(
          segmentsTable,
          ListSelectionModel.SINGLE_SELECTION,
          new SegmentsTableModel(),
          null,
          SegmentsTableModel.Column.NAME.getColumnWidth(),
          SegmentsTableModel.Column.MAXDOCS.getColumnWidth(),
          SegmentsTableModel.Column.DELS.getColumnWidth(),
          SegmentsTableModel.Column.DELGEN.getColumnWidth(),
          SegmentsTableModel.Column.VERSION.getColumnWidth(),
          SegmentsTableModel.Column.CODEC.getColumnWidth());
      diagRB.setEnabled(false);
      attrRB.setEnabled(false);
      codecRB.setEnabled(false);
      segDetailList.setModel(new DefaultListModel<>());
    }
  }

  enum ActionCommand {
    DIAGNOSTICS,
    ATTRIBUTES,
    CODEC;
  }

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

    enum Column implements TableColumnInfo {
      FILENAME("Filename", 0, String.class, 200),
      SIZE("Size", 1, String.class, Integer.MAX_VALUE);

      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;
      }
    }

    FilesTableModel() {
      super();
    }

    FilesTableModel(List<File> files) {
      super(files.size());
      for (int i = 0; i < files.size(); i++) {
        File file = files.get(i);
        data[i][Column.FILENAME.getIndex()] = file.getFileName();
        data[i][Column.SIZE.getIndex()] = file.getDisplaySize();
      }
    }

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

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

    enum Column implements TableColumnInfo {
      NAME("Name", 0, String.class, 60),
      MAXDOCS("Max docs", 1, Integer.class, 60),
      DELS("Dels", 2, Integer.class, 60),
      DELGEN("Del gen", 3, Long.class, 60),
      VERSION("Lucene ver.", 4, String.class, 60),
      CODEC("Codec", 5, String.class, 100),
      SIZE("Size", 6, String.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;
      }
    }

    SegmentsTableModel() {
      super();
    }

    SegmentsTableModel(List<Segment> segments) {
      super(segments.size());
      for (int i = 0; i < segments.size(); i++) {
        Segment segment = segments.get(i);
        data[i][Column.NAME.getIndex()] = segment.getName();
        data[i][Column.MAXDOCS.getIndex()] = segment.getMaxDoc();
        data[i][Column.DELS.getIndex()] = segment.getDelCount();
        data[i][Column.DELGEN.getIndex()] = segment.getDelGen();
        data[i][Column.VERSION.getIndex()] = segment.getLuceneVer();
        data[i][Column.CODEC.getIndex()] = segment.getCodecName();
        data[i][Column.SIZE.getIndex()] = segment.getDisplaySize();
      }
    }

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