package nxm.sys.libg;

import java.awt.Button;
import java.awt.Canvas;
import java.awt.Checkbox;
import java.awt.Choice;
import java.awt.Component;
import java.awt.Container;
import java.awt.Label;
import java.awt.List;
import java.awt.Rectangle;
import java.awt.Scrollbar;
import java.awt.TextComponent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.TextEvent;
import java.awt.event.TextListener;
import java.beans.Expression;
import java.lang.reflect.Method;
import java.util.EventListener;
import java.util.EventObject;

import javax.swing.AbstractButton;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JFileChooser;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JList;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JProgressBar;
import javax.swing.JRootPane;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.JToolTip;
import javax.swing.JTree;
import javax.swing.JViewport;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.basic.BasicInternalFrameTitlePane;
import javax.swing.table.JTableHeader;

import nxm.sys.inc.Keyable;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.Widget;
import nxm.sys.inc.Widget.WidgetListener;
import nxm.sys.lib.Message;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Util;

/** This class provides the ability to wrap a Swing/AWT component so that it has some of the same
    basic behaviors as a NeXtMidas widget. When a {@link Widget} is provided, this class uses the
    methods defined in the {@link Widget} interface when wrapping the class; for example:
    <pre>
      MACRO                     JAVA
      -------------------------------------------------------------
      res gc.&lt;name&gt;.value &lt;val&gt;   {@link Widget#setWidgetValue setWidgetValue(&lt;val&gt;)}
      res gc.&lt;name&gt;.value         {@link Widget#getWidgetValue getWidgetValue()}
      res gc.&lt;name&gt;.action &lt;val&gt;  {@link Widget#setWidgetAction setWidgetAction(&lt;val&gt;)}

      MESSAGES
      -------------------------------------------------------------
      &lt;event to specified Java listener&gt;    --&gt; Macro's processMessage ...
    </pre>
    This makes the widget behave as if it was a {@link GWidget} <br>
    <br>
    For cases where a generic AWT/Swing component is provided that does not implement {@link Widget},
    an attempt is made to support most of the features via reflection. This works fine for many basic
    widgets (like a {@link JButton}) but may be unsatisfactory for many cases. In such an event, the
    component can just be extended such that it implements {@link Widget}.
    <br>
    In general this class is only used by the nxm.sys.prim.panel class when it is loading
    up a new panel class. Users should not call this class directly.

    @see Widget
    @see GWidget

    @author  J. Czechowski IV
    @since   NeXtMidas 1.9.2
 */
public final class WidgetWrapper implements Keyable, ActionListener, ChangeListener, ItemListener,
                                            AdjustmentListener, TextListener, WidgetListener {

  /** The recognized keys. */
  private static final String[] KEYS = { "V", "VALUE", "POS", "POSITION", "THEME", "TITLE" };


  private final Component      comp;     // The Swing/AWT component that provides the GUI for this.
  private final Widget         widget;   // The underlying widget.
  private final MessageHandler mh;       // The NeXtMidas message handler for the widget.
  private final String         name;     // The name of the widget.
  private final Object         data;     // The widget's data (for message processing).
  private final Object         tid;      // The To ID (for message processing).
  private final Object         fid;      // The From ID (for message processing).

  private static final boolean DEF_SEND_MSG = true;
  private boolean              sendMsg      = DEF_SEND_MSG;  // whether or not to send a message back to the macro (Bug 2309)

  /** Creates a new instance.
      @param widget The widget this class is a wrapper for.
      @param mh     The message handler (usually the macro).
      @param name   The name of the widget (usually the variable name of the widget).
      @param data   The data to send in messages from this widget.
      @param tid    The "To ID" to use for messages from this widget.
      @param fid    The "From ID" to use for messages from this widget.
   */
  public WidgetWrapper (Widget widget, MessageHandler mh, String name,
                        Object data, Object tid, Object fid) {
    this(widget, widget.getWidgetComponent(), mh, name, data, tid, fid);
  }

  /** Creates a new instance.
      @param comp   The component this class is a wrapper for.
      @param mh     The message handler (usually the macro).
      @param name   The name of the widget (usually the variable name of the widget).
      @param data   The data to send in messages from this widget.
      @param tid    The "To ID" to use for messages from this widget.
      @param fid    The "From ID" to use for messages from this widget.
   */
  public WidgetWrapper (Component comp, MessageHandler mh, String name,
                        Object data, Object tid, Object fid) {
    this(null, comp, mh, name, data, tid, fid);
  }

  /** Creates a new instance.
      @param widget The widget this class is a wrapper for.
      @param comp   The component this class is a wrapper for.
      @param mh     The message handler (usually the macro).
      @param name   The name of the widget (usually the variable name of the widget).
      @param data   The data to send in messages from this widget.
      @param tid    The "To ID" to use for messages from this widget.
      @param fid    The "From ID" to use for messages from this widget.
   */
  private WidgetWrapper (Widget widget, Component comp, MessageHandler mh, String name,
                         Object data, Object tid, Object fid) {
    if ((widget == null) && (comp instanceof Widget)) {
      widget = (Widget)comp;
    }
    this.widget   = widget;
    this.comp     = comp;
    this.mh       = mh;
    this.name     = name;
    this.data     = data;
    this.tid      = tid;
    this.fid      = fid;

    Class<? extends EventListener> listenerClass = getListenerClass();

    if (listenerClass != null) {
      try {
        String   methName  = "add" + Util.getClassName(listenerClass);
        Object[] params    = new Object[] { this };
        Method   listener  = comp.getClass().getMethod(methName, new Class<?>[] { listenerClass });

        listener.invoke(comp, params);

      } catch (Exception e) {
        Shell.printStackTrace("ERROR: PANEL/JSETUP: Unable to add listener of type "+listenerClass.getName()
                            + " to widget "+name, e);
      }
    }
  }

  /** Determines the listener class to use.
      @return The listener class to use or <code>null</code> if none/unknown.
      @since NeXtMidas 1.9.5
   */
  private Class<? extends EventListener> getListenerClass () {
    if (widget != null) {
      return widget.getWidgetEventListener();
    }
    else if (comp instanceof JComponent) {
      if (comp instanceof AbstractButton             ) { return ActionListener.class; }
      if (comp instanceof BasicInternalFrameTitlePane) { return null;                 }
      if (comp instanceof Box                        ) { return null;                 }
      if (comp instanceof Box.Filler                 ) { return null;                 }
      if (comp instanceof JColorChooser              ) { return null;                 }
      if (comp instanceof JComboBox                  ) { return ItemListener.class;   }
      if (comp instanceof JFileChooser               ) { return null;                 }
      if (comp instanceof JInternalFrame             ) { return null;                 }
      if (comp instanceof JInternalFrame.JDesktopIcon) { return null;                 }
      if (comp instanceof JLabel                     ) { return null;                 }
      if (comp instanceof JLayeredPane               ) { return null;                 }
      if (comp instanceof JList                      ) { return null;                 }
      if (comp instanceof JMenuBar                   ) { return null;                 }
      if (comp instanceof JOptionPane                ) { return null;                 }
      if (comp instanceof JPanel                     ) { return null;                 }
      if (comp instanceof JPopupMenu                 ) { return null;                 }
      if (comp instanceof JProgressBar               ) { return null;                 }
      if (comp instanceof JRootPane                  ) { return null;                 }
      if (comp instanceof JScrollBar                 ) { return null;                 }
      if (comp instanceof JScrollPane                ) { return null;                 }
      if (comp instanceof JSeparator                 ) { return null;                 }
      if (comp instanceof JSlider                    ) { return ChangeListener.class; }
      if (comp instanceof JSpinner                   ) { return ChangeListener.class; }
      if (comp instanceof JSplitPane                 ) { return null;                 }
      if (comp instanceof JTabbedPane                ) { return ChangeListener.class; }
      if (comp instanceof JTable                     ) { return null;                 }
      if (comp instanceof JTableHeader               ) { return null;                 }
      if (comp instanceof JTextField                 ) { return ActionListener.class; }
      if (comp instanceof JTextArea                  ) { return null;                 }
      if (comp instanceof JEditorPane                ) { return null;                 }
      if (comp instanceof JToolBar                   ) { return null;                 }
      if (comp instanceof JToolTip                   ) { return null;                 }
      if (comp instanceof JTree                      ) { return null;                 }
      if (comp instanceof JViewport                  ) { return null;                 }

      Shell.warning("PANEL/JSETUP: JComponent type (" + comp.getClass().getName()+") not "
                  + "recognized by NeXtMidas, no messages will be made for component "+name+". "
                  + "Have "+name+" implement nxm.sys.inc.Widget to avoid this warning.");
      return null;

    } else {
      if (comp instanceof Button        ) { return ActionListener.class;     }
      if (comp instanceof Canvas        ) { return null;                     }
      if (comp instanceof Checkbox      ) { return ItemListener.class;       }
      if (comp instanceof Choice        ) { return ItemListener.class;       }
      if (comp instanceof Container     ) { return null;                     }
      if (comp instanceof Label         ) { return null;                     }
      if (comp instanceof List          ) { return ItemListener.class;       }
      if (comp instanceof Scrollbar     ) { return AdjustmentListener.class; }
      if (comp instanceof TextComponent ) { return TextListener.class;       }

      Shell.warning("PANEL/JSETUP: Component type (" + comp.getClass().getName()+") not "
                  + "recognized by NeXtMidas, no messages will be made for component "+name+". "
                  + "Have "+name+" implement nxm.sys.inc.Widget to avoid this warning.");
      return null;
    }
  }


  /** Sets the component's value as if the user made the change graphically.
      @param value The new value for the widget.
   */
  public void setAction (Object value) {
    if (widget != null) widget.setWidgetAction(value);
    else                setValue(value);
  }

  /** Sets the component's parent. Currently this does nothing.
      @param value The new parent.
   */
  public void setParent (Object value) { }

  /** Sets the component's value.
      @param value The new value.
   */
  public void setValue (Object value) {
    setValue (value, true);
  }

  /** Sets the component's value.
      @param value The new value.
      @param sendMsg Whether or not to send a message back to the macro
   */
  public void setValue (Object value, boolean sendMsg) {
    this.sendMsg = sendMsg;  // added for Bug 2309
    if (widget != null) {
      widget.setWidgetValue(value);
    }
    else if (comp instanceof JTabbedPane) {
      JTabbedPane tabbedPane = (JTabbedPane)comp;
      String      str        = value.toString().toUpperCase();
      int         index      = -1;

      for (int i = 0; (index < 0) && (i < tabbedPane.getTabCount()); i++) {
        if (tabbedPane.getTitleAt(i).equals(str)) {
          index = i;
        }
      }
      if (index >= 0) {
        tabbedPane.setSelectedIndex(index);
      }
      else {
        throw new MidasException("WidgetWrapper: Can not set tab to '"+str+"' for "+name+".");
      }
    }
    else if (comp != null) {
      double fp = 0;
      long   in = 0;

      final Object[]  val  = new Object[]  { value };
      final String[]  valS = new String[]  { "" + value };
      final Boolean[] valB = new Boolean[] { Boolean.valueOf(StringUtil.isTrue(valS[0])) };

      try {fp = Double.parseDouble(valS[0]);} catch (Exception e) {/* Ignore. */}
      try {in = Long.parseLong(    valS[0]);} catch (Exception e) {/* Ignore. */}

      final Number[] valD = new Number[] { Double.valueOf(fp)       };
      final Number[] valL = new Number[] { Long.valueOf(in)         };
      final Number[] valF = new Number[] { Float.valueOf((float)fp) };
      final Number[] valI = new Number[] { Integer.valueOf((int)in) };


      Expression[] expressions = new Expression[] {
        new Expression(comp, "setValue",        val ),
        new Expression(comp, "setValue",        valS),
        new Expression(comp, "setValue",        valB),
        new Expression(comp, "setValue",        valD),
        new Expression(comp, "setValue",        valL),
        new Expression(comp, "setValue",        valF),
        new Expression(comp, "setValue",        valI),

        new Expression(comp, "setSelected",     valB),

        new Expression(comp, "setText",         val ),
        new Expression(comp, "setText",         valS),

        new Expression(comp, "setSelectedItem", val )
      };

      executeExp(expressions);
    }
    this.sendMsg = DEF_SEND_MSG;  // set this back so the next message is not affected
  }

  /** Gets the component's value.
   *  @return component's value */
  public Object getValue () {
    Object value = null;

    if (widget != null) {
      value = widget.getWidgetValue();
    }
    else if (comp instanceof JTabbedPane) {
      JTabbedPane tabbedPane = (JTabbedPane)comp;
      value = tabbedPane.getTitleAt(tabbedPane.getSelectedIndex());
    }
    else if (comp != null) {
      Expression[] expressions = new Expression[] {
        new Expression(comp, "getValue",        new Object[0]),
        new Expression(comp, "isSelected",      new Object[0]),
        new Expression(comp, "getText",         new Object[0]),
        new Expression(comp, "getSelectedItem", new Object[0])
      };

      value = executeExp(expressions);
    }
    return value;
  }

  /** Sets the component's title.
      @param value The new title.
   */
  public void setTitle (Object value) {
    if (widget   != null) {
      widget.setWidgetTitle(value);
    }
    else if (comp != null) {
      Object[]  val  = new Object[]  { value };
      String[]  valS = new String[]  { "" + value };

      Expression[] expressions = new Expression[] {
        new Expression(comp, "setTitle",    val ),
        new Expression(comp, "setTitle",    valS),

        new Expression(comp, "setName",     val ),
        new Expression(comp, "setName",     valS)
      };
      executeExp(expressions);
    }
  }

  /** Gets the component's title.
   *  @return the component's title
   */
  public Object getTitle () {
    Object title = null;
    if (widget != null) {
      title = widget.getWidgetTitle();
    }
    else if (comp != null) {
      Expression[] expressions = new Expression[] {
        new Expression(comp, "getTitle",    new Object[0]),
        new Expression(comp, "getName",     new Object[0])
      };
      title = executeExp(expressions);
    }
    return title;
  }

  /** Sets the component's position.
      @return The position.
   */
  public Object getPosition () {
    if (comp == null) return null;
    Rectangle bounds = comp.getBounds();
    return new MBox(bounds.x, bounds.y, bounds.width, bounds.height);
  }

  /** Sets the component's position.
      @param box The new position.
   */
  public void setPosition (MBox box) {
    if (comp != null) comp.setBounds(box.x, box.y, box.w, box.h);
  }

  /** Refreshes the component. */
  public void refresh() {
    if (comp != null) comp.repaint();
  }

  @Override
  public Object getKey (String key) {
    if (key.equals("V"))      return getValue();
    if (key.equals("VALUE"))  return getValue();
    if (key.equals("POS"))    return getPosition();
    if (key.equals("PARENT")) return null;
    if (key.equals("THEME"))  return null;
    if (key.equals("TITLE"))  return getTitle();

    return null;
  }

  @Override
  public String[] getKeys() { return KEYS; }

  @Override
  public Object setKey (String key, Object value) {
    if (key.equals("V"))       { setValue(value);              return value; }
    if (key.equals("VALUE"))   { setValue(value);              return value; }
    if (key.equals("ACTION"))  { setAction(value);             return value; }
    if (key.equals("POS"))     { setPosition((MBox)value);     return value; }
    if (key.equals("PARENT"))  { setParent(value);             return value; }
    if (key.equals("TITLE"))   { setTitle(value);              return value; }
    if (key.equals("SHOW"))    { comp.setVisible(true);        return value; }
    if (key.equals("REFRESH")) { refresh();                    return value; }

    return null;
  }

  /** This will invoke the first of the list of methods with the given parameters.
      TBD: Future parameter: hasReturn Indicates if the expression has a return value.
      @param exps      The possible expressions.
      @return The results of executing the expression (may return <code>null</code>) or
              <code>null</code> if none are invoked, or <code>null</code> if <code>hasReturn</code>
              is <code>false</code>.
   */
  private static Object executeExp (Expression[] exps) {
    for (int i = 0; i < exps.length; i++) {
      try {
        return exps[i].getValue();
      } catch (Exception e) {
        /* Ignore */
      }
    }
    return null;
  }

  /** Gets a brief description of the object.
      @return A brief description of the object.
   */
  @Override
  public String toString () {
    String clazz = (comp == null)? "Unknown Widget": comp.getClass().getName();

    return "WidgetWrapper for a '" + clazz + "' named '" + name + "'";
  }

  /** Gets the underlying widget.  USE WITH CARE!!!
      @return The underlying widget.
      @since NeXtMidas 1.9.5
   */
  public Object getWidget () {
    return (widget != null)? (Object)widget : (Object)comp;
  }

  /** Sends an event triggered message. */
  private void sendMessage (EventObject e) {
    Object d = (data != null)? data: getValue();

    if ( (mh != null) && sendMsg) {  // check for sendMsg added for Bug 2309
      Table quals = new Table();
      quals.put("EVENT", e);
      mh.processMessage(new Message(name, 0, d, tid, fid, quals)); // add EventObject to QUALS Table under "EVENT" key for BUG 2310
      // mh.processMessage(new Message(name, 0, d, tid, fid));     // ORIG prior to 2.9.1
    }
  }

  @Override
  public void actionPerformed (ActionEvent e)            { sendMessage(e); }
  @Override
  public void itemStateChanged (ItemEvent e)             { sendMessage(e); }
  @Override
  public void adjustmentValueChanged (AdjustmentEvent e) { sendMessage(e); }
  @Override
  public void textValueChanged (TextEvent e)             { sendMessage(e); }
  @Override
  public void stateChanged (ChangeEvent e)               { sendMessage(e); }
  @Override
  public void widgetUpdated (EventObject e)              { sendMessage(e); }
}
