/**
 * ========================================
 * JFreeReport : a free Java report library
 * ========================================
 *
 * Project Info:  http://reporting.pentaho.org/
 *
 * (C) Copyright 2000-2007, by Object Refinery Limited, Pentaho Corporation and Contributors.
 *
 * 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., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
 * in the United States and other countries.]
 *
 * ------------
 * $Id: ComponentDrawable.java 10756 2009-12-02 15:58:24Z tmorgner $
 * ------------
 * (C) Copyright 2000-2005, by Object Refinery Limited.
 * (C) Copyright 2005-2007, by Pentaho Corporation.
 */

package org.jfree.report.util;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Window;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Creation-Date: 11.10.2005, 14:03:15
 *
 * @author Thomas Morgner
 */
public class ComponentDrawable
{
  private static final Log logger = LogFactory.getLog (ComponentDrawable.class);

  /**
   * A runnable that executes the drawing operation on the event-dispatcher thread.
   */
  private class PainterRunnable implements Runnable
  {
    /**
     * The draw-area as defined by the drawable.
     */
    private Rectangle2D area;
    /**
     * The graphics-context to which the runnable draws to.
     */
    private Graphics2D graphics;

    /**
     * Default constructor.
     */
    protected PainterRunnable()
    {
    }

    /**
     * Returns the Graphics2D to which the drawable should be rendered.
     *
     * @return the graphics.
     */
    public Graphics2D getGraphics()
    {
      return graphics;
    }

    /**
     * Defines the Graphics2D to which the drawable should be rendered.
     *
     * @param graphics the graphics.
     */
    public void setGraphics(final Graphics2D graphics)
    {
      this.graphics = graphics;
    }

    /**
     * Returns the draw-area to which the drawable should be rendered.
     *
     * @return the graphics.
     */
    public Rectangle2D getArea()
    {
      return area;
    }

    /**
     * Defines the draw-area to which the drawable should be rendered.
     *
     * @param area the graphics.
     */
    public void setArea(final Rectangle2D area)
    {
      this.area = area;
    }

    /**
     * Draws the drawable.
     */
    public void run()
    {
      try
      {
        final Component component = getComponent();
        if (component instanceof Window)
        {
          final Window w = (Window) component;
          w.validate();
        }
        else if (isOwnPeerConnected())
        {
          final Window w = ComponentDrawable.getWindowAncestor(component);
          if (w != null)
          {
            w.validate();
          }
        }
        else
        {
          peerSupply.pack();
          contentPane.add(component);
        }

        component.setBounds
            ((int) area.getX(), (int) area.getY(),
                (int) area.getWidth(), (int) area.getHeight());
        component.validate();
        component.paint(graphics);
      }
      finally
      {
        cleanUp();
      }
    }
  }

  /**
   * A runnable that queries the preferred size. As this may involve font-computations, this has to be done on the
   * EventDispatcher thread.
   */
  private class PreferredSizeRunnable implements Runnable
  {
    /**
     * The return value.
     */
    private Dimension retval;

    /**
     * Default Constructor.
     */
    protected PreferredSizeRunnable()
    {
    }

    /**
     * Returns the dimension that has been computed.
     *
     * @return the preferred size.
     */
    public Dimension getRetval()
    {
      return retval;
    }

    /**
     * Computes the preferred size on the EventDispatcherThread.
     */
    public void run()
    {
      retval = null;
      try
      {
        final Component component = getComponent();
        if (component instanceof Window == false && isOwnPeerConnected() == false)
        {
          peerSupply.pack();
          contentPane.add(component);
          contentPane.validate();
          component.validate();
        }
        else if (isOwnPeerConnected())
        {
          retval = component.getSize();
          return;
        }
        else
        {
          component.validate();
        }
        retval = component.getPreferredSize();
      }
      finally
      {
        cleanUp();
      }
    }
  }

  /**
   * A runnable that queries the defined size. As this may involve font-computations, this has to be done on the
   * EventDispatcher thread.
   */
  private class DefinedSizeRunnable implements Runnable
  {
    /**
     * The return value.
     */
    private Dimension retval;

    /**
     * Default Constructor.
     */
    protected DefinedSizeRunnable()
    {
    }

    /**
     * Returns the dimension that has been computed.
     *
     * @return the declared size.
     */
    public Dimension getRetval()
    {
      return retval;
    }

    /**
     * Computes the declared size on the EventDispatcherThread.
     */
    public void run()
    {
      retval = null;
      try
      {
        final Component component = getComponent();
        if (component instanceof Window == false && isOwnPeerConnected() == false)
        {
          peerSupply.pack();
          contentPane.add(component);
        }
        retval = component.getSize();
      }
      finally
      {
        cleanUp();
      }
    }
  }

  /**
   * A flag indicating whether the aspect ratio should be preserved by the layouter.
   */
  private boolean preserveAspectRatio;
  /**
   * The component that should be drawn.
   */
  private Component component;
  /**
   * The Frame that connects the component to the native Window-System.
   */
  private JFrame peerSupply;
  /**
   * The content pane of the frame.
   */
  private JPanel contentPane;
  /**
   * The runnable that paints the component.
   */
  private PainterRunnable runnable;
  /**
   * The runnable that computes the preferred size.
   */
  private PreferredSizeRunnable preferredSizeRunnable;
  /**
   * The runnable that computes the declared size.
   */
  private DefinedSizeRunnable sizeRunnable;
  /**
   * A flag indicating whether all paint-operations should be performed on the Event-Dispatcher thread.
   */
  private boolean paintSynchronized;
  /**
   * A flag indicating whether components are allowed to provide their own AWT-Window.
   */
  private boolean allowOwnPeer;

  /**
   * Default Constructor.
   */
  public ComponentDrawable()
  {
    this(new JFrame());
  }

  /**
   * Creates a new ComponentDrawable with the given Frame as peer-supply.
   *
   * @param peerSupply the frame that should be used as peer-source.
   */
  public ComponentDrawable(final JFrame peerSupply)
  {
    if (peerSupply == null)
    {
      throw new NullPointerException();
    }
    this.peerSupply = peerSupply;
    this.contentPane = new JPanel();
    this.contentPane.setLayout(null);
    peerSupply.setContentPane(contentPane);
    this.runnable = new PainterRunnable();
    this.preferredSizeRunnable = new PreferredSizeRunnable();
    this.sizeRunnable = new DefinedSizeRunnable();
  }

  /**
   * Returns, whether components are allowed to provide their own AWT-Window.
   *
   * @return true, if foreign peers are allowed, false otherwise.
   */
  public boolean isAllowOwnPeer()
  {
    return allowOwnPeer;
  }

  /**
   * Defines, whether components are allowed to provide their own AWT-Window.
   *
   * @param allowOwnPeer true, if components can provide their own peers, false otherwise.
   */
  public void setAllowOwnPeer(final boolean allowOwnPeer)
  {
    this.allowOwnPeer = allowOwnPeer;
  }

  /**
   * Returns, whether component operations will happen on the Event-Dispatcher threads. As the AWT is not synchronized,
   * weird things can happen if AWT functionality is executed on user threads.
   *
   * @return true, if all operations will be done on the AWT-EventDispatcher thread, false if they should be done in the
   *         local thread.
   */
  public boolean isPaintSynchronized()
  {
    return paintSynchronized;
  }

  /**
   * Defines, whether component operations will happen on the Event-Dispatcher threads. As the AWT is not synchronized,
   * weird things can happen if AWT functionality is executed on user threads.
   *
   * @param paintSynchronized true, if all operations will be done on the AWT-EventDispatcher thread, false if they
   *                          should be done in the local thread.
   */
  public void setPaintSynchronized(final boolean paintSynchronized)
  {
    this.paintSynchronized = paintSynchronized;
  }

  /**
   * A helper method that performs some cleanup and disconnects the component from the AWT and the Swing-Framework to
   * avoid memory-leaks.
   */
  protected final void cleanUp()
  {
    if (component instanceof JComponent && isOwnPeerConnected() == false)
    {
      final JComponent jc = (JComponent) component;
      RepaintManager.currentManager(jc).removeInvalidComponent(jc);
      RepaintManager.currentManager(jc).markCompletelyClean(jc);
    }
    contentPane.removeAll();
    RepaintManager.currentManager(contentPane).removeInvalidComponent(contentPane);
    RepaintManager.currentManager(contentPane).markCompletelyClean(contentPane);
    peerSupply.dispose();
  }

  /**
   * Returns the component that should be drawn.
   *
   * @return the component.
   */
  public Component getComponent()
  {
    return component;
  }

  /**
   * Defines the component that should be drawn.
   *
   * @param component the component.
   */
  public void setComponent(final Component component)
  {
    this.component = component;
    prepareComponent(component);
  }

  /**
   * Returns the preferred size of the drawable. If the drawable is aspect ratio aware, these bounds should be used to
   * compute the preferred aspect ratio for this drawable.
   * <p/>
   * This calls {@link java.awt.Component#getPreferredSize()} on the given component.
   *
   * @return the preferred size.
   */
  public Dimension getPreferredSize()
  {
    if (component == null)
    {
      return new Dimension(0, 0);
    }
    if (SwingUtilities.isEventDispatchThread() || paintSynchronized == false)
    {
      preferredSizeRunnable.run();
      return preferredSizeRunnable.getRetval();
    }

    try
    {
      SwingUtilities.invokeAndWait(preferredSizeRunnable);
      return preferredSizeRunnable.getRetval();
    }
    catch (Exception e)
    {
      ComponentDrawable.logger.warn("Failed to compute the preferred size.");
    }
    return new Dimension(0, 0);
  }

  /**
   * Returns the declared size of the drawable. If the drawable is aspect ratio aware, these bounds should be used to
   * compute the declared aspect ratio for this drawable.
   * <p/>
   * This calls {@link java.awt.Component#getSize()} on the given component.
   *
   * @return the preferred size.
   */
  public Dimension getSize()
  {
    if (component == null)
    {
      return new Dimension(0, 0);
    }
    if (SwingUtilities.isEventDispatchThread() || paintSynchronized == false)
    {
      sizeRunnable.run();
      return sizeRunnable.getRetval();
    }

    try
    {
      SwingUtilities.invokeAndWait(sizeRunnable);
      return sizeRunnable.getRetval();
    }
    catch (Exception e)
    {
      ComponentDrawable.logger.warn("Failed to compute the defined size.");
    }
    return new Dimension(0, 0);
  }

  /**
   * A private helper method that checks, whether the component provides an own peer.
   *
   * @return true, if the component has an own peer, false otherwise.
   */
  protected final boolean isOwnPeerConnected()
  {
    if (allowOwnPeer == false)
    {
      return false;
    }
    final Window windowAncestor = ComponentDrawable.getWindowAncestor(component);
    return (windowAncestor != null && windowAncestor != peerSupply);
  }

  /**
   * A private helper method that locates the Window to which the component is currently added.
   *
   * @param component the component for which the Window should be returned.
   * @return the AWT-Window that is the (possibly indirect) parent of this component.
   */
  protected static Window getWindowAncestor(final Component component)
  {
    Component parent = component.getParent();
    while (parent != null)
    {
      if (parent instanceof Window)
      {
        return (Window) parent;
      }
      parent = parent.getParent();
    }
    return null;
  }

  /**
   * Defines whether the layouter should preserve the aspect ratio of the component's preferred size.
   *
   * @param preserveAspectRatio true, if the aspect ratio should be preserved, false otherwise.
   */
  public void setPreserveAspectRatio(final boolean preserveAspectRatio)
  {
    this.preserveAspectRatio = preserveAspectRatio;
  }

  /**
   * Returns true, if this drawable will preserve an aspect ratio during the drawing.
   *
   * @return true, if an aspect ratio is preserved, false otherwise.
   */
  public boolean isPreserveAspectRatio()
  {
    return preserveAspectRatio;
  }

  /**
   * Draws the component.
   *
   * @param g2  the graphics device.
   * @param area  the area inside which the object should be drawn.
   */
  public void draw(final Graphics2D g2, final Rectangle2D area)
  {
    if (component == null)
    {
      return;
    }

    runnable.setArea(area);
    runnable.setGraphics(g2);

    if (SwingUtilities.isEventDispatchThread() || paintSynchronized == false)
    {
      runnable.run();
    }
    else
    {
      try
      {
        SwingUtilities.invokeAndWait(runnable);
      }
      catch (Exception e)
      {
        ComponentDrawable.logger.warn("Failed to redraw the component.");
      }
    }
  }

  /**
   * Prepares the component for drawing. This recursively disables the double-buffering as this would interfere
   * with the drawing.
   *
   * @param c the component that should be prepared.
   */
  private void prepareComponent(final Component c)
  {
    if (c instanceof JComponent)
    {
      final JComponent jc = (JComponent) c;
      jc.setDoubleBuffered(false);
      final Component[] childs = jc.getComponents();
      for (int i = 0; i < childs.length; i++)
      {
        final Component child = childs[i];
        prepareComponent(child);
      }
    }
  }
}
