/*
 * The contents of this file is dual-licensed under 2 
 * alternative Open Source/Free licenses: LGPL 2.1 or later and 
 * Apache License 2.0. (starting with JNA version 4.0.0).
 * 
 * You can freely decide which license you want to apply to 
 * the project.
 * 
 * You may obtain a copy of the LGPL License at:
 * 
 * http://www.gnu.org/licenses/licenses.html
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 * 
 * You may obtain a copy of the Apache License at:
 * 
 * http://www.apache.org/licenses/
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.contrib.demo;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.ArrayList;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Popup;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
//TODO: FilteredTextField - Comment this class.
/**
 * The FilteredTextField class is a JTextField that only allows specified
 * characters to be entered into it.  The allowed characters can be added to
 * the text field, and entry validation is performed as each character is
 * typed.  In addition, complete string validation is tested against a
 * configurable regular expression when leaving the field.  If the string is
 * invalid the text field is bordered with a red line, and the user is notified
 * of the error upon returning to the text field.  The text field can also be
 * configured to accept a limited number of characters.
 */
@SuppressWarnings("serial")
public class FilteredTextField extends JTextField {
  public static final Character[] UPPERCASE_CHARS = {'A', 'B', 'C', 'D', 'E',
    'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
    'U', 'V', 'W', 'X', 'Y', 'Z'};
  public static final Character[] LOWERCASE_CHARS = {'a', 'b', 'c', 'd', 'e',
    'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
    'u', 'v', 'w', 'x', 'y', 'z'};
  public static final Character[] NUMERIC_CHARS = {'1', '2', '3', '4', '5',
    '6', '7', '8', '9', '0'};
  
  private static final Integer ENTRY_BALLOON = 0;
  private static final Integer VALID_BALLOON = 1;
  private static final Integer LENGTH_BALLOON = 2;
  private static final Border RED_BORDER =
    BorderFactory.createLineBorder(Color.RED, 2);
  
  private ArrayList<Character> allowable = new ArrayList<Character>();
  private int maximumLength = String.valueOf(Long.MAX_VALUE).length();
  
  private Border defaultBorder = null;
  private boolean isValid = true;
  
  private Popup balloon = null;
  private String entryError = null;
  private String validRegex = null;
  private String validError = null;
  private Color balloonBorderColor = null;
  private Color balloonBackgroundColor = null;
  private Color balloonTextColor = null;
  private Integer balloonDuration = null;
  
  private Integer balloonType = null;
  
  /**
   * Create a FilteredTextField.
   */
  public FilteredTextField () {
    super();
    init();
  }
  
  /**
   * Create a FilteredTextField.
   * @param columns the number of columns to use to calculate the preferred
   * width
   */
  public FilteredTextField (int columns) {
    super(columns);
    init();
  }
  
  /*
   * Initialize the FilteredTextField.
   */
  private void init () {
    defaultBorder = getBorder();
    entryError = "";
    validRegex = "";
    validError = "";
    balloonBorderColor = BalloonTipManager.DEFAULT_BORDER_COLOR;
    balloonBackgroundColor = BalloonTipManager.DEFAULT_BACKGROUND_COLOR;
    balloonTextColor = BalloonTipManager.DEFAULT_TEXT_COLOR;
    balloonDuration = 10000;
    balloonType = ENTRY_BALLOON;
    addFocusListener(new ValidationEar());
  }
  
  /**
   * Sets the allowable character used for entry validation.
   * @param characters the allowable characters
   */
  public void setCharacters (Character[] characters) {
    clearCharacters();
    for (int i = 0; i < characters.length; i++) {
      addCharacter(characters[i]);
    }
  }
  
  /**
   * Adds the character array to the list used for entry validation.
   * @param characters the character array
   */
  public void addCharacters (Character[] characters) {
    for (int i = 0; i < characters.length; i++) {
      addCharacter(characters[i]);
    }
  }
  
  /**
   * Adds the character to the list used for entry validation.
   * @param characters the character
   */
  public void addCharacter (Character character) {
    if (!allowable.contains(character)) {
      allowable.add(character);
    }
  }
  
  /**
   * Clears the list of allowable characters for entry validation.
   */
  public void clearCharacters () {
    allowable.clear();
  }
  
  /**
   * Removes the character array from the list used for entry validation.
   * @param characters the character array
   */
  public void removeCharacters (Character[] characters) {
    for (int i = 0; i < characters.length; i++) {
      removeCharacter(characters[i]);
    }
  }
  
  /**
   * Removes the character from the list used for entry validation.
   * @param character the character
   */
  public void removeCharacter (Character character) {
    if (allowable.contains(character)) {
      allowable.remove(character);
    }
  }
  
  /**
   * Sets the maximum number of characters for the length of the entry string.
   * @param maximumLength the number of characters
   */
  public void setMaximumLength (int maximumLength) {
    this.maximumLength = maximumLength;
  }
  
  /**
   * Sets the message that is displayed when there is an entry error.
   * @param entryError the entry error message
   */
  public void setEntryError (String entryError) {
    this.entryError = entryError;
  }
  
  /**
   * Sets the regular expression that is used for string validation.  String
   * validation is checked when exiting the text field.
   * @param validRegex the validation regular expression
   */
  public void setValidRegex (String validRegex) {
    this.validRegex = validRegex;
  }
  
  /**
   * Sets the message that is displayed when there is a validation error.
   * @param validError the validation error message
   */
  public void setValidError (String validError) {
    this.validError = validError;
  }
  
  /**
   * Sets the color to use for the balloon border.
   * @param borderColor the balloon border color
   */
  public void setBalloonBorderColor (Color borderColor) {
    balloonBorderColor = borderColor;
  }
  
  /**
   * Sets the color to use for the balloon background.
   * @param backgroundColor the balloon background color
   */
  public void setBalloonBackgroundColor (Color backgroundColor) {
    balloonBackgroundColor = backgroundColor;
  }
  
  /**
   * Sets the color to use for the balloon text.
   * @param textColor the balloon text color
   */
  public void setBalloonTextColor (Color textColor) {
    balloonTextColor = textColor;
  }
  
  /**
   * Sets the time in milliseconds that the balloon is visible before
   * disappearing.  This is the maximum time that the balloon will be visible,
   * as other events can also make the balloon disappear.
   * @param duration the time in milliseconds
   */
  public void setBalloonDuration (Integer duration) {
    balloonDuration = duration;
  }
  
  /*
   * (non-Javadoc)
   * @see javax.swing.JTextField#createDefaultModel()
   */
  protected Document createDefaultModel () {
    return new FilteredTextFieldDocument();
  }
  
  /*
   * This class defines the document used for the FilteredTextField.
   */
  private class FilteredTextFieldDocument extends PlainDocument {
    /*
     * Create a FilteredTextFieldDocument.
     */
    public FilteredTextFieldDocument () {
      addDocumentListener(new FilteredTextFieldEar());
    }
    
    /*
     * (non-Javadoc)
     * @see javax.swing.text.PlainDocument#insertString(
     * int, java.lang.String, javax.swing.text.AttributeSet)
     */
    public void insertString (int offset, String str, AttributeSet a)
        throws BadLocationException
    {
      if (balloon != null && BalloonTipManager.isShowing()) {
        if (balloonType == VALID_BALLOON) {
          balloon.hide();
        }
      }
      StringBuilder buffer =
        new StringBuilder(FilteredTextField.this.getText());
      if (offset >= 0 && offset <= buffer.length()) {
        buffer.insert(offset, str);
        String strBuf = buffer.toString();
        
        if (buffer.length() > maximumLength) {
          if (balloon != null && BalloonTipManager.isShowing()) {
            if (balloonType == LENGTH_BALLOON) {
              BalloonTipManager.restartTimer();
              return;
            }
            else {
              balloon.hide();
            }
          }
          balloon = BalloonTipManager.getBalloonTip(FilteredTextField.this,
            "The number of characters must be less than or equal to " +
              maximumLength, 0, 0, balloonDuration, balloonBorderColor,
              balloonBackgroundColor, balloonTextColor);
          balloon.show();
          balloonType = LENGTH_BALLOON;
          return;
        }
        
        if (strBuf == null || strBuf.equals("")) {
          remove(0, getLength());
          super.insertString(0, "", null);
          if (balloon != null && BalloonTipManager.isShowing()) {
            balloon.hide();
          }
          return;
        }

        if (allowable.contains(str.charAt(0))) {
          super.insertString(offset, str, a);
          if (balloon != null && BalloonTipManager.isShowing()) {
            balloon.hide();
          }
        }
        else {
          if (balloon != null && BalloonTipManager.isShowing()) {
            if (balloonType == ENTRY_BALLOON) {
              BalloonTipManager.restartTimer();
              return;
            }
            else {
              balloon.hide();
            }
          }
          balloon = BalloonTipManager.getBalloonTip(FilteredTextField.this,
            entryError, 0, 0, balloonDuration, balloonBorderColor,
            balloonBackgroundColor, balloonTextColor);
          balloon.show();
          balloonType = ENTRY_BALLOON;
        }
      }
    }
    
    /*
     * This listener class is needed to catch character removal events.
     */
    private class FilteredTextFieldEar implements DocumentListener {
      /*
       * (non-Javadoc)
       * @see javax.swing.event.DocumentListener#insertUpdate(
       * javax.swing.event.DocumentEvent)
       */
      public void insertUpdate (DocumentEvent e) {/* N/A */}
      
      /*
       * (non-Javadoc)
       * @see javax.swing.event.DocumentListener#removeUpdate(
       * javax.swing.event.DocumentEvent)
       */
      public void removeUpdate (DocumentEvent e) {
        if (balloon != null && BalloonTipManager.isShowing())
        {
          balloon.hide();
        }
      }
      
      /*
       * (non-Javadoc)
       * @see javax.swing.event.DocumentListener#changedUpdate(
       * javax.swing.event.DocumentEvent)
       */
      public void changedUpdate (DocumentEvent e) {/* N/A */}
    }
  }

  /*
   * This listener class is used to determine whether the string is valid based
   * on a regular expression.  The validation is tested when leaving the text
   * field, and notification is performed when returning to the text field.
   */
  private class ValidationEar extends FocusAdapter {
    /*
     * (non-Javadoc)
     * @see java.awt.event.FocusAdapter#focusLost(java.awt.event.FocusEvent)
     */
    public void focusLost (FocusEvent e) {
      String entered = getText().trim();
      if (!entered.matches(validRegex)) {
        if (balloon != null) {
          balloon.hide();
        }
        setBorder(
          BorderFactory.createCompoundBorder(RED_BORDER, defaultBorder));
        isValid = false;
      }
      else {
        setBorder(defaultBorder);
        isValid = true;
      }
    }
    
    /*
     * (non-Javadoc)
     * @see java.awt.event.FocusAdapter#focusGained(java.awt.event.FocusEvent)
     */
    public void focusGained (FocusEvent e) {
      if (!isValid) {
        balloon = BalloonTipManager.getBalloonTip(FilteredTextField.this,
          validError, 0, 0, balloonDuration, balloonBorderColor,
          balloonBackgroundColor, balloonTextColor);
        balloon.show();
        balloonType = VALID_BALLOON;
      }
    }
  }

  /*
   * A main entry point to test the FilteredTextField.
   * @param args application arguments
   */
  public static void main (String[] args) {
    try {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    }
    catch (Exception e) {
      e.printStackTrace();
    }
    JFrame jframe = new JFrame("Balloon Tips on FilteredTextField");
    jframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    jframe.setSize(400, 75);
    jframe.setLocation(400, 400);
    JPanel jpanel = new JPanel();
    jpanel.setLayout(new BorderLayout());
    FilteredTextField ftfield = new FilteredTextField(10);
    ftfield.setCharacters(LOWERCASE_CHARS);
    ftfield.addCharacter('-');
    ftfield.addCharacter('_');
    ftfield.addCharacter(' ');
    ftfield.setMaximumLength(10);
    ftfield.setEntryError(
      "Only lower case letters, hyphens, underscores, and spaces allowed.");
    ftfield.setValidRegex("^a+[a-z-_ ]*");
    ftfield.setValidError("The string must begin with the letter 'a'.");
    jpanel.add(new JLabel("Type some text into either field"), BorderLayout.NORTH);
    jpanel.add(ftfield, BorderLayout.CENTER);
    jpanel.add(new FilteredTextField(10), BorderLayout.SOUTH);
    jframe.getContentPane().add(jpanel);
    jframe.setVisible(true);
  }
}
