/*****************************************************************************
 *                     Yumetech, Inc Copyright (c) 2006
 *                               Java Source
 *
 * This source is licensed under the BSD-style License
 * Please read http://www.opensource.org/licenses/bsd-license.php for more
 * information or docs/BSD.txt in the downloaded code.
 *
 * This software comes with the standard NO WARRANTY disclaimer for any
 * purpose. Use it at your own risk. If there's a problem you get to fix it.
 *
 ****************************************************************************/

package org.j3d.opengl.swt.draw2d;

// External imports
import javax.media.opengl.*;

import java.security.AccessController;
import java.security.PrivilegedAction;

import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;

// Local imports
// None

/**
 * A lightweight that represents a surface that OpenGL draws into.
 * <p>
 *
 * <b>Drawable Management</b>
 * <p>
 * The canvas automatically creates a GLDrawable object to correspond to this
 * canvas, and an accompanying GLContext instance. The GLContext instance, by
 * default is synchronized. It also has several other properties that are a
 * little non-standard.
 * </p>
 * <p>
 * In order to be as widely usable as possible, we don't guarantee that the
 * rendering is backed by a Pbuffer, frame buffer object or anything else.
 * We try to use, in order, FBO, Pbuffer and finally an ordinary offscreen
 * drawable object. Thus, the drawable that you can request from this surface
 * is a generic wrapper object that hides which of these renderables we've
 * ended up using under the covers.
 * </p>
 * <p>
 * This drawable wrapper acts just like any other drawable with one exception.
 * You can create others using shared contexts, add listeners, call repaint
 * and so forth. We however, will not let you resize it. The size of the
 * drawable is directly linked to the size of this figure. As the figure
 * changes size, we resize the underlying drawable to match. There are many
 * reasons we need to do this, including handling such this as driver bugs and
 * managing the repaint timing. Calling the setSize() method is silently
 * ignored.
 * </p>
 *
 * @author Justin Couch
 * @version $Revision: 1.7 $
 */
public class GLFigure extends Figure
{
    /** Message when the user didn't provide a device to the constructor */
    private static final String NULL_DEVICE_MSG =
        "A device instance is required to be provided to the contructor.";

    /** The handler of all the GL state, callbacks etc */
    private DrawableWrapper drawableWrapper;

    /** Do we have a pbuffer context to play with? */
    private boolean isInitialised;

    /** Flag set when either size or opacity has changed */
    private boolean needsReshape;

    /** User flag to say whether they are doing manual context management */
    private boolean useManualDrawing;

    /** Listener(s) for size change events */
    private GLFigureSizeListener sizeListener;

    /** Keeps track of non-zero dimension status */
    private boolean uselessSize;

    /** 
     * The current image to be drawn. Updated as required from the underlying context
     * using the contained inner class. 
     */
    private Image currentImage;

    /** Lock object for synchronisation between the image updates and image drawing */
    private Object swapLock;

    /**
     * Inner class definition used for the callback functionality. Hides this interface
     * from the outside world as we really don't want the end user's trying to set their
     * own images.
     */
    private class ImageCallback implements DrawableObserver
    {
        public void imageChanged(Image img)
        {
            synchronized(swapLock)
            {
                if(currentImage != null)
                    currentImage.dispose();

                 currentImage = img;
            }
        }
    }

    /**
     * Static constructor to make sure that the right system property is set
     * for our SWT-specific factory.
     */
    static
    {
        AccessController.doPrivileged(
            new PrivilegedAction ()
            {
                public Object run()
                {
                    System.setProperty("opengl.factory.class.name",
                                       "org.j3d.opengl.swt.SWTRIDrawableFactory");
                    return null;
                }
            }
        );
    }

    /**
     * Create a new isntance of this widget using the default GL capabilities.
     *
     * @param device The screen device context that holds the parent component.
     *    Must not be null
     * @throws IllegalArgumentException The device instance was null
     */
    public GLFigure(Display device)
        throws IllegalArgumentException
    {
        this(device, null, null, null);
    }

    /**
     * Create a new isntance of this widget
     *
     * @param device The screen device context that holds the parent component.
     *    Must not be null
     * @param sharedWith The GL context to share resources with. May be null
     * @throws IllegalArgumentException The device instance was null
     */
    public GLFigure(Display device, GLContext sharedWith)
        throws IllegalArgumentException
    {
        this(device, null, null, sharedWith);
    }

    /**
     * Create a new instance of this widget. The capabilities are defined from
     * the list of prefered options in the chooser. If no chooser is given, a
     * default implementation is used that picks the best option.
     *
     * @param device The screen device context that holds the parent component.
     *    Must not be null
     * @param capabilities A selection of the requested capabilities
     * @param chooser A chooser to help decide between requested and available
     *    screen and device capabilities
     * @param sharedWith The GL context to share resources with. May be null
     */
    public GLFigure(Display device,
                    GLCapabilities capabilities,
                    GLCapabilitiesChooser chooser,
                    GLContext sharedWith)
        throws IllegalArgumentException
    {
        if(device == null)
            throw new IllegalArgumentException(NULL_DEVICE_MSG);

        useManualDrawing = false;
        needsReshape = true;
        uselessSize = true;
        swapLock = new Object();

        drawableWrapper = new DrawableWrapper(device, 
                                              capabilities,
                                              chooser,
                                              sharedWith,
                                              new ImageCallback());
    }

    //----------------------------------------------------------
    // Methods defined by Figure
    //----------------------------------------------------------

// NOTE:
// Should we be overriding these too?
// setValid()
// setVisible()

    /**
     * Change the sige of this figure to the new dimensions. Subclasses that
     * override this method should make sure it is called so that we can resize
     * the OpenGL information appropriately.
     *
     * @param r The rectangle representing the bounds
     */
    public void setBounds(Rectangle r)
    {
        Rectangle b = getBounds();
        int old_w = b.width;
        int old_h = b.height;

        super.setBounds(r);

        uselessSize = (r.width == 0 || r.height == 0);

        // Now resize the pbuffer only if the size changed. Ignore the situation
        // when only the location changes.
        if((r.height != old_h) || (r.width != old_w))
            needsReshape = true;

        fireResizeEvent(r.width, r.height);
    }

    /**
     * Change the sige of this figure to the new dimensions. Subclasses that
     * override this method should make sure it is called so that we can resize
     * the OpenGL information appropriately.
     *
     * @param w The new width to use
     * @param h The new height to use
     */
    public void setSize(int w, int h)
    {
        super.setSize(w, h);

        uselessSize = (w == 0 || h == 0);

        // Now resize the pbuffer
        needsReshape = true;

        fireResizeEvent(w, h);
    }

    /**
     * Paint the contents of this figure now.
     */
    public void paintFigure(Graphics g)
    {
        // NOTE: must do this when the context is not current as it may
        // involve destroying the pbuffer (current context) and
        // re-creating it -- tricky to do properly while the context is
        // current
        if(needsReshape)
            processReshape();

        if(!useManualDrawing)
            drawableWrapper.display();

        synchronized(swapLock)
        {
            if((currentImage != null) && !currentImage.isDisposed())
            {
                Point loc = getLocation();
                g.translate(loc);

                // Draw resulting image in one shot. We assume that the figure is
                // always the same size as the underlying pbuffer. The JOGL RI
                // notes that NVidia cards have a lot of issues if you attempt to
                // read a series of bytes from the pbuffer when the read with is
                // not the same as the pbuffer width. So, for now we're just going
                // to resize the pbuffer every time the figure is resized.
                g.drawImage(currentImage, 0, 0);

                g.translate(loc.getNegated());
            }
        }
    }

    /*
     * Sets this IFigure to be opaque if opaque is true and transparent if
     * opaque is false.
     *
     * @param opaque true to make this non-transparent
     */
    public void setOpaque(boolean opaque)
    {
        if(opaque != isOpaque())
        {
            needsReshape = true;
        }

        super.setOpaque(opaque);
    }

    /**
     * Overridden to track when this component is added to the parent
     * figure and hence may be able to start up  OpenGL state. Subclasses
     * which override this method must call <code>super.addNotify()</code>
     * in their <code>addNotify()</code> method in order to function properly.
     */
    public void addNotify()
    {
        super.addNotify();

        // Tell the drawable to create the pbuffer now by setting the size to
        // the current size.
        Rectangle bounds = getBounds();
        drawableWrapper.resizeImage(bounds.width, bounds.height, isOpaque());
    }

    /**
     * Overridden to track when this component is removed from the parent
     * figure and hence may be able to throw away OpenGL state. Subclasses
     * which override this method must call <code>super.removeNotify()</code>
     * in their <code>removeNotify()</code> method in order to function properly.
     */
    public void removeNotify()
    {
        super.removeNotify();

        // Tell the drawable to dispose of the pbuffer now by setting the size
        // to zeroes.
        drawableWrapper.resizeImage(0, 0, false);
    }

    //----------------------------------------------------------
    // Local methods
    //----------------------------------------------------------

    /**
     * Fetch the device that this figure was created on. Useful for when you
     * are doing manual drawing and need to asynchronously schedule repaint
     * events onto the surface.
     *
     * @return The device instance this was created with
     */
    public Display getDisplay()
    {
        return drawableWrapper.getDisplay();
    }

    /**
     * Convenience method to see if it is worth attempting to draw to this
     * image, even when it is visible. Basically it checks the size of the
     * figure and if is non-zero in both dimensions.
     *
     * @return true if it is worthwhile attempting to draw to this figure
     */
    public boolean isZeroSized()
    {
        return uselessSize;
    }

    /**
     * Add a new listener for size. If the listener is already added, it is
     * ignored. A value of null is ignored.
     *
     * @param l The listener instance to add
     */
    public void addGLFigureSizeListener(GLFigureSizeListener l)
    {
        sizeListener = GLFigureSizeListenerMulticaster.add(sizeListener, l);
    }

    /**
     * Remove an existing listener for size. If the listener is not added,
     * the request is silently ignored. A value of null is ignored.
     *
     * @param l The listener instance to add
     */
    public void removeGLFigureSizeListener(GLFigureSizeListener l)
    {
        sizeListener = GLFigureSizeListenerMulticaster.remove(sizeListener, l);
    }

    /**
     * Get the GLContext instance that was created for this figure instance.
     *
     * @return The current context
     */
    public GLContext getGLContext()
    {
        return drawableWrapper.getContext();
    }

    /**
     * Get the drawable instance that was created for this figure instance.
     * This drawable will be a pbuffer that the underlying system renders to if
     * hardware support for pbuffers is available, otherwise an offscreen
     * renderable is used.
     *
     * @return The current context
     */
    public GLAutoDrawable getGLAutoDrawable()
    {
        return drawableWrapper;
    }

    /**
     * Notification that you wish to do your own manual context management and
     * buffer swapping.
     * <p>
     *
     * If you are going to do your own context management rather than using the
     * GLEventListener registered to the pbuffer, you will need to also make
     * sure that you call swapBuffers() on this figure so that the contents of
     * the underlying pbuffer is read in.
     */
    public void setManualDrawing(boolean enable)
    {
        useManualDrawing = enable;
    }

    /**
     * Check to see if manual drawing is being done.
     */
    public boolean isManualDrawing()
    {
        return useManualDrawing;
    }

    /**
     * Handle updating the reshaping of the underlying rendering surfaces.
     */
    private void processReshape()
    {
        Rectangle bounds = getBounds();
        drawableWrapper.resizeImage(bounds.width, bounds.height, isOpaque());

        needsReshape = false;
    }

    /**
     * Send a resize event off to the listeners.
     */
    private void fireResizeEvent(int width, int height)
    {
        if(sizeListener != null)
        {
            try
            {
                sizeListener.figureSizeChanged(this, width, height);
            }
            catch(Throwable th)
            {
                System.out.println(GLFigureSizeListenerMulticaster.SIZE_ERROR_MSG +
                                   sizeListener);
                System.out.println("Message: " + th.getMessage());
                th.printStackTrace();
            }
        }
    }
}
