/**
* LineFilter.bsh - a BeanShell macro for the jEdit 4.3 text editor
* that filters lines of a buffer due to a provided regular expression.
*
 * :folding=indent:collapseFolds=1:
*
* The filter actions are:   - Write to a new buffer (grep-like).
*                           - Remove from current buffer.
* The filter is applied on: - matching lines, or
*                           - not matching lines
* The filter works on:      - the whole buffer, or
*                           - the selection, if it spans over mor than one line
*
 *
 * Copyright (C) 2008 Robert Schwenn
 *
 * History:
   * 08.08.2005  1.0  initial Version
   * 29.01.2008  1.2  - changed regexp to java.util.regex
   *                  - consolidated gui and code
   * 01.06.2008  1.3  - "Remove from current buffer" is only available,
   *                    if the buffer is editable and nor multiple or
   *                    rectangular selection is present.
   *                  - Input field is pre-filled with the selection.
   *
 *
 * This macro is based on the following macros:
 * - Retain_Or_Remove_Lines.bsh                by Jia Zhiming
 * - Grep_Buffer.bsh, Reverse_Grep_Buffer.bsh  by Robert Fletcher
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
*/


import java.util.regex.*;
import javax.swing.border.*;

// constants used in more than one methods
final String DELETE_COMMAND = "delete";
final String WRITE_TO_NEW_BUFFER_COMMAND = "writeToNewBuffer";


// GUI
void LineFilterDialog() {
  //
  // constants
    final static String processNotMatchedCommand          = "notMatching";
    final static String processMatchedCommand             = "matching";

    // Common Dialog Labels
    final static String PROPERTY_COMMON_OK                = jEdit.getProperty("common.ok");
    final static String PROPERTY_COMMON_CANCEL            = jEdit.getProperty("common.cancel");

    final static String NotEditableMessage                = jEdit.getProperty("macro.rs.general.ErrorNotEditableDialog.message",             "Buffer is not editable");
    final static String MultipleSelectionMessage          = jEdit.getProperty("macro.rs.LineFilter.MultipleSelection.message",               "Multiple selection is not supported");
    final static String BlockSelectionMessage             = jEdit.getProperty("macro.rs.LineFilter.BlockSelection.message",                  "Rectangular selection is not supported");
    final static String DeleteActionDisabledMessage       = jEdit.getProperty("macro.rs.LineFilter.DeleteActionDisabled.message",            "Delete is not available");

    // Special Dialog Labels
    final static String MainDialogTitle                   = jEdit.getProperty("macro.rs.LineFilter.MainDialog.title",                        "LineFilter");
    final static String ignoreCaseCheckBoxText            = jEdit.getProperty("macro.rs.LineFilter.ignoreCaseCheckBox.label",                "Ignore case");
    final static String regexpPanelBorderTitle            = jEdit.getProperty("macro.rs.LineFilter.regexpPanelBorder.title",                 "Pattern to compare (Regular Expression)");

    final static String processModeRadioButtonMatching    = jEdit.getProperty("macro.rs.LineFilter.processModeRadioButtonMatching.label",    "Matching");
    final static String processModeRadioButtonNotMatching = jEdit.getProperty("macro.rs.LineFilter.processModeRadioButtonNotMatching.label", "Not Matching");
    final static String processModeOptionPanelBorderTitle = jEdit.getProperty("macro.rs.LineFilter.processModeOptionPanelBorder.title",      "Lines");

    final static String actionModeRadioButtonWrite        = jEdit.getProperty("macro.rs.LineFilter.actionModeRadioButtonWrite.label",        "Write to new buffer");
    final static String actionModeRadioButtonDelete       = jEdit.getProperty("macro.rs.LineFilter.actionModeRadioButtonDelete.label",       "Delete");
    final static String actionModeOptionPanelBorderTitle  = jEdit.getProperty("macro.rs.LineFilter.actionModeOptionPanelBorder.title",       "Action");

  //
  // ***  Eventhandler  *********************************************************

    // Button oder ENTER key pressed
    public void actionPerformed(ActionEvent e) {
      //Macros.message(view, "actionPerformed: ''" + e.getActionCommand() + "'");
      boolean processMatchedLines;

      if (e.getActionCommand() != PROPERTY_COMMON_CANCEL) {
        // evaluate status of radio buttons
        if (processModeButtonGroup.getSelection().getActionCommand() == processMatchedCommand) {
          processMatchedLines = true;
        } else {
          processMatchedLines = false;
        }
        //String actionModeCommand = e.getactionModeCommand();
        String actionModeCommand = actionModeButtonGroup.getSelection().getActionCommand();
        String regExp = regexpTextfield.getText();
        boolean ignoreCase = ignoreCaseCheckBox.isSelected();
        regexpTextfield.addCurrentToHistory();

        if (processText(regExp, ignoreCase, processMatchedLines, actionModeCommand)) dialog.dispose();

      } else {
        dialog.dispose();
      }
    }

    public void keyPressed(KeyEvent e) {
      //Macros.message(view, "keyPressed..." + e.getKeyCode());
      if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
        //Macros.message(view, "ESCAPE pressed.");
        dialog.setVisible(false);
        dialog.dispose();
      }
    }

    // to avoid BeanShell throwing exceptions each time one of these events occur.
    public void keyReleased(KeyEvent e) { }
    public void keyTyped(KeyEvent e) { }
  // ****************************************************************************

  //
  // prepare dialog
    dialog  = new JDialog(view, MainDialogTitle, true);
    content = new JPanel(new BorderLayout());
    content.setBorder(new EmptyBorder(8, 8, 8, 8));
    dialog.setContentPane(content);

  // prepare Buttons and button panel
    JButton okButton = new JButton(PROPERTY_COMMON_OK);
    JButton cancelButton = new JButton(PROPERTY_COMMON_CANCEL);

    dialog.getRootPane().setDefaultButton(okButton);

    buttonPanel = new JPanel();
    buttonPanel.setLayout(new BoxLayout(buttonPanel,BoxLayout.X_AXIS));
    buttonPanel.setBorder(new EmptyBorder(10, 10, 0, 10));
    buttonPanel.add(Box.createGlue());
    buttonPanel.add(cancelButton);
    buttonPanel.add(Box.createHorizontalStrut(15));
    buttonPanel.add(okButton);
    buttonPanel.add(Box.createGlue());

  // prepare regexp input frame
    regexpTextfield = new HistoryTextField("macro.rs.LineFilter.regexp");
    regexpTextfield.setActionCommand(dialog.getRootPane().getDefaultButton().getActionCommand());

    ignoreCaseCheckBox = new JCheckBox(ignoreCaseCheckBoxText, false);

    regexpPanel = new JPanel(new GridLayout(2, 1, 5, 8));
    regexpPanel.add(regexpTextfield);
    regexpPanel.add(ignoreCaseCheckBox);
    regexpPanel.setBorder(BorderFactory.createCompoundBorder(
                          BorderFactory.createTitledBorder(" " + regexpPanelBorderTitle + " "),
                          BorderFactory.createEmptyBorder(0,0,0,0)));

  // prepare process mode option panel
    ButtonGroup processModeButtonGroup = new ButtonGroup();
    JRadioButton[] processModeRadioButtons = new JRadioButton[2];
    processModeRadioButtons[0] = new JRadioButton(processModeRadioButtonMatching);
    processModeRadioButtons[1] = new JRadioButton(processModeRadioButtonNotMatching);
    processModeRadioButtons[0].setActionCommand(processMatchedCommand);
    processModeRadioButtons[1].setActionCommand(processNotMatchedCommand);
    processModeRadioButtons[0].setSelected(true);
    processModeButtonGroup.add(processModeRadioButtons[0]);
    processModeButtonGroup.add(processModeRadioButtons[1]);

    processModeOptionPanel = new JPanel(new GridLayout(2, 1, 0, 0));
    processModeOptionPanel.add(processModeRadioButtons[0]);
    processModeOptionPanel.add(processModeRadioButtons[1]);
    processModeOptionPanel.setBorder(BorderFactory.createCompoundBorder(
                         BorderFactory.createTitledBorder(" " + processModeOptionPanelBorderTitle + " "),
                         BorderFactory.createEmptyBorder(0,0,0,0)));


  // prepare action mode option panel
    ButtonGroup actionModeButtonGroup = new ButtonGroup();
    JRadioButton[] actionModeRadioButtons = new JRadioButton[2];
    actionModeRadioButtons[0] = new JRadioButton(actionModeRadioButtonWrite);
    actionModeRadioButtons[1] = new JRadioButton(actionModeRadioButtonDelete);
    actionModeRadioButtons[0].setActionCommand(WRITE_TO_NEW_BUFFER_COMMAND);
    actionModeRadioButtons[1].setActionCommand(DELETE_COMMAND);
    actionModeRadioButtons[0].setSelected(true);
    actionModeButtonGroup.add(actionModeRadioButtons[0]);
    actionModeButtonGroup.add(actionModeRadioButtons[1]);

    actionModeOptionPanel = new JPanel(new GridLayout(2, 1, 0, 0));
    actionModeOptionPanel.add(actionModeRadioButtons[0]);
    actionModeOptionPanel.add(actionModeRadioButtons[1]);
    actionModeOptionPanel.setBorder(BorderFactory.createCompoundBorder(
                         BorderFactory.createTitledBorder(" " + actionModeOptionPanelBorderTitle + " "),
                         BorderFactory.createEmptyBorder(0,0,0,0)));



  // place panels at dialog
    content.add(regexpPanel, BorderLayout.NORTH);
    content.add(processModeOptionPanel, BorderLayout.WEST);
    content.add(actionModeOptionPanel, BorderLayout.EAST);
    content.add(buttonPanel, BorderLayout.SOUTH);
  //
  // add event listeners to components
    okButton.addActionListener(this);
    cancelButton.addActionListener(this);
    regexpTextfield.addActionListener(this);

    okButton.addKeyListener(this);
    cancelButton.addKeyListener(this);
    regexpTextfield.addKeyListener(this);
    ignoreCaseCheckBox.addKeyListener(this);
    processModeRadioButtons[0].addKeyListener(this);
    processModeRadioButtons[1].addKeyListener(this);
    actionModeRadioButtons[0].addKeyListener(this);
    actionModeRadioButtons[1].addKeyListener(this);
  //
  // change GUI environment-dependent
    Selection[] selection = textArea.getSelection();

    // pre-fill input field with selection
      if (selection.length != 0) {
        if (selection[0].getStartLine() == selection[0].getEndLine()) {
          String SelectedText = textArea.getSelectedText(selection[0]);
          regexpTextfield.setText(SelectedText);
        }
      }

    // availability of delete action is conditioned:
      delete_ok = true;

      if (! buffer.isEditable()) {
        delete_ok = false;
        Msg = NotEditableMessage;
      }

      if (selection.length > 1) {
        // multiple selection present!
        delete_ok = false;
        Msg = MultipleSelectionMessage;

      } else if (selection.length == 1) {
        if ((textArea.getSelectedLines().length > 1) && (! (selection[0] instanceof Selection.Range))) {
          // single selection consists of more than one line, but is not of range type.
          delete_ok = false;
          Msg = BlockSelectionMessage;
        }
      }

      if (! delete_ok) {
        actionModeRadioButtons[1].setEnabled(false);
        view.getStatus().setMessageAndClear(MainDialogTitle + ": " + DeleteActionDisabledMessage + " (" + Msg + ")");
      }


  //
  // start dialog
    dialog.pack();
    dialog.setLocationRelativeTo(view);
    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    dialog.setVisible(true);
}

// do the job
boolean processText(String regexpString, boolean ignoreCase, boolean processMatchedLines, String actionModeCommand) {
  // returns false, if regular expression is invalid, else true.

  // debug
    //Macros.message(view, "processText: regexpString = ''" + regexpString + "'"
    //                   + "'\nprocessMatchedLines = "  + processMatchedLines
    //                   + "\nactionModeCommand = " + actionModeCommand
    //                   );
  // labels
    final static String ErrorRegexpDialogTitle   = jEdit.getProperty("macro.rs.LineFilter.ErrorRegexpDialog.title",   "Invalid regular expression");
    final static String ErrorRegexpDialogMessage = jEdit.getProperty("macro.rs.LineFilter.ErrorRegexpDialog.message", "The Regular expression is invalid!");
    final static String SummaryDeletedText       = jEdit.getProperty("macro.rs.LineFilter.SummaryDeleted.label",      "Lines deleted!");
    final static String SummaryWrittenText       = jEdit.getProperty("macro.rs.LineFilter.SummaryWritten.label",      "Lines written to a new buffer.");

  // declarations
    boolean writeLinesToNewBuffer = false;
    boolean deleteLines = false;
    boolean lineMatches;
    boolean appendLine;
    int regexpFlags  = 0;
    int deletedLines = 0;
    int writtenLines = 0;
    int sbfLength;

    StringBuffer sbf = new StringBuffer();
    Pattern pattern = null;
    Matcher matcher = null;

  // evaluate parameters
    if (actionModeCommand == DELETE_COMMAND) deleteLines = true;
    if (actionModeCommand == WRITE_TO_NEW_BUFFER_COMMAND) writeLinesToNewBuffer = true;

    if (ignoreCase) regexpFlags += Pattern.CASE_INSENSITIVE;

  //
  // create the regexp matcher
    try {
      pattern = Pattern.compile(regexpString, regexpFlags);
      matcher = pattern.matcher("x");
    }
    catch (PatternSyntaxException e) {
      JOptionPane.showMessageDialog(view, ErrorRegexpDialogMessage, ErrorRegexpDialogTitle, JOptionPane.ERROR_MESSAGE);
      return false;
    }

  //
  // get lines to process
    int[] lines = textArea.getSelectedLines();

    if (lines == null || lines.length <= 1) {
      // no selection ==> process all lines
      iMax = textArea.getLineCount();
      linesSelected = false;
    } else {
      // selection exist
      iMax = lines.length;
      linesSelected = true;
    }

  // parse every line to process
    for (int i = 0; i < iMax; i++) {
      // get currentLineNr
        if (linesSelected) {
          currentLineNr = lines[i];
        } else {
          currentLineNr = i;
        }

      // compare current line's text against regxep
        lineText = textArea.getLineText(currentLineNr);
        matcher.reset(lineText);
        lineMatches = matcher.find();
        appendLine  = false;
        //Macros.message(view, "matcher.matches() = " + matcher.find());

      // collect lines for result (which is a new or the current buffer)
        if (writeLinesToNewBuffer) {
          appendLine = ((processMatchedLines && lineMatches) || (!processMatchedLines && !lineMatches));

        } else if (deleteLines) {
          appendLine = ((processMatchedLines && !lineMatches) || (!processMatchedLines && lineMatches));
        }

        if (appendLine) sbf.append(lineText).append("\n");
    }
    sbfLength = sbf.length();

  // write collected lines (to new or current buffer)
    if (deleteLines) {
      if (sbfLength > 0) sbf.deleteCharAt(sbfLength - 1);
      int oldLineCount = textArea.getLineCount();
      if (linesSelected) {
        textArea.setSelectedText(sbf.toString());
      } else {
        textArea.setText(sbf.toString());
      }
      int newLineCount = textArea.getLineCount();
      deletedLines = oldLineCount - newLineCount;
      Msg = deletedLines.toString() + " " + SummaryDeletedText;

    } else if (writeLinesToNewBuffer) {
      if (sbfLength > 0) {
        jEdit.newFile(view);
        textArea.setSelectedText(sbf.toString());
        writtenLines = textArea.getLineCount() - 1;
      }
      Msg = writtenLines.toString() + " " + SummaryWrittenText;
    }
    view.getStatus().setMessageAndClear(Msg);

  return true;
}

// starting point
LineFilterDialog();
