/*
    GNU LESSER GENERAL PUBLIC LICENSE
    Copyright (C) 2006 The Lobo Project

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    Contact info: lobochief@users.sourceforge.net
 */
/*
 * Created on Nov 19, 2005
 */
package org.lobobrowser.html.gui;

import java.awt.Cursor;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.EventObject;
import java.util.concurrent.Future;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import org.lobobrowser.html.HtmlRendererContext;
import org.lobobrowser.html.domimpl.DocumentNotificationListener;
import org.lobobrowser.html.domimpl.ElementImpl;
import org.lobobrowser.html.domimpl.HTMLDocumentImpl;
import org.lobobrowser.html.domimpl.NodeImpl;
import org.lobobrowser.html.parser.DocumentBuilderImpl;
import org.lobobrowser.html.parser.InputSourceImpl;
import org.lobobrowser.html.renderer.BoundableRenderable;
import org.lobobrowser.html.renderer.FrameContext;
import org.lobobrowser.html.renderer.NodeRenderer;
import org.lobobrowser.html.renderer.RenderableSpot;
import org.lobobrowser.html.style.RenderState;
import org.lobobrowser.main.DefferedLayoutSupport;
import org.lobobrowser.ua.UserAgentContext;
import org.lobobrowser.util.EventDispatch2;
import org.lobobrowser.util.gui.WrapperLayout;
import org.w3c.dom.Document;
import org.w3c.dom.Text;
import org.w3c.dom.html.HTMLFrameSetElement;

/**
 * The <code>HtmlPanel</code> class is a Swing component that can render a HTML
 * DOM. It uses either {@link HtmlBlockPanel} or {@link FrameSetPanel}
 * internally, depending on whether the document is determined to be a FRAMESET
 * or not.
 * <p>
 * Invoke method {@link #setDocument(Document, HtmlRendererContext)} in order to
 * schedule a document for rendering.
 */
public class HtmlPanel extends JComponent implements FrameContext, DefferedLayoutSupport {
  private static final long serialVersionUID = -8342517547909796721L;
  private final EventDispatch2 selectionDispatch = new SelectionDispatch();
  private final javax.swing.Timer notificationTimer;
  private final DocumentNotificationListener notificationListener;
  private final Runnable notificationImmediateAction;
  private static final int NOTIF_TIMER_DELAY = 10;

  private volatile boolean isFrameSet = false;
  private volatile NodeRenderer nodeRenderer = null;
  private volatile NodeImpl rootNode;
  private volatile int preferredWidth = -1;
  private volatile int defaultOverflowX = RenderState.OVERFLOW_AUTO;
  private volatile int defaultOverflowY = RenderState.OVERFLOW_AUTO;

  protected volatile HtmlBlockPanel htmlBlockPanel;
  protected volatile FrameSetPanel frameSetPanel;

  /**
   * Constructs an <code>HtmlPanel</code>.
   */
  public HtmlPanel() {
    super();
    this.setLayout(WrapperLayout.getInstance());
    this.setOpaque(false);
    this.notificationTimer = new javax.swing.Timer(NOTIF_TIMER_DELAY, new NotificationTimerAction());
    this.notificationTimer.setRepeats(false);
    this.notificationListener = new LocalDocumentNotificationListener();
    this.notificationImmediateAction = new Runnable() {
      public void run() {
        processNotifications();
      }
    };
  }

  /**
   * Sets a preferred width that serves as a hint in calculating the preferred
   * size of the <code>HtmlPanel</code>. Note that the preferred size can only
   * be calculated when a document is available, and it will vary during
   * incremental rendering.
   * <p>
   * This method currently does not have any effect when the document is a
   * FRAMESET.
   * <p>
   * Note also that setting the preferred width (to a value other than
   * <code>-1</code>) will negatively impact performance.
   *
   * @param width
   *          The preferred width, or <code>-1</code> to unset.
   */
  public void setPreferredWidth(final int width) {
    this.preferredWidth = width;
    final HtmlBlockPanel htmlBlock = this.htmlBlockPanel;
    if (htmlBlock != null) {
      htmlBlock.setPreferredWidth(width);
    }
  }

  /**
   * If the current document is not a FRAMESET, this method scrolls the body
   * area to the given location.
   * <p>
   * This method should be called from the GUI thread.
   *
   * @param bounds
   *          The bounds in the scrollable block area that should become
   *          visible.
   * @param xIfNeeded
   *          If this parameter is true, scrolling will only occur if the
   *          requested bounds are not currently visible horizontally.
   * @param yIfNeeded
   *          If this parameter is true, scrolling will only occur if the
   *          requested bounds are not currently visible vertically.
   */
  public void scrollTo(final Rectangle bounds, final boolean xIfNeeded, final boolean yIfNeeded) {
    final HtmlBlockPanel htmlBlock = this.htmlBlockPanel;
    if (htmlBlock != null) {
      htmlBlock.scrollTo(bounds, xIfNeeded, yIfNeeded);
    }
  }

  /**
   * Scrolls the body area to the node given, if it is part of the current
   * document.
   * <p>
   * This method should be called from the GUI thread.
   *
   * @param node
   *          A DOM node.
   */
  public void scrollTo(final org.w3c.dom.Node node) {
    final HtmlBlockPanel htmlBlock = this.htmlBlockPanel;
    if (htmlBlock != null) {
      htmlBlock.scrollTo(node);
    }
  }

  /**
   * Gets the root <code>Renderable</code> of the HTML block. It returns
   * <code>null</code> for FRAMESETs.
   */
  public BoundableRenderable getBlockRenderable() {
    final HtmlBlockPanel htmlBlock = this.htmlBlockPanel;
    return htmlBlock == null ? null : htmlBlock.getRootRenderable();
  }

  /**
   * Gets an instance of {@link FrameSetPanel} in case the currently rendered
   * page is a FRAMESET.
   * <p>
   * Note: This method should be invoked in the GUI thread.
   *
   * @return A <code>FrameSetPanel</code> instance or <code>null</code> if the
   *         document currently rendered is not a FRAMESET.
   */
  public FrameSetPanel getFrameSetPanel() {
    final int componentCount = this.getComponentCount();
    if (componentCount == 0) {
      return null;
    }
    final Object c = this.getComponent(0);
    if (c instanceof FrameSetPanel) {
      return (FrameSetPanel) c;
    }
    return null;
  }

  private void setUpAsBlock(final UserAgentContext ucontext, final HtmlRendererContext rcontext) {
    final HtmlBlockPanel shp = this.createHtmlBlockPanel(ucontext, rcontext);
    shp.setPreferredWidth(this.preferredWidth);
    shp.setDefaultOverflowX(this.defaultOverflowX);
    shp.setDefaultOverflowY(this.defaultOverflowY);
    this.htmlBlockPanel = shp;
    this.frameSetPanel = null;
    this.removeAll();
    this.add(shp);
    this.nodeRenderer = shp;
  }

  private void setUpFrameSet(final NodeImpl fsrn) {
    this.isFrameSet = true;
    this.htmlBlockPanel = null;
    final FrameSetPanel fsp = this.createFrameSetPanel();
    this.frameSetPanel = fsp;
    this.nodeRenderer = fsp;
    this.removeAll();
    this.add(fsp);
    fsp.setRootNode(fsrn);
  }

  /**
   * Method invoked internally to create a {@link HtmlBlockPanel}. It is made
   * available so it can be overridden.
   */
  protected HtmlBlockPanel createHtmlBlockPanel(final UserAgentContext ucontext, final HtmlRendererContext rcontext) {
    return new HtmlBlockPanel(java.awt.Color.WHITE, true, ucontext, rcontext, this);
  }

  /**
   * Method invoked internally to create a {@link FrameSetPanel}. It is made
   * available so it can be overridden.
   */
  protected FrameSetPanel createFrameSetPanel() {
    return new FrameSetPanel();
  }

  /**
   * Scrolls the document such that x and y coordinates are placed in the
   * upper-left corner of the panel.
   * <p>
   * This method may be called outside of the GUI Thread.
   *
   * @param x
   *          The x coordinate.
   * @param y
   *          The y coordinate.
   */
  public void scroll(final int x, final int y) {
    if (SwingUtilities.isEventDispatchThread()) {
      this.scrollImpl(x, y);
    } else {
      SwingUtilities.invokeLater(() -> scrollImpl(x, y));
    }
  }

  public void scrollBy(final int x, final int y) {
    if (SwingUtilities.isEventDispatchThread()) {
      this.scrollByImpl(x, y);
    } else {
      SwingUtilities.invokeLater(() -> scrollByImpl(x, y));
    }
  }

  private void scrollImpl(final int x, final int y) {
    this.scrollTo(new Rectangle(x, y, 16, 16), false, false);
  }

  private void scrollByImpl(final int xOffset, final int yOffset) {
    final HtmlBlockPanel bp = this.htmlBlockPanel;
    if (bp != null) {
      bp.scrollBy(xOffset, yOffset);
    }
  }

  /**
   * Clears the current document if any. If called outside the GUI thread, the
   * operation will be scheduled to be performed in the GUI thread.
   */
  public void clearDocument() {
    if (SwingUtilities.isEventDispatchThread()) {
      this.clearDocumentImpl();
    } else {
      SwingUtilities.invokeLater(() -> HtmlPanel.this.clearDocumentImpl());
    }
  }

  private void clearDocumentImpl() {
    final HTMLDocumentImpl prevDocument = (HTMLDocumentImpl) this.rootNode;
    if (prevDocument != null) {
      prevDocument.removeDocumentNotificationListener(this.notificationListener);
    }
    final NodeRenderer nr = this.nodeRenderer;
    if (nr != null) {
      nr.setRootNode(null);
    }
    this.rootNode = null;
    this.htmlBlockPanel = null;
    this.nodeRenderer = null;
    this.isFrameSet = false;
    this.removeAll();
    this.revalidate();
    this.repaint();
  }

  /**
   * Sets an HTML DOM node and invalidates the component so it is rendered as
   * soon as possible in the GUI thread.
   * <p>
   * If this method is called from a thread that is not the GUI dispatch thread,
   * the document is scheduled to be set later. Note that
   * {@link #setPreferredWidth(int) preferred size} calculations should be done
   * in the GUI dispatch thread for this reason.
   *
   * @param node
   *          This should normally be a Document instance obtained with
   *          {@link org.lobobrowser.html.parser.DocumentBuilderImpl}.
   *          <p>
   * @param rcontext
   *          A renderer context.
   * @see org.lobobrowser.html.parser.DocumentBuilderImpl#parse(org.xml.sax.InputSource)
   * @see org.lobobrowser.html.test.SimpleHtmlRendererContext
   */
  public void setDocument(final Document node, final HtmlRendererContext rcontext) {
    setCursor(Cursor.getDefaultCursor());

    if (SwingUtilities.isEventDispatchThread()) {
      this.setDocumentImpl(node, rcontext);
    } else {
      SwingUtilities.invokeLater(() -> HtmlPanel.this.setDocumentImpl(node, rcontext));
    }
  }

  @Override
  public void setCursor(final Cursor cursor) {
    if (cursor != getCursor()) {
      super.setCursor(cursor);
    }
  }

  /**
   * Scrolls to the element identified by the given ID in the current document.
   * <p>
   * If this method is invoked outside the GUI thread, the operation is
   * scheduled to be performed as soon as possible in the GUI thread.
   *
   * @param nameOrId
   *          The name or ID of the element in the document.
   */
  public void scrollToElement(final String nameOrId) {
    if (SwingUtilities.isEventDispatchThread()) {
      this.scrollToElementImpl(nameOrId);
    } else {
      SwingUtilities.invokeLater(() -> scrollToElementImpl(nameOrId));
    }
  }

  private void scrollToElementImpl(final String nameOrId) {
    final NodeImpl node = this.rootNode;
    if (node instanceof HTMLDocumentImpl) {
      final HTMLDocumentImpl doc = (HTMLDocumentImpl) node;
      final org.w3c.dom.Element element = doc.getElementById(nameOrId);
      if (element != null) {
        this.scrollTo(element);
      }
    }
  }

  private void setDocumentImpl(final Document node, final HtmlRendererContext rcontext) {
    // Expected to be called in the GUI thread.
    /*
    if (!(node instanceof HTMLDocumentImpl)) {
      throw new IllegalArgumentException("Only nodes of type HTMLDocumentImpl are currently supported. Use DocumentBuilderImpl.");
    }
    */

    if (this.rootNode instanceof HTMLDocumentImpl) {
      final HTMLDocumentImpl prevDocument = (HTMLDocumentImpl) this.rootNode;
      prevDocument.removeDocumentNotificationListener(this.notificationListener);
    }
    if (node instanceof HTMLDocumentImpl) {
    final HTMLDocumentImpl nodeImpl = (HTMLDocumentImpl) node;
    nodeImpl.addDocumentNotificationListener(this.notificationListener);
    }

    if (node instanceof NodeImpl) {
    final NodeImpl nodeImpl = (NodeImpl) node;
    this.rootNode = nodeImpl;
    final NodeImpl fsrn = this.getFrameSetRootNode(nodeImpl);
    final boolean newIfs = fsrn != null;
    if ((newIfs != this.isFrameSet) || (this.getComponentCount() == 0)) {
      this.isFrameSet = newIfs;
      if (newIfs) {
        this.setUpFrameSet(fsrn);
      } else {
        this.setUpAsBlock(rcontext.getUserAgentContext(), rcontext);
      }
    }
    final NodeRenderer nr = this.nodeRenderer;
    if (nr != null) {
      // These subcomponents should take care
      // of revalidation.
      if (newIfs) {
        nr.setRootNode(fsrn);
      } else {
        nr.setRootNode(nodeImpl);
      }
    } else {
      this.invalidate();
      this.validate();
      this.repaint();
    }
    }
  }

  /**
   * Renders HTML given as a string.
   *
   * @param htmlSource
   *          The HTML source code.
   * @param uri
   *          A base URI used to resolve item URIs.
   * @param rcontext
   *          The {@link HtmlRendererContext} instance.
   * @see org.lobobrowser.html.test.SimpleHtmlRendererContext
   * @see #setDocument(Document, HtmlRendererContext)
   */
  public void setHtml(final String htmlSource, final String uri, final HtmlRendererContext rcontext) {
    try {
      final DocumentBuilderImpl builder = new DocumentBuilderImpl(rcontext.getUserAgentContext(), rcontext);
      try (
        final Reader reader = new StringReader(htmlSource)) {
        final InputSourceImpl is = new InputSourceImpl(reader, uri);
        final Document document = builder.parse(is);
        this.setDocument(document, rcontext);
      }
    } catch (final java.io.IOException ioe) {
      throw new IllegalStateException("Unexpected condition.", ioe);
    } catch (final org.xml.sax.SAXException se) {
      throw new IllegalStateException("Unexpected condition.", se);
    }
  }

  /**
   * Gets the HTML DOM node currently rendered if any.
   */
  public NodeImpl getRootNode() {
    return this.rootNode;
  }

  private boolean resetIfFrameSet() {
    final NodeImpl nodeImpl = this.rootNode;
    final NodeImpl fsrn = this.getFrameSetRootNode(nodeImpl);
    final boolean newIfs = fsrn != null;
    if ((newIfs != this.isFrameSet) || (this.getComponentCount() == 0)) {
      this.isFrameSet = newIfs;
      if (newIfs) {
        this.setUpFrameSet(fsrn);
        final NodeRenderer nr = this.nodeRenderer;
        nr.setRootNode(fsrn);
        // Set proper bounds and repaint.
        this.validate();
        this.repaint();
        return true;
      }
    }
    return false;
  }

  private NodeImpl getFrameSetRootNode(final NodeImpl node) {
    if (node instanceof Document) {
      final ElementImpl element = (ElementImpl) ((Document) node).getDocumentElement();
      if ((element != null) && "HTML".equalsIgnoreCase(element.getTagName())) {
        return this.getFrameSet(element);
      } else {
        return this.getFrameSet(node);
      }
    } else {
      return null;
    }
  }

  private NodeImpl getFrameSet(final NodeImpl node) {
    final NodeImpl[] children = node.getChildrenArray();
    if (children == null) {
      return null;
    }
    final int length = children.length;
    NodeImpl frameSet = null;
    for (int i = 0; i < length; i++) {
      final NodeImpl child = children[i];
      if (child instanceof Text) {
        // Ignore
      } else if (child instanceof ElementImpl) {
        final String tagName = child.getNodeName();
        if ("HEAD".equalsIgnoreCase(tagName) || "NOFRAMES".equalsIgnoreCase(tagName) || "TITLE".equalsIgnoreCase(tagName)
            || "META".equalsIgnoreCase(tagName) || "SCRIPT".equalsIgnoreCase(tagName) || "NOSCRIPT".equalsIgnoreCase(tagName)) {
          // ignore it
        } else if ("FRAMESET".equalsIgnoreCase(tagName)) {
          frameSet = child;
          break;
        } else {
          if (this.hasSomeHtml((ElementImpl) child)) {
            return null;
          }
        }
      }
    }
    return frameSet;
  }

  private boolean hasSomeHtml(final ElementImpl element) {
    final String tagName = element.getTagName();
    if ("HEAD".equalsIgnoreCase(tagName) || "TITLE".equalsIgnoreCase(tagName) || "META".equalsIgnoreCase(tagName)) {
      return false;
    }
    final NodeImpl[] children = element.getChildrenArray();
    if (children != null) {
      final int length = children.length;
      for (int i = 0; i < length; i++) {
        final NodeImpl child = children[i];
        if (child instanceof Text) {
          final String textContent = ((Text) child).getTextContent();
          if ((textContent != null) && !"".equals(textContent.trim())) {
            return false;
          }
        } else if (child instanceof ElementImpl) {
          if (this.hasSomeHtml((ElementImpl) child)) {
            return false;
          }
        }
      }
    }
    return true;
  }

  /**
   * Internal method used to expand the selection to the given point.
   * <p>
   * Note: This method should be invoked in the GUI thread.
   */
  public void expandSelection(final RenderableSpot rpoint) {
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block != null) {
      block.setSelectionEnd(rpoint);
      block.repaint();
      this.selectionDispatch.fireEvent(new SelectionChangeEvent(this, block.isSelectionAvailable()));
    }
  }

  /**
   * Internal method used to reset the selection so that it is empty at the
   * given point. This is what is called when the user clicks on a point in the
   * document.
   * <p>
   * Note: This method should be invoked in the GUI thread.
   */
  public void resetSelection(final RenderableSpot rpoint) {
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block != null) {
      block.setSelectionStart(rpoint);
      block.setSelectionEnd(rpoint);
      block.repaint();
    }
    this.selectionDispatch.fireEvent(new SelectionChangeEvent(this, false));
  }

  /**
   * Gets the selection text.
   * <p>
   * Note: This method should be invoked in the GUI thread.
   */
  public String getSelectionText() {
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block == null) {
      return null;
    } else {
      return block.getSelectionText();
    }
  }

  /**
   * Gets a DOM node enclosing the selection. The node returned should be the
   * inner-most node that encloses both selection start and end points. Note
   * that the selection end point may be just outside of the selection.
   * <p>
   * Note: This method should be invoked in the GUI thread.
   *
   * @return A node enclosing the current selection, or <code>null</code> if
   *         there is no such node. It also returns <code>null</code> for
   *         FRAMESETs.
   */
  public org.w3c.dom.Node getSelectionNode() {
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block == null) {
      return null;
    } else {
      return block.getSelectionNode();
    }
  }

  /**
   * Returns true only if the current block has a selection. This method has no
   * effect in FRAMESETs at the moment.
   */
  public boolean hasSelection() {
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block == null) {
      return false;
    } else {
      return block.hasSelection();
    }
  }

  /**
   * Copies the current selection, if any, into the clipboard. This method has
   * no effect in FRAMESETs at the moment.
   */
  public boolean copy() {
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block != null) {
      return block.copy();
    } else {
      return false;
    }
  }

  /**
   * Adds listener of selection changes. Note that it does not have any effect
   * on FRAMESETs.
   *
   * @param listener
   *          An instance of {@link SelectionChangeListener}.
   */
  public void addSelectionChangeListener(final SelectionChangeListener listener) {
    this.selectionDispatch.addListener(listener);
  }

  /**
   * Removes a listener of selection changes that was previously added.
   */
  public void removeSelectionChangeListener(final SelectionChangeListener listener) {
    this.selectionDispatch.removeListener(listener);
  }

  /**
   * Sets the default horizontal overflow.
   * <p>
   * This method has no effect on FRAMESETs.
   *
   * @param overflow
   *          See {@link org.lobobrowser.html.style.RenderState}.
   */
  public void setDefaultOverflowX(final int overflow) {
    this.defaultOverflowX = overflow;
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block != null) {
      block.setDefaultOverflowX(overflow);
    }
  }

  /**
   * Sets the default vertical overflow.
   * <p>
   * This method has no effect on FRAMESETs.
   *
   * @param overflow
   *          See {@link org.lobobrowser.html.style.RenderState}.
   */
  public void setDefaultOverflowY(final int overflow) {
    this.defaultOverflowY = overflow;
    final HtmlBlockPanel block = this.htmlBlockPanel;
    if (block != null) {
      block.setDefaultOverflowY(overflow);
    }
  }

  private final ArrayList<DocumentNotification> notifications = new ArrayList<>(1);

  private void addNotification(final DocumentNotification notification) {
    // This can be called in a random thread.
    final ArrayList<DocumentNotification> notifs = this.notifications;
    synchronized (notifs) {
      notifs.add(notification);
    }
    if (SwingUtilities.isEventDispatchThread()) {
      // In this case we want the notification to be processed
      // immediately. However, we don't want potential recursions
      // to occur when a Javascript property is set in the GUI thread.
      // Additionally, many property values may be set in one
      // event block.
      SwingUtilities.invokeLater(this.notificationImmediateAction);
    } else {
      this.notificationTimer.restart();
    }
  }

  /**
   * Invalidates the layout of the given node and schedules it to be layed out
   * later. Multiple invalidations may be processed in a single document layout.
   */
  public void delayedRelayout(final NodeImpl node) {
    final ArrayList<DocumentNotification> notifs = this.notifications;
    synchronized (notifs) {
      notifs.add(new DocumentNotification(DocumentNotification.SIZE, node));
    }
    this.notificationTimer.restart();
  }

  private void processNotifications() {
    // This is called in the GUI thread.
    final ArrayList<DocumentNotification> notifs = this.notifications;
    DocumentNotification[] notifsArray;
    synchronized (notifs) {
      final int size = notifs.size();
      if (size == 0) {
        return;
      }
      notifsArray = new DocumentNotification[size];
      notifsArray = notifs.toArray(notifsArray);
      notifs.clear();
    }
    final int length = notifsArray.length;
    for (int i = 0; i < length; i++) {
      final DocumentNotification dn = notifsArray[i];
      if ((dn.node instanceof HTMLFrameSetElement) && (this.htmlBlockPanel != null)) {
        if (this.resetIfFrameSet()) {
          // Revalidation already taken care of.
          return;
        }
      }
    }
    final HtmlBlockPanel blockPanel = this.htmlBlockPanel;
    if (blockPanel != null) {
      blockPanel.processDocumentNotifications(notifsArray);
    }
    final FrameSetPanel frameSetPanel = this.frameSetPanel;
    if (frameSetPanel != null) {
      frameSetPanel.processDocumentNotifications(notifsArray);
    }
  }

  private class SelectionDispatch extends EventDispatch2 {
    /*
     * (non-Javadoc)
     *
     * @see
     * org.xamjwg.util.EventDispatch2#dispatchEvent(java.util.EventListener,
     * java.util.EventObject)
     */
    @Override
    protected void dispatchEvent(final EventListener listener, final EventObject event) {
      ((SelectionChangeListener) listener).selectionChanged((SelectionChangeEvent) event);
    }
  }

  private class LocalDocumentNotificationListener implements DocumentNotificationListener {
    public void allInvalidated() {
      HtmlPanel.this.addNotification(new DocumentNotification(DocumentNotification.GENERIC, null));
    }

    public void invalidated(final NodeImpl node) {
      HtmlPanel.this.addNotification(new DocumentNotification(DocumentNotification.GENERIC, node));
    }

    public void lookInvalidated(final NodeImpl node) {
      HtmlPanel.this.addNotification(new DocumentNotification(DocumentNotification.LOOK, node));
    }

    public void positionInvalidated(final NodeImpl node) {
      HtmlPanel.this.addNotification(new DocumentNotification(DocumentNotification.POSITION, node));
    }

    public void sizeInvalidated(final NodeImpl node) {
      HtmlPanel.this.addNotification(new DocumentNotification(DocumentNotification.SIZE, node));
    }

    public void externalScriptLoading(final NodeImpl node) {
      // Ignorable here.
    }

    public void nodeLoaded(final NodeImpl node) {
      HtmlPanel.this.addNotification(new DocumentNotification(DocumentNotification.GENERIC, node));
    }

    public void structureInvalidated(final NodeImpl node) {
      HtmlPanel.this.addNotification(new DocumentNotification(DocumentNotification.GENERIC, node));
    }
  }

  private class NotificationTimerAction implements java.awt.event.ActionListener {
    public void actionPerformed(final ActionEvent e) {
      HtmlPanel.this.processNotifications();
    }
  }

  @Override
  public Future<Boolean> layoutCompletion() {
    return htmlBlockPanel.layoutCompletion();
  }

  public boolean isReadyToPaint() {
    final HtmlBlockPanel htmlBlock = this.htmlBlockPanel;
    if (htmlBlock != null) {
      return  (notifications.size() == 0) && htmlBlock.isReadyToPaint();
    }
    return false;
  }

  public void disableRenderHints() {
    this.htmlBlockPanel.disableRenderHints();
  }
}
