package sale;

import java.util.*;

import javax.swing.*;
import java.awt.*;

/**
  * A MenuSheet consisting of {@link MenuSheetObject MenuSheetObjects}.
  *
  * <p>MenuSheets represent menus, abstracting from the form they are being displayed in. A MenuSheet could be
  * displayed as a JMenuBar as well as a JMenu or JPopUpMenu. Independently of this it will always behave in
  * the same way from the point of view of the applicion developer.</p>
  *
  * @author Steffen Zschaler
  * @version 2.0 20/05/1999
  * @since v1.0
  */
public class MenuSheet extends MenuSheetObject {

  /**
    * The JMenu peer, if any.
    */
  protected transient JMenu m_jmPeer = null;

  /**
    * The JMenuBar peer, if any.
    */
  protected transient JMenuBar m_jmbBarPeer = null;

  /**
    * The monitor used to synchronize access to the peers.
    */
  private transient Object m_oPeerLock;
  /**
    * Return the monitor used to synchronized access to the peers.
    *
    * @override Never
    */
  protected final Object getPeerLock() {
    if (m_oPeerLock == null) {
      m_oPeerLock = new Object();
    }

    return m_oPeerLock;
  }

  /**
    * The MenuSheet that has been merged into this one, if any.
    *
    * @serial
    */
  protected MenuSheet m_msMerged = null;

  /**
    * The tag before which the merged menu was inserted.
    *
    * @serial
    */
  protected String m_sMergedBefore = null;

  /**
    * The peer index at which the merged menu was inserted.
    *
    * @serial
    */
  protected int m_nMergedAt = -1;

  /**
    * The MenuSheet into which this MenuSheet was merged, if any.
    *
    * @serial
    */
  protected MenuSheet m_msMergeParent = null;

  /**
    * The items of this MenuSheet.
    *
    * @serial
    */
  protected LinkedList m_lmsoItems;

  /**
    * The Images associated with the icons of this MenuSheet( [0]:DefaultImage, [1]:PressedImage,
    * [2]:DisabledImage, [3]:PressedDiabledImage ).
    *
    * @serial
    */
  protected Image m_aiImages[] = null;

  /**
    * The Mnemonic of this MenuSheet.
    *
    * @serial
    */
  protected char m_cMnemonic;

  private void writeObject (java.io.ObjectOutputStream oos)
    throws java.io.IOException {
    util.Debug.print ("Starting to write menu sheet: \"" + getCaption () + "\" <" + getTag () + ">", -1);

    oos.defaultWriteObject ();

    util.Debug.print ("Done writing menu sheet: \"" + getCaption () + "\" <" + getTag () + ">", -1);
  }

  // Helpmethod for setting an ImageIcon
  private void setIcon (ImageIcon iiImageIcon, int nIndex) {
    if (m_aiImages == null)
      m_aiImages = new Image[4];

    m_aiImages[nIndex] = iiImageIcon.getImage();

    synchronized (getPeerLock()) {
      if (m_jmPeer != null) {
        switch (nIndex) {
          case DEFAULT_IMAGE :
            m_jmPeer.setIcon (iiImageIcon);
            break;
          case SELECTED_IMAGE :
            m_jmPeer.setSelectedIcon (iiImageIcon);
            break;
          case DISABLED_IMAGE :
            m_jmPeer.setDisabledIcon (iiImageIcon);
            break;
          case DISABLED_SELECTED_IMAGE :
            m_jmPeer.setDisabledSelectedIcon (iiImageIcon);
            break;
        }

        m_jmPeer.validate();
      }
    }
  }

  /**
    * Create a new MenuSheet.
    *
    * @param sCaption the caption of the new MenuSheet.
    * @param sTag the tag of the new MenuSheet. If <code>null</code> a default tag will be created.
    * @param cMnemonic the mnemonic of the new MenuSheet.
    */
    public MenuSheet (String sCaption,
                      String sTag,
                      char cMnemonic) {
      super (sCaption, sTag);
      m_cMnemonic = cMnemonic;

      m_lmsoItems = new LinkedList();
    }


  /**
    * Create a new MenuSheet.
    *
    * @param sCaption the caption of the new MenuSheet.
    * @param sTag the tag of the new MenuSheet. If <code>null</code> a default tag will be created.
    */
  public MenuSheet (String sCaption,
                    String sTag) {
    this (sCaption, sTag, '\0');

    m_lmsoItems = new LinkedList();
  }

  /**
    * Create a new MenuSheet with a default tag.
    *
    * @param sCaption the caption of the new MenuSheet.
    */
  public MenuSheet (String sCaption) {
    this (sCaption, null, '\0');
  }


  /**
    * Add a MenuSheetObject to the end of this MenuSheet.
    *
    * <p>Should the MenuSheet currently be displayed, the new item will be displayed as
    * well.</p>
    *
    * @override Never
    *
    * @param mso the new MenuSheetObject.
    */
  public synchronized void add (MenuSheetObject mso) {
    if (m_msMergeParent == null) {
      // we are not merged into some other MenuSheet, so we posess the peer.
      synchronized (getPeerLock()) {
        // update peer

        if (mso.isSeparator()) {
          if (m_jmPeer != null) {
            m_jmPeer.addSeparator();
            m_jmPeer.validate();
          }
        }
        else {
          if (m_jmPeer != null) {
            m_jmPeer.add (mso.getPeer());
            m_jmPeer.validate();
          }

          if (m_jmbBarPeer != null) {
            JMenu jm = mso.getMenuPeer();
            m_jmbBarPeer.add (jm);
            if (HELP_MENU_TAG.equals (mso.getTag())) {
              m_jmbBarPeer.setHelpMenu (jm);
            }
            m_jmbBarPeer.validate();
          }
        }
      }

      // insert MenuSheetObject
      m_lmsoItems.addLast (mso); // Attention: Must be after peer update to make sure,
                                 // we don't change the list while trying to create the
                                 // peer.
    }
    else {
      // We are merged into another MenuSheet, so we don't manage the peer.

      m_lmsoItems.addLast (mso);

      // Tell merge parent to update peer.
      if (!mso.isSeparator()) {
        int nIndex = m_lmsoItems.size() - 1; // nIndex == m_lmsoItems.indexOf (mso)

        // index needs to be in peer "coordinates"
        for (Iterator i = m_lmsoItems.iterator(); i.hasNext();) {
          if (((MenuSheetObject) i.next()).isSeparator()) {
            nIndex--;
          }
        }

        m_msMergeParent.mergedAdd (mso, nIndex);
      }
    }

    // Tell new item about its new environment
    mso.setParent (this);
    mso.setVisible (isVisible());
    mso.attach (m_spAttached);
    mso.attach (m_pAttached);
  }

  /**
    * Internal method used for updating merged peers correctly.
    *
    * @override Never
    *
    * @param mso the MenuSheet that was added in the merged MenuSheet.
    * @param nIndex the peer index at which the element was added into the merged MenuSheet.
    */
  synchronized void mergedAdd (MenuSheetObject mso, int nIndex) {
    if (m_msMergeParent == null) {
      synchronized (getPeerLock()) {
        if (m_jmbBarPeer != null) {
          m_jmbBarPeer.add (mso.getMenuPeer(), nIndex + m_nMergedAt);
          m_jmbBarPeer.validate();
        }
      }
    }
    else {
      m_msMergeParent.mergedAdd (mso, nIndex + m_nMergedAt);
    }
  }

  /**
    * Remove a tagged top level item from the MenuSheet.
    *
    * <p>If an item with the given tag is found among the top level items of this
    * MenuSheet, it is removed and the removed item is returned. Otherwise, the call is
    * ignored.</p>
    *
    * <p>If the MenuSheet is currently on display, the peer will reflect the changes.</p>
    *
    * @override Never
    *
    * @param sTag the tag of the item to be removed.
    *
    * @return the removed item
    */
  public MenuSheetObject remove (String sTag) {
    MenuSheetObject msoRemove = getTaggedItem (sTag, true);

    return ((msoRemove != null)?(remove (msoRemove)):(null));
  }

  /**
    * Remove a MenuSheetObject from the MenuSheet.
    *
    * <p>If the MenuSheet is currently on display, the peer will reflect the changes.</p>
    *
    * @override Never
    *
    * @param mso the MenuSheetObject to be removed.
    *
    * @return the removed MenuSheetObject.
    */
  public synchronized MenuSheetObject remove (MenuSheetObject msoRemove) {
    // find the index of the item
    int nIndex = m_lmsoItems.indexOf (msoRemove);
    if (nIndex == -1) {
      // item not found
      return null;
    }

    if (m_msMergeParent == null) {
      // We have the peer, so we update it.
      synchronized (getPeerLock()) {
        if (m_jmPeer != null) {
          m_jmPeer.remove (nIndex); // Separators can only be removed via index
          m_jmPeer.validate();
        }

        if (!msoRemove.isSeparator()) {
          if (m_jmbBarPeer != null) {
            m_jmbBarPeer.remove (msoRemove.getMenuPeer());
            m_jmbBarPeer.validate();
          }
        }
      }
    }
    else {
      // inform MenuSheet into which we are merged
      m_msMergeParent.mergedRemove (msoRemove);
    }

    // remove item
    m_lmsoItems.remove (msoRemove);

    // set item's environment.
    msoRemove.attach ((SalesPoint) null);
    msoRemove.attach ((SaleProcess) null);
    msoRemove.setVisible (false);
    msoRemove.setParent (null);

    return msoRemove;
  }

  /**
    * Internal method used to properly update merged peers.
    *
    * @override Never
    *
    * @param mso the MenuSheetObject that was removed from the merged MenuSheet.
    */
  synchronized void mergedRemove (MenuSheetObject mso) {
    if (m_msMergeParent == null) {
      synchronized (getPeerLock()) {
        if ((m_jmbBarPeer != null) && (!mso.isSeparator())) {
          m_jmbBarPeer.remove (mso.getMenuPeer());
          m_jmbBarPeer.validate();
        }
      }
    }
    else {
      m_msMergeParent.mergedRemove (mso);
    }
  }

  /**
    * Return a <i>fail-fast</i>, readonly iterator of the items in this MenuSheet.
    *
    * <p>Fail-fast means, that this iterator will throw a <code>ConcurrentModificationException</code> when a
    * structural change occured to the underlying MenuSheet.</p>
    *
    * <p>Also, the <code>remove()</code> method will throw an <code>UnsupportedOperationException</code>, as
    * this is a readonly iterator.</p>
    *
    * @override Never
    *
    * @see ConcurrentModificationException
    */
  public Iterator iterator() {
    class I implements Iterator {
      private Iterator m_i;

      public I (Iterator i) {
        m_i = i;
      }

      public boolean hasNext() {
        return m_i.hasNext();
      }

      public Object next() {
        return m_i.next();
      }

      public void remove() {
        throw new UnsupportedOperationException ("Please use the MenuSheet's remove() methods, not the iterator's.");
      }
    }

    return new I (m_lmsoItems.iterator());
  }

  /**
    * Internal method used when merging peers.
    *
    * @override Never
    */
  synchronized void setMergeParent (MenuSheet msMergeParent) {
    m_msMergeParent = msMergeParent;
  }

  /**
    * Merge the peers of two MenuSheets. If a MenuSheet is already merged into this one,
    * it will be removed and marked invisible.
    *
    * <p>The peers of the top level MenuSheetObjects of the given MenuSheet are merged
    * into the JMenuBar peer of this MenuSheet. They will be inserted into the JMenuBar
    * peer before the MenuSheetpObject with the given tag. If no such MenuSheetObject can
    * be found in the MenuSheet they are appended to the end of the peer.</p>
    *
    * <p>Merging can always only result in a JMenuBar peer. A JMenu peer cannot be merged.
    * However, merging can be nested. I.e. it is legal, to merge a MenuSheet, into whose
    * peer another MenuSheet's peer has been merged, into a third MenuSheet.</p>
    *
    * <p>Although the peers of the two MenuSheets are merged, they stay independent with
    * respect to anything else. The MenuSheetObjects of the merged MenuSheet can still
    * only be accessed through that MenuSheet and vice-vera. Also, the attached SalesPoint
    * and SaleProcess stay independent.</p>
    *
    * <p>For merging to function correctly, you must set the created JMenuBar in your
    * JFrame. Something like this will do the trick:</p>
    * <pre>
    *   setJMenuBar (ms.mergePeers (msToMerge, "MERGE_BEFORE_THIS"));
    * </pre>
    *
    * <p>This method is usually not called directly.</p>
    *
    * @param msToMerge the MenuSheet to be merged into this one.
    * @param sBeforeTag before which tag to merge in the MenuSheet.
    *
    * @override Never
    */
  public final JMenuBar mergePeers (MenuSheet msToMerge, String sBeforeTag) {
    synchronized (getPeerLock()) {
      boolean fVisible = isVisible();

      if (fVisible) {
        setVisible (false);
      }

      if (m_msMerged != null) {
        m_msMerged.setMergeParent (null);
      }

      m_msMerged = msToMerge;
      m_sMergedBefore = sBeforeTag;
      m_nMergedAt = -1;

      if (m_msMerged != null) {
        m_msMerged.setMergeParent (this);
      }

      if (fVisible) {
        setVisible (true);
      }

      if (m_msMergeParent != null) {
        return m_msMergeParent.remergePeers();
      }
      else {
        return getMenuBar();
      }
    }
  }

  /**
    * Internal method used for proper merging of peers.
    *
    * @override Never
    */
  JMenuBar remergePeers() {
    return mergePeers (m_msMerged, m_sMergedBefore);
  }

  /**
    * Get a MenuSheetObject by its tag.
    *
    * <p>This will iterate over all MenuSheetObjects in this MenuSheet and return the
    * first one, that has the given tag.</p>
    *
    * @override Never
    *
    * @param sTag the tag to search for.
    * @param fTopLevelOnly if true, only the direct elements in this MenuSheet are searched.
    *
    * @exception ConcurrentModificationException if the structure of the MenuSheet changed
    * during the search.
    *
    * @return the first MenuSheetObject that has the given tag, if any.
    */
  public MenuSheetObject getTaggedItem (String sTag, boolean fTopLevelOnly) {
    MenuSheetObject msoReturn = super.getTaggedItem (sTag, fTopLevelOnly);
    if (msoReturn != null) {
      return msoReturn;
    }

    for (Iterator i = iterator(); i.hasNext();) {
      MenuSheetObject mso = (MenuSheetObject) i.next();

      if (fTopLevelOnly) {
        String sCurTag = mso.getTag();

        if (sCurTag.equals (sTag)) {
          return mso;
        }
      }
      else {
        msoReturn = mso.getTaggedItem (sTag, fTopLevelOnly);

        if (msoReturn != null) {
          return msoReturn;
        }
      }
    }

    return null;
  }

  /**
    * Attach a SalesPoint to this MenuSheet.
    *
    * @override Never
    */
  public synchronized SalesPoint attach (SalesPoint sp) {
    for (Iterator i = iterator(); i.hasNext();) {
      ((MenuSheetObject) i.next()).attach (sp);
    }

    return super.attach (sp);
  }

  /**
    * Attach a SaleProcess to this MenuSheet.
    *
    * @override Never
    */
  public synchronized SaleProcess attach (SaleProcess p) {
    for (Iterator i = iterator(); i.hasNext();) {
      ((MenuSheetObject) i.next()).attach (p);
    }

    return super.attach (p);
  }

  /**
    * Mark this MenuSheet, all its descendants and merged peer MenuSheets visible or
    * invisible.
    *
    * @override Never
    */
  public synchronized void setVisible (boolean fVisible) {
    super.setVisible (fVisible);

    for (Iterator i = iterator(); i.hasNext();) {
      ((MenuSheetObject) i.next()).setVisible (fVisible);
    }

    if (m_msMerged != null) {
      m_msMerged.setVisible (fVisible);
    }

    if (!fVisible) {
      synchronized (getPeerLock()) {
        if (m_jmbBarPeer != null) {
          m_jmbBarPeer.removeAll();
          m_jmbBarPeer = null;
        }

        if (m_jmPeer != null) {
          m_jmPeer.removeAll();
          m_jmPeer = null;
        }
      }
    }
  }

  /**
    * Set the caption of this MenuSheet.
    *
    * <p>If there is a peer it will reflect the changes immediately.</p>
    *
    * @override Never
    *
    * @param sCaption the new caption.
    */
  public void setCaption (String sCaption) {
    super.setCaption (sCaption);

    synchronized (getPeerLock()) {
      if (m_jmPeer != null) {
        m_jmPeer.setText (sCaption);
        m_jmPeer.validate();
      }
    }
  }

  /**
    * Set the mnemonic of this MenuSheet.
    *
    * <p>If there is a peer it will reflect the changes immediately.</p>
    *
    * @override Never
    *
    * @param sMnemonic the new mnemonic.
    */
  public void setMnemonic (char cMnemonic) {
    m_cMnemonic = cMnemonic;

    synchronized (getPeerLock()) {
      if (m_jmPeer != null) {
            m_jmPeer.setMnemonic (cMnemonic);
            m_jmPeer.validate();
      }
    }
  }

  /**
    * Set the default icon of this MenuSheet.
    *
    * <p>If there is a peer it will reflect the changes immediately.</p>
    *
    * @override Never
    *
    * @param iiImageIcon the new icon.
    */
  public void setDefaultIcon (ImageIcon iiImageIcon) {
    setIcon (iiImageIcon, DEFAULT_IMAGE);
  }

  /**
    * Set the selected icon of this MenuSheet.
    *
    * <p>If there is a peer it will reflect the changes immediately.</p>
    *
    * @override Never
    *
    * @param iiImageIcon the new icon.
    */
  public void setSelectedIcon (ImageIcon iiImageIcon) {
    setIcon (iiImageIcon, SELECTED_IMAGE);
  }

  /**
    * Set the disabled icon of this MenuSheet.
    *
    * <p>If there is a peer it will reflect the changes immediately.</p>
    *
    * @override Never
    *
    * @param iiImageIcon the new icon.
    */
  public void setDisabledIcon (ImageIcon iiImageIcon) {
    setIcon (iiImageIcon, DISABLED_IMAGE);
  }

  /**
    * Set the disabled selected icon of this MenuSheet.
    *
    * <p>If there is a peer it will reflect the changes immediately.</p>
    *
    * @override Never
    *
    * @param iiImageIcon the new icon.
    */
  public void setDisabledSelectedIcon (ImageIcon iiImageIcon) {
    setIcon (iiImageIcon, DISABLED_SELECTED_IMAGE);
  }

  /**
    * The JMenuItem peer of a MenuSheet is, of course, a JMenu.
    *
    * @override Never
    */
  public JMenuItem getPeer() {
    synchronized (getPeerLock()) {
      if (m_jmPeer == null) {
        m_jmPeer = new JMenu (getCaption());

        // add Mnemonic to JMenu if exists
        if (m_cMnemonic != '\0')
          m_jmPeer.setMnemonic (m_cMnemonic);

        if (m_aiImages != null) {
          // add DefaultIcon, if any
          if (m_aiImages[DEFAULT_IMAGE] != null)
            m_jmPeer.setIcon (new ImageIcon ((Image)m_aiImages[DEFAULT_IMAGE]));
          // add PressedIcon, if any
          if (m_aiImages[SELECTED_IMAGE] != null)
            m_jmPeer.setSelectedIcon (new ImageIcon ((Image)m_aiImages[SELECTED_IMAGE]));
          // add DisabledIcon, if any
          if (m_aiImages[DISABLED_IMAGE] != null)
            m_jmPeer.setDisabledIcon (new ImageIcon ((Image)m_aiImages[DISABLED_IMAGE]));
          // add DisabledSelectedIcon, if any
          if (m_aiImages[DISABLED_SELECTED_IMAGE] != null)
            m_jmPeer.setDisabledSelectedIcon (new ImageIcon ((Image)m_aiImages[DISABLED_SELECTED_IMAGE]));
        }

        for (Iterator i = iterator(); i.hasNext();) {
          MenuSheetObject mso = (MenuSheetObject) i.next();

          if (mso.isSeparator()) {
            m_jmPeer.addSeparator();
          }
          else {
            m_jmPeer.add (mso.getPeer());
          }
        }
      }
    }

    return m_jmPeer;
  }

  /**
    * For MenuSheets there is no difference between JMenuItem and JMenu peer, they are
    * both JMenus.
    *
    * @override Never
    */
  public JMenu getMenuPeer() {
    return (JMenu)getPeer();
  }

  /**
    * Return the JMenuBar peer.
    *
    * <p>For a MenuSheet there is a special peer: the JMenuBar peer. All items of this
    * MenuSheet will present their JMenu peer in the JMenuBar. Only MenuSheetSeparators
    * are not displayed in a JMenuBar representation. Merged peers are only displayed in
    * the JMenuBar representation.</p>
    *
    * @override Never
    */
  public JMenuBar getMenuBar() {
    synchronized (getPeerLock()) {
      if (m_jmbBarPeer == null) {
        boolean fResolvedMerge = false;

        m_jmbBarPeer = new JMenuBar();

        for (Iterator i = iterator(); i.hasNext();) {
          MenuSheetObject mso = (MenuSheetObject) i.next();

          if ((!fResolvedMerge) && (m_msMerged != null)) {
            if (mso.getTag().equals (m_sMergedBefore)) {

              m_nMergedAt = m_jmbBarPeer.getMenuCount();

              synchronized (m_msMerged) {
                MenuElement[] ajmMerged = m_msMerged.getMenuBar().getSubElements();
                m_msMerged.getMenuBar().removeAll();

                for (int n = 0; n < ajmMerged.length; n++) {
                  m_jmbBarPeer.add ((JMenu) ajmMerged[n]);
                }
              }

              fResolvedMerge = true;
            }
          }

          if (!mso.isSeparator()) {
            m_jmbBarPeer.add (mso.getMenuPeer());
            if (HELP_MENU_TAG.equals (mso.getTag())) {
              m_jmbBarPeer.setHelpMenu (mso.getMenuPeer());
            }
          }
        }

        if ((!fResolvedMerge) && (m_msMerged != null)) {
          // mark merge at end !
          m_sMergedBefore = null;
          m_nMergedAt = m_jmbBarPeer.getMenuCount();

          synchronized (m_msMerged) {
            MenuElement[] ajmMerged = m_msMerged.getMenuBar().getSubElements();
            m_msMerged.getMenuBar().removeAll();

            for (int n = 0; n < ajmMerged.length; n++) {
              m_jmbBarPeer.add ((JMenu) ajmMerged[n]);
            }
          }
        }
      }
    }

    return m_jmbBarPeer;
  }

  /**
    * Get the Mnemonic of this MenuSheet.
    *
    * @override Never
    *
    * @return the mnemonic of this MenuSheet.
    */
  public char getMnemonic() {
    return m_cMnemonic;
  }

  /**
    * Get the default icon of this MenuSheet.
    *
    * @override Never
    *
    * @return the default icon of this MenuSheet.
    */
  public ImageIcon getDefaultIcon() {
    return (m_aiImages != null)? new ImageIcon ((Image)m_aiImages[DEFAULT_IMAGE]) : null;
  }

  /**
    * Get the selected icon of this MenuSheet.
    *
    * @override Never
    *
    * @return the pressed icon of this MenuSheet.
    */
  public ImageIcon getSelectedIcon() {
     return (m_aiImages != null)? new ImageIcon ((Image)m_aiImages[SELECTED_IMAGE]) : null;
  }

  /**
    * Get the disabled item of this MenuSheet.
    *
    * @override Never
    *
    * @return the disabled icon of this MenuSheet.
    */
  public ImageIcon getDisabledIcon() {
    return (m_aiImages != null)? new ImageIcon ((Image)m_aiImages[DISABLED_IMAGE]) : null;
  }

  /**
    * Get the disabled selected item of this MenuSheet.
    *
    * @override Never
    *
    * @return the disabled selected icon of this MenuSheet.
    */
  public ImageIcon getDisabledSelectedIcon() {
     return (m_aiImages != null)? new ImageIcon ((Image)m_aiImages[DISABLED_SELECTED_IMAGE]) : null;
  }

  /**
    * A tag that will identify the help menu, should this MenuSheet be displayed as a
    * JMenuBar.
    */
  public final static String HELP_MENU_TAG = "__TAG:_HELP_MENU";
  // A Tag that will identify the Image for the DefaultIcon
  private final static int DEFAULT_IMAGE = 0;
  // A Tag that will identify the Image for the SelectedIcon
  private final static int SELECTED_IMAGE = 1;
  // A Tag that will identify the Image for the DisabledIcon
  private final static int DISABLED_IMAGE = 2;
  // A Tag that will identify the Image for the DisabledSelectedIcon
  private final static int DISABLED_SELECTED_IMAGE = 3;
}