/*
 * jNPad v0.3 - jNPad's an Simple Text Editor written in Java
 *
 * Copyright (C) 2014-2017  rgs
 *
 * Require JDK 1.6 (or later)
 *
 * 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 (at your option)
 * 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.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 *
 * Info, Questions, Suggestions & Bugs Report to rgsevero@gmail.com
 */

package jnpad.search;

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.text.BadLocationException;

import jnpad.GUIUtilities;
import jnpad.JNPadBundle;
import jnpad.JNPadFrame;
import jnpad.text.EditPane;
import jnpad.text.JNPadTextArea;
import jnpad.text.Buffer;
import jnpad.ui.EdgeBorder;
import jnpad.ui.EscapableDialog;
import jnpad.ui.JNPadCheckBox;
import jnpad.ui.JNPadComboBox;
import jnpad.ui.JNPadLabel;
import jnpad.ui.ReporterUtilities;
import jnpad.ui.status.StatusDisplayable.StatusType;
import jnpad.util.Utilities;

/**
 * The Class SearchDialog.
 *
 * @version 0.3
 * @since   jNPad v0.1
 */
public abstract class SearchDialog extends EscapableDialog {
  JPanel                    contentPane;
  JPanel                    pnAllButtons     = new JPanel();
  JPanel                    pnAllOptions     = new JPanel();
  JPanel                    pnButtons        = new JPanel();

  JPanel                    pnOptions        = new JPanel();
  JPanel                    pnOptionsN       = new JPanel();
  JPanel                    pnOptionsS       = new JPanel();
  JPanel                    pnOptionsSW      = new JPanel();
  JPanel                    pnOptionsSE      = new JPanel();

  JPanel                    pnFind           = new JPanel();
  JLabel                    lbFind           = new JNPadLabel();
  JComboBox                 cmbFind          = new JNPadComboBox();

  static final int          ACTIVE_DOCUMENT  = 0, OPEN_DOCUMENTS = 1, SELECTION = 2;

  JPanel                    pnLookIn         = new JPanel();
  JLabel                    lbLookIn         = new JNPadLabel();
  JComboBox                 cmbLookIn        = new JNPadComboBox(new String[] {
                                             SearchBundle.getString("SearchDialog.lookIn.activeDocument"), //$NON-NLS-1$
                                             SearchBundle.getString("SearchDialog.lookIn.openDocuments"),  //$NON-NLS-1$
                                             SearchBundle.getString("SearchDialog.lookIn.selection") });   //$NON-NLS-1$

  JCheckBox                 cbMatchCase      = new JNPadCheckBox();
  JCheckBox                 cbFindFromCursor = new JNPadCheckBox();
  JCheckBox                 cbMatchWholeWord = new JNPadCheckBox();

  JPanel                    pnFindDirection  = new JPanel();
  JRadioButton              rbFindFoward     = new JRadioButton();
  JRadioButton              rbFindBackward   = new JRadioButton();
  ButtonGroup               findDirection    = new ButtonGroup();

  JButton                   btFind           = new JButton();
  JButton                   btClose          = new JButton();

  JLabel                    lbStatusBar      = new JNPadLabel();

  Buffer                    _buffer;
  int                       _actualPosition;
  int                       _minPosition;
  int                       _maxPosition;
  int                       _inc;
  int                       _lastInc;

  JNPadFrame                jNPad;

  SearchManagerDialog       searchManagerDialog;

  final ActionListener      actionHandler    = new ActionHandler();
  final KeyListener         keyHandler       = new KeyHandler();

  /** Logger */
  static final Logger       LOGGER           = Logger.getLogger(SearchDialog.class.getName());

  /** UID */
  private static final long serialVersionUID = -7753041887038443611L;

  /**
   * Instantiates a new search dialog.
   *
   * @param jNPad JNPadFrame
   * @param title String
   */
  SearchDialog(JNPadFrame jNPad, String title) {
    super(jNPad, title, true);
    try {
      this.jNPad = jNPad;

      jbInit();
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      jNPad.setStatus(StatusType.ERROR, ex.getMessage());
    }
  }

  /**
   * Component initialization.
   *
   * @throws Exception the exception
   */
  private void jbInit() throws Exception {
    contentPane = (JPanel)this.getContentPane();
    contentPane.setLayout(new BorderLayout(5, 5));
    contentPane.setBorder(GUIUtilities.createEmptyBorder(5));
    contentPane.add(pnAllButtons, BorderLayout.EAST);
    contentPane.add(pnAllOptions, BorderLayout.CENTER);
    contentPane.add(lbStatusBar, BorderLayout.SOUTH);

    searchManagerDialog = new SearchManagerDialog(jNPad, this);

    lbStatusBar.setBorder(new EdgeBorder(EdgeBorder.EDGE_TOP));
    lbStatusBar.setText(Utilities.SPACE_STRING);

    lbFind.setText(SearchBundle.getString("SearchDialog.find"));                     //$NON-NLS-1$
    lbLookIn.setText(SearchBundle.getString("SearchDialog.lookIn"));                 //$NON-NLS-1$
    cbMatchCase.setText(SearchBundle.getString("SearchDialog.matchCase"));           //$NON-NLS-1$
    cbFindFromCursor.setText(SearchBundle.getString("SearchDialog.findFromCursor")); //$NON-NLS-1$
    cbMatchWholeWord.setText(SearchBundle.getString("SearchDialog.matchWholeWord")); //$NON-NLS-1$
    rbFindFoward.setText(SearchBundle.getString("SearchDialog.findFoward"));         //$NON-NLS-1$
    rbFindBackward.setText(SearchBundle.getString("SearchDialog.findBackward"));     //$NON-NLS-1$
    findDirection.add(rbFindFoward);
    findDirection.add(rbFindBackward);

    cmbFind.setEditable(true);
    cmbFind.getEditor().getEditorComponent().addKeyListener(keyHandler);
    cmbLookIn.addActionListener(actionHandler);

    btFind.setText(SearchBundle.getString("SearchDialog.findNext")); //$NON-NLS-1$
    btFind.addActionListener(actionHandler);
    btClose.setText(SearchBundle.getString("SearchDialog.close")); //$NON-NLS-1$
    btClose.addActionListener(actionHandler);

    pnAllButtons.setLayout(new BorderLayout());
    pnAllButtons.add(pnButtons, BorderLayout.NORTH);

    pnButtons.setLayout(new GridLayout(0, 1, 5, 5));

    pnAllOptions.setLayout(new BorderLayout());
    pnAllOptions.add(pnOptions, BorderLayout.NORTH);

    pnOptions.setLayout(new BorderLayout(5, 5));
    pnOptions.add(pnOptionsN, BorderLayout.NORTH);
    pnOptions.add(pnOptionsS, BorderLayout.SOUTH);

    pnOptionsN.setLayout(new GridLayout(0, 1, 5, 5));

    pnFind.setLayout(new BorderLayout(5, 5));
    pnFind.add(lbFind, BorderLayout.WEST);
    pnFind.add(cmbFind, BorderLayout.CENTER);

    pnLookIn.setLayout(new BorderLayout(5, 5));
    pnLookIn.add(lbLookIn, BorderLayout.WEST);
    pnLookIn.add(cmbLookIn, BorderLayout.CENTER);

    pnOptionsS.setLayout(new BorderLayout(5, 5));
    pnOptionsS.add(pnOptionsSW, BorderLayout.WEST);
    pnOptionsS.add(pnOptionsSE, BorderLayout.EAST);

    pnOptionsSW.setLayout(new GridLayout(0, 1));
    pnOptionsSW.add(cbMatchCase, null);
    pnOptionsSW.add(cbFindFromCursor, null);
    pnOptionsSW.add(cbMatchWholeWord, null);

    pnOptionsSE.setLayout(new BorderLayout());
    pnOptionsSE.add(pnFindDirection, BorderLayout.NORTH);

    pnFindDirection.setBorder(BorderFactory.createTitledBorder(SearchBundle.getString("SearchDialog.direction"))); //$NON-NLS-1$
    pnFindDirection.setLayout(new GridLayout(0, 1));
    pnFindDirection.add(rbFindFoward, null);
    pnFindDirection.add(rbFindBackward, null);

    setResizable(false);
  }

  /**
   * Load state.
   */
  abstract void loadState();

  /**
   * Save state.
   */
  abstract void saveState();

  /**
   * Initialize.
   *
   * @param hide the hide
   * @return true, if successful
   */
  abstract boolean initialize(boolean hide);

  /**
   * Gets the search for.
   *
   * @return String
   */
  String getSearchFor() {
    return GUIUtilities.getItem(cmbFind);
  }

  /**
   * Gets the search context.
   *
   * @return the search context
   */
  abstract SearchContext getSearchContext();

  /**
   * Check buffer.
   *
   * @return true, if successful
   */
  boolean checkBuffer() {
    if (_buffer == null) {
      _buffer = jNPad.getActiveBuffer();
    }
    return _buffer != null;
  }

  /**
   * Reset find.
   */
  void resetFind() {
    if (!checkBuffer())
      return;

    resetFind(true);

    if (cbFindFromCursor.isSelected()) {
      _actualPosition = _buffer.getCaretPosition();
    }
  }

  /**
   * Reset replace.
   */
  void resetReplace() {
    //empty
  }

  /**
   * Reset find.
   *
   * @param next boolean
   */
  void resetFind(boolean next) {
    final JNPadTextArea textArea = _buffer.getSelectedTextArea();

    _lastInc = 0;
    _inc = rbFindFoward.isSelected() ? 1 : -1;

    final boolean selection = cmbLookIn.getSelectedIndex() == SELECTION;

    if(next) {
      if (selection) {
        _actualPosition = rbFindFoward.isSelected() ? textArea.getSelectionStart() : textArea.getSelectionEnd();
        _minPosition = textArea.getSelectionStart();
        _maxPosition = textArea.getSelectionEnd();
      }
      else {
        if (rbFindFoward.isSelected()) {
          _actualPosition = 0;
        }
        else {
          _actualPosition = textArea.getDocument().getLength() - 1;
          if (_actualPosition < 0)
            _actualPosition = 0;
        }
        _minPosition = 0;
        _maxPosition = textArea.getDocument().getLength() - 1;
        if (_maxPosition < 0)
          _maxPosition = 0;
      }
    }
    else {
      if (selection) {
        _actualPosition = rbFindFoward.isSelected() ? textArea.getSelectionEnd() : textArea.getSelectionStart();
        _minPosition = textArea.getSelectionStart();
        _maxPosition = textArea.getSelectionEnd();
      }
      else {
        if (rbFindFoward.isSelected()) {
          _actualPosition = textArea.getDocument().getLength() - 1;
          if (_actualPosition < 0)
            _actualPosition = 0;
        }
        else {
          _actualPosition = 0;
        }
        _minPosition = 0;
        _maxPosition = textArea.getDocument().getLength() - 1;
        if (_maxPosition < 0)
          _maxPosition = 0;
      }
    }
  }

  /**
   * Search next.
   *
   * @return true, if successful
   */
  boolean searchNext() {
    if(rbFindFoward.isSelected()) {
      _inc = 1;
      if (_lastInc == -1) {
        _actualPosition += 2;
      }
    }
    else {
      _inc = -1;
      if (_lastInc == 1) {
        _actualPosition -= 2;
      }
    }
    return find(false);
  }

  /**
   * Search previous.
   *
   * @return true, if successful
   */
  boolean searchPrevious() {
    if(rbFindFoward.isSelected()) {
      _inc = -1;
      if (_lastInc == 1) {
        _actualPosition -= 2;
      }
    }
    else {
      _inc = 1;
      if (_lastInc == -1) {
        _actualPosition += 2;
      }
    }
    return find(false);
  }

  /**
   * Find.
   *
   * @param all boolean
   * @return true, if successful
   */
  boolean find(boolean all) {
    return checkBuffer() && doFind(_buffer, all);
  }

  /**
   * Do find.
   *
   * @param buffer the buffer
   * @param all boolean
   * @return true, if successful
   */
  boolean doFind(Buffer buffer, boolean all) {
    final EditPane     editPane = buffer.getSelectedEditPane();
    final JNPadTextArea textArea = buffer.getSelectedTextArea();
    final SearchContext context = getSearchContext();

    removeHighlight(editPane, context);

    String searchIn = textArea.getText();
    String searchFor = getSearchFor();

    char[] searchInArray;
    char[] searchForArray;

    if (cbMatchCase.isSelected()) {
      searchInArray = searchIn.toCharArray();
      searchForArray = searchFor.toCharArray();
    }
    else {
      searchInArray = searchIn.toLowerCase().toCharArray();
      searchForArray = searchFor.toLowerCase().toCharArray();
    }

    if (all) { // find all
      jNPad.setFindResultsVisible(true);
    }

    boolean addNewFound = false;

    int len = searchInArray.length;

    for (; _actualPosition < len && _actualPosition <= _maxPosition && _actualPosition >= _minPosition; _actualPosition += _inc) {
      if (searchInArray[_actualPosition] != searchForArray[0] || (_actualPosition + searchForArray.length) - 1 > _maxPosition)
        continue;
      int endPosition = _actualPosition + 1;
      int ln;
      for (ln = 1; ln < searchForArray.length && searchInArray[endPosition] == searchForArray[ln]; ln++)
        endPosition++;

      if (ln != searchForArray.length)
        continue;

      if (!checkWholeWord(searchInArray, _actualPosition, ln, _minPosition, _maxPosition))
        continue;

      if (all) { // find all
        if (!addNewFound) {
          jNPad.addNewFound(getSearchFor(), buffer.getFilePath());
          addNewFound = true;
        }
        try {
          String line = textArea.getLineTextOfOffset(_actualPosition);
          line = Utilities.removeNewLine(line);
          jNPad.addNewText(new Text(line, context, buffer.getFilePath(), _actualPosition, endPosition));
        }
        catch(BadLocationException ex) {
          LOGGER.log(Level.WARNING, ex.getMessage(), ex);
        }
        continue;
      }

      try { // find text
        editPane.highlightSearch(context, _actualPosition, endPosition);
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
      _actualPosition += _inc;
      _lastInc = _inc;
      return true;
    }

    if (all) { // find all
      editPane.highlightAllOccurrences(context);
      jNPad.showFoundTarget();
    }

    return false;
  }

  /**
   * Removes the highlight.
   *
   * @param editPane the edit pane
   * @param context the context
   */
  abstract void removeHighlight(EditPane editPane, SearchContext context);

  /**
   * Report word not found.
   */
  void reportWordNotFound() {
    ReporterUtilities.reportError(this,
                              SearchBundle.getString("SearchDialog.notFound", getSearchFor()), //$NON-NLS-1$
                              getTitle());
  }

  /**
   * Report word not found2.
   *
   * @return int
   */
  int reportWordNotFound2() {
    int option;

    String[] options = { JNPadBundle.getYesOptionText(), JNPadBundle.getCancelOptionText() };

    if (rbFindFoward.isSelected()) {
      option = JOptionPane.showOptionDialog(this,
                                            SearchBundle.getString("SearchDialog.options.message.next", getSearchFor()), //$NON-NLS-1$
                                            JNPadBundle.getInformationTitle(),
                                            JOptionPane.DEFAULT_OPTION,
                                            JOptionPane.INFORMATION_MESSAGE,
                                            null,
                                            options,
                                            options[0]);
    }
    else {
      option = JOptionPane.showOptionDialog(this,
                                            SearchBundle.getString("SearchDialog.options.message.previous", getSearchFor()), //$NON-NLS-1$
                                            JNPadBundle.getInformationTitle(),
                                            JOptionPane.DEFAULT_OPTION,
                                            JOptionPane.INFORMATION_MESSAGE,
                                            null,
                                            options,
                                            options[0]);
    }

    return option;
  }

  /**
   * Replace.
   *
   * @param all the all
   * @return true, if successful
   */
  boolean replace(boolean all) {
    return false;
  }

  /**
   * Check whole word.
   *
   * @param searchInArray char[]
   * @param index int
   * @param len int
   * @param minIndex int
   * @param maxIndex int
   * @return true, if successful
   */
  boolean checkWholeWord(char[] searchInArray, int index, int len, int minIndex, int maxIndex) {
    boolean wsBefore = true, wsAfter = true;

    if (cbMatchWholeWord.isSelected()) {
      if (index - 1 >= minIndex)
        wsBefore = !Character.isUnicodeIdentifierPart(searchInArray[index - 1]);
      if (index + len <= maxIndex)
        wsAfter = !Character.isUnicodeIdentifierPart(searchInArray[index + len]);
    }

    return wsBefore && wsAfter;
  }

  //////////////////////////////////////////////////////////////////////////////
  /**
   * The Class KeyHandler.
   */
  class KeyHandler extends KeyAdapter {
    /**
     * Key pressed.
     *
     * @param e the KeyEvent
     * @see java.awt.event.KeyAdapter#keyPressed(java.awt.event.KeyEvent)
     */
    @Override
    public void keyPressed(KeyEvent e) {handleKeyPressed(e);}
  }
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Handle key pressed.
   *
   * @param e KeyEvent
   */
  abstract void handleKeyPressed(KeyEvent e);

  //////////////////////////////////////////////////////////////////////////////
  /**
   * The Class ActionHandler.
   */
  class ActionHandler implements ActionListener {
    /**
     * Action performed.
     *
     * @param e the ActionEvent
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     */
    @Override
    public void actionPerformed(ActionEvent e) {handleActionPerformed(e);}
  }
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Handle action performed.
   *
   * @param e ActionEvent
   */
  void handleActionPerformed(ActionEvent e) {
    Object obj = e.getSource();
    if (obj == btClose) {
      setVisible(false);
    }
    else if (obj == btFind && initialize(true)) {
      if (find(false)) {
        searchManagerDialog.loadState();
      }
      else {
        reportWordNotFound();
      }
    }
  }

}
