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

import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.util.MissingResourceException;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.KeyStroke;

import jnpad.GUIUtilities;
import jnpad.JNPadFrame;
import jnpad.action.JNPadActions.Group;
import jnpad.config.Accelerators;
import jnpad.config.Config;
import jnpad.util.Platform;
import jnpad.util.Utilities;

/**
 * The Class JNPadAction.
 *
 * @version 0.3
 * @since   jNPad v0.1
 */
public abstract class JNPadAction extends AbstractAction implements ItemListener {
  /** The Constant LABEL. */
  public static final String  LABEL                           = "jNPad-Label";                                //$NON-NLS-1$

  /** The Constant IS_STATE. */
  public static final String  IS_STATE                        = "jNPad-State";                                //$NON-NLS-1$

  /** The Constant GROUP. */
  public static final String  GROUP                           = "jNPad-Group"; //$NON-NLS-1$

  /** The Constant KEY_BINDING. */
  public static final String  KEY_BINDING                     = "jNPad-KeyBinding";                           //$NON-NLS-1$

  /** The Constant BUTTON_TOOLTIP. */
  public static final String  BUTTON_TOOLTIP                  = "jNPad-ButtonToolTip";                        //$NON-NLS-1$

  /** The Constant BUTTON_TEXT. */
  public static final String  BUTTON_TEXT                     = "jNPad-ButtonText";                           //$NON-NLS-1$
  
  /** The jNPad's frame. */
  protected JNPadFrame        jNPad;

  private boolean             performActionInSeparateThread;

  private boolean             isItemStateChangedEnabled       = true;

  private static boolean      isGlobalItemStateChangedEnabled = true;

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

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

  /**
   * Instantiates a new <code>JNPadAction</code>.
   *
   * @param jNPad the jNPad's frame
   * @param name the name
   * @param group the group
   */
  protected JNPadAction(JNPadFrame jNPad, String name, Group group) {
    super(name);

    this.jNPad = jNPad;
    
    // set group
    setGroup(group);
  }

  /**
   * Instantiates a new <code>JNPadAction</code>.
   *
   * @param jNPad the jNPad's frame
   * @param name the name
   * @param group the group
   * @param defaultAccelerator the default accelerator
   * @param picture the picture
   */
  protected JNPadAction(JNPadFrame jNPad, String name, Group group, KeyStroke defaultAccelerator, String picture) {
    this(jNPad, name, group);
    
    // set label & mnemonic
    setActionLabel(ActionBundle.getString(name.concat(".label"))); //$NON-NLS-1$
    // set description
    setDescription(ActionBundle.getString(name.concat(".description"))); //$NON-NLS-1$

    // set icon
    if (picture != null && Config.isDefaultMode()) {
      setIcon(Utilities.EMPTY_STRING.equals(picture) ? GUIUtilities.EMPTY_ICON : GUIUtilities.loadIcon(picture));
    }

    // set shortcuts
    if (defaultAccelerator != null) {
      if (Accelerators.isUsingCompositeShortcuts()) {
        setKeyBinding(Accelerators.getShortcutLabel(name));
        setButtonToolTipText(ActionBundle.getString(name.concat(".button.tooltip"), getKeyBinding())); //$NON-NLS-1$
      }
      else if (Config.isUsingCustomShortcuts()) {
        KeyStroke ks = Accelerators.getPropAccelerator(name.concat(".shortcut"), null); //$NON-NLS-1$
        if (ks != null) {
          setAccelerator(ks);
          setButtonToolTipText(ActionBundle.getString(name.concat(".button.tooltip"), getAcceleratorText())); //$NON-NLS-1$
        }
      }
      else {
        setAccelerator(Accelerators.getPropAccelerator(name.concat(".shortcut"), defaultAccelerator)); //$NON-NLS-1$
        setButtonToolTipText(ActionBundle.getString(name.concat(".button.tooltip"), getAcceleratorText())); //$NON-NLS-1$
      }
    }
  }
  
  /**
   * Gets the label.
   *
   * @return the label
   */
  public String getLabel() {
    return (String) getValue(LABEL);
  }

  /**
   * Sets the label.
   *
   * @param label the new label
   */
  public void setLabel(String label) {
    putValue(LABEL, label);
  }

  /**
   * Sets the group.
   *
   * @param group the new group
   */
  public void setGroup(Group group) {
    putValue(GROUP, group);
  }

  /**
   * Gets the group.
   *
   * @return the group
   */
  public Group getGroup() {
    return (Group) getValue(GROUP);
  }
  
  /**
   * Gets the key binding.
   *
   * @return the key binding
   */
  public String getKeyBinding() {
    return (String) getValue(KEY_BINDING);
  }

  /**
   * Sets the key binding.
   *
   * @param keyBinding the new key binding
   */
  public void setKeyBinding(String keyBinding) {
    putValue(KEY_BINDING, keyBinding);
  }

  /**
   * Gets the button text.
   *
   * @return the button text
   */
  public String getButtonText() {
    return (String) getValue(BUTTON_TEXT);
  }

  /**
   * Sets the button text.
   *
   * @param text the new button text
   */
  public void setButtonText(String text) {
    putValue(BUTTON_TEXT, text);
  }

  /**
   * Gets the button tool tip text.
   *
   * @return the button tool tip text
   */
  public String getButtonToolTipText() {
    return (String) getValue(BUTTON_TOOLTIP);
  }

  /**
   * Sets the button tool tip text.
   *
   * @param toolTipText the new button tool tip text
   */
  public void setButtonToolTipText(String toolTipText) {
    putValue(BUTTON_TOOLTIP, toolTipText);
  }
  
  /**
   * Checks if is state action.
   *
   * @return true, if is state action
   */
  public boolean isStateAction() {
    Boolean state = (Boolean) getValue(IS_STATE);
    return state != null && state;
  }

  /**
   * Sets the state action.
   */
  public void setStateAction() {
    setStateAction(true);
  }

  /**
   * Sets the state action.
   *
   * @param state the new state action
   */
  public void setStateAction(boolean state) {
    putValue(IS_STATE, Boolean.valueOf(state));
  }

  /**
   * Gets the name.
   *
   * @return the name
   */
  public String getName() {
    return (String) getValue(NAME);
  }

  /**
   * Sets the name.
   *
   * @param name the new name
   */
  public void setName(String name) {
    putValue(NAME, name);
  }

  /**
   * Gets the mnemonic.
   *
   * @return the mnemonic
   */
  public int getMnemonic() {
    Integer value = (Integer) getValue(MNEMONIC_KEY);
    if (value != null) {
      return value;
    }
    return '\0';
  }

  /**
   * Sets the mnemonic.
   *
   * @param mnemonic the new mnemonic
   */
  public void setMnemonic(String mnemonic) {
    if (mnemonic != null && mnemonic.length() > 0) {
      putValue(MNEMONIC_KEY, Integer.valueOf(mnemonic.charAt(0)));
    }
  }

  /**
   * Sets the mnemonic.
   *
   * @param mnemonic the new mnemonic
   */
  public void setMnemonic(int mnemonic) {
    putValue(MNEMONIC_KEY, Integer.valueOf(mnemonic));
  }

  /**
   * Sets the mnemonic.
   *
   * @param mnemonic the new mnemonic
   */
  public void setMnemonic(char mnemonic) {
    int vk = mnemonic;
    if (vk >= 'a' && vk <= 'z') {
      vk -= ('a' - 'A');
    }
    setMnemonic(vk);
  }

  /**
   * Gets the tool tip text.
   *
   * @return the tool tip text
   */
  public String getToolTipText() {
    return (String) getValue(SHORT_DESCRIPTION);
  }

  /**
   * Sets the tool tip text.
   *
   * @param toolTipText the new tool tip text
   */
  public void setToolTipText(String toolTipText) {
    putValue(SHORT_DESCRIPTION, toolTipText);
  }

  /**
   * Gets the description.
   *
   * @return the description
   */
  public String getDescription() {
    return (String) getValue(LONG_DESCRIPTION);
  }

  /**
   * Sets the description.
   *
   * @param description the new description
   */
  public void setDescription(String description) {
    putValue(LONG_DESCRIPTION, description);
  }

  /**
   * Checks if is selected.
   *
   * @return true, if is selected
   */
  public boolean isSelected() {
    Boolean value = (Boolean) getValue(SELECTED_KEY);
    return value != null && value;
  }

  /**
   * Sets the selected.
   *
   * @param b the new selected
   */
  public void setSelected(boolean b) {
    putValue(SELECTED_KEY, b);
  }

  /**
   * Gets the icon.
   *
   * @return the icon
   */
  public Icon getIcon() {
    return (Icon) getValue(SMALL_ICON);
  }

  /**
   * Sets the icon.
   *
   * @param icon the new icon
   */
  public void setIcon(Icon icon) {
    putValue(SMALL_ICON, icon);
  }

  /**
   * Gets the large icon.
   *
   * @return the large icon
   */
  public Icon getLargeIcon() {
    return (Icon) getValue(LARGE_ICON_KEY);
  }

  /**
   * Sets the large icon.
   *
   * @param icon the new large icon
   */
  public void setLargeIcon(Icon icon) {
    putValue(LARGE_ICON_KEY, icon);
  }
  
  /**
   * Gets the accelerator.
   *
   * @return the accelerator
   */
  public KeyStroke getAccelerator() {
    return (KeyStroke) getValue(ACCELERATOR_KEY);
  }

  /**
   * Sets the accelerator.
   *
   * @param accelerator the new accelerator
   */
  public void setAccelerator(KeyStroke accelerator) {
    putValue(ACCELERATOR_KEY, accelerator);
  }

  /**
   * Accelerators equal.
   *
   * @param ks1 KeyStroke
   * @param ks2 KeyStroke
   * @return true, if successful
   */
  protected boolean acceleratorsEqual(KeyStroke ks1, KeyStroke ks2) {
    return ks1.getKeyChar() == ks2.getKeyChar()
        && ks1.getKeyCode() == ks2.getKeyCode()
        && ks1.getModifiers() == ks2.getModifiers();
  }

  /**
   * Checks if is accelerator.
   *
   * @param keyStroke the key stroke
   * @return true, if is accelerator
   */
  public boolean isAccelerator(KeyStroke keyStroke) {
    KeyStroke accelerator = getAccelerator();
      return accelerator != null && acceleratorsEqual(accelerator, keyStroke);
  }

  /**
   * Gets the accelerator text.
   *
   * @return the accelerator text
   */
  public String getAcceleratorText() {
    KeyStroke accelerator = getAccelerator();
    if (accelerator == null) {
      return null;
    }
    String text = KeyEvent.getKeyText(accelerator.getKeyCode());
    int modifiers = accelerator.getModifiers();
    if (modifiers != 0) {
      text = KeyEvent.getKeyModifiersText(modifiers) + "+" + text; //$NON-NLS-1$
    }
    return text;
  }

  /**
   * Dispose.
   */
  public void dispose() {
    for (PropertyChangeListener listener : getPropertyChangeListeners()) {
      removePropertyChangeListener(listener);
    }
  }

  /**
   * Perform action in separate thread.
   *
   * @return true, if successful
   */
  public boolean performActionInSeparateThread() {
    return performActionInSeparateThread;
  }

  /**
   * Sets the perform action in separate thread.
   *
   * @param performActionInSeparateThread the new perform action in separate thread
   */
  public void setPerformActionInSeparateThread(boolean performActionInSeparateThread) {
    this.performActionInSeparateThread = performActionInSeparateThread;
  }

  /**
   * Sets the item state changed enabled.
   *
   * @param b the new item state changed enabled
   */
  public void setItemStateChangedEnabled(boolean b) {
    isItemStateChangedEnabled = b;
  }

  /**
   * Checks if is item state changed enabled.
   *
   * @return true, if is item state changed enabled
   */
  public boolean isItemStateChangedEnabled() {
    return isItemStateChangedEnabled;
  }

  /**
   * Sets the global item state changed enabled.
   *
   * @param b the new global item state changed enabled
   */
  public static void setGlobalItemStateChangedEnabled(boolean b) {
    isGlobalItemStateChangedEnabled = b;
  }

  /**
   * Checks if is global item state changed enabled.
   *
   * @return true, if is global item state changed enabled
   */
  public static boolean isGlobalItemStateChangedEnabled() {
    return isGlobalItemStateChangedEnabled;
  }

  /**
   * Check selection.
   *
   * @param a the action
   * @param b the boolean
   */
  public static void checkSelection(JNPadAction a, boolean b) {
    setGlobalItemStateChangedEnabled(false);
    if (b != a.isSelected()) {
      a.setSelected(b);
    }
    setGlobalItemStateChangedEnabled(true);
  }

  /**
   * Perform action.
   */
  public void performAction() {
    //empty
  }

  /**
   * Handle state changed.
   *
   * @param e the ItemEvent
   */
  public void handleStateChanged(final ItemEvent e) {
    //empty
  }

  /**
   * Item state changed.
   *
   * @param e the item event
   * @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
   */
  @Override
  public void itemStateChanged(final ItemEvent e) {
    if (!isStateAction()) {
      return;
    }
    
    setSelected(ItemEvent.SELECTED == e.getStateChange());

    if (!isItemStateChangedEnabled() || !isGlobalItemStateChangedEnabled()) {
      return;
    }

    handleStateChanged(e);
  }

  /**
   * Action performed.
   *
   * @param e the action event
   * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
   */
  @Override
  public void actionPerformed(final ActionEvent e) {
    if (isStateAction()) {
      return;
    }

    if (performActionInSeparateThread()) {
      Runnable r = new Runnable() {
        public void run() {
          performAction();
        }
      };
      new Thread(r).start();
    }
    else {
      performAction();
    }
  }

  /**
   * Gets the current event modifiers.
   *
   * @return the current event modifiers
   */
  protected static int getCurrentEventModifiers() {
    int modifiers = 0;
    AWTEvent currentEvent = EventQueue.getCurrentEvent();
    if (currentEvent instanceof InputEvent) {
      modifiers = ( (InputEvent) currentEvent).getModifiers();
    }
    else if (currentEvent instanceof ActionEvent) {
      modifiers = ( (ActionEvent) currentEvent).getModifiers();
    }
    return modifiers;
  }

  /**
   * Sets the action label.
   *
   * @param label the new action label
   */
  public void setActionLabel(String label) {
    if (label == null) {
      return;
    }

    int i = GUIUtilities.findMnemonicAmpersand(label);

    if (i < 0) {
      // no '&' - don't set the mnemonic
      setLabel(label);
      setMnemonic(0);
    }
    else {
      setLabel(label.substring(0, i) + label.substring(i + 1));
      //#67807 no mnemonics on macosx
      if (Platform.isMac) {
        setMnemonic(0);
      }
      else {
        char ch = label.charAt(i + 1);
        if (label.startsWith("<html>")) { //$NON-NLS-1$
          // Workaround for JDK bug #6510775
          setLabel(label.substring(0, i) + "<u>" + ch + "</u>" + label.substring(i + 2)); //$NON-NLS-1$//$NON-NLS-2$
          i += 3; // just in case it gets fixed
        }
        if ( ( (ch >= 'A') && (ch <= 'Z')) || ( (ch >= 'a') && (ch <= 'z')) || ( (ch >= '0') && (ch <= '9'))) {
          // it's latin character or arabic digit,
          // setting it as mnemonics
          setMnemonic(ch);
        }
        else {
          // it's non-latin, getting the latin correspondance
          try {
            int latinCode = GUIUtilities.getLatinKeycode(ch);
            setMnemonic(latinCode);
          }
          catch (MissingResourceException ex) {
            //ignored
          }
        }
      }
    }
  }

  /**
   * To string.
   *
   * @return the string
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder("["); //$NON-NLS-1$
    sb.append(this.getClass().toString());
    sb.append(":"); //$NON-NLS-1$
    try {
      Object[] keys = getKeys();
      for (int i = 0; i < keys.length; i++) {
        sb.append(keys[i]);
        sb.append('=');
        sb.append(getValue( (String) keys[i]).toString());
        if (i < keys.length - 1) {
          sb.append(',');
        }
      }
      sb.append(']');
    }
    catch (Exception ex) {
      LOGGER.warning(ex.getMessage());
    }
    return sb.toString();
  }

}
