/*****************************************************************************
 *                     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 java.awt.event.*;
import javax.media.opengl.*;

import java.beans.PropertyChangeListener;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;

import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.widgets.Display;

// Local imports
import org.j3d.opengl.swt.Threading;

/**
 * A lightweight that represents a surface that OpenGL draws into.
 * <p>
 *
 * The canvas automatically creates a GLDrawable object to correspond to this
 * canvas, and an accompanying GLContext instance. The GLContext instance, by
 * default is not synchronized.
 *
 * @author Justin Couch
 * @version $Revision: 1.11 $
 */
class DrawableWrapper
    implements GLAutoDrawable, GLEventListener
{
    /** Message when we fail to create a pbuffer */
    private static final String PBUFFER_CREATE_FAILURE_MSG =
        "Draw2D GLFigure unable to use pbuffers. Using software rendering instead.";

    /**
     * Whenever a call is made to access the pbuffer, but it failed to initialise
     * earlier.
     */
    private static final String FAILED_INIT_MSG =
        "Unable to process the request due to a previously failed pbuffer " +
        "creation attempt";

    /**
     * RGB palette definition used for creating the new image data each
     * frame.
     */
    private static final PaletteData RGB_PALETTE;

    /**
     * BGR palette definition used for creating the new image data each
     * frame.
     */
    private static final PaletteData BGR_PALETTE;

    /** The drawable we render to */
    private GLAutoDrawable realDrawable;

    /** The GL context object */
    private ContextWrapper contextWrapper;

    /** Our callback for when the buffers have been swapped */
    private DrawableObserver containingFigure;

    /** The device to create the images on */
    private Display onscreenDisplay;

    /** The main capabilities for creating new pbuffers */
    private GLCapabilities offscreenCaps;

    /** The chooser used when creating new pbuffers */
    private GLCapabilitiesChooser offscreenChooser;

    /** A GL Context that we share the pbuffer with */
    private GLContext sharedContext;

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

    /** The width to read back as pixels */
    private int imageWidth;

    /** The width to read back as pixels */
    private int imageHeight;

    /** The bit-depth of the image. Typically 24 or 32 bit */
    private int imageDepth;

    /** The selected palette type suitable for copying to the display */
    private PaletteData imagePalette;

    /** Flag to say that the image is opaque (true) or transparent (false) */
    private boolean isOpaque;

    /** Flag for the auto swap buffers mode */
    private boolean autoSwapImageBuffer;

    /** Flag to say that the image has changed size, send a reshape  */
    private boolean changedImage;

    /**
     * The listener(s) that are held by this wrapper. We call these as a result
     * of being called by the drawable that we're wrapping. We do some
     * processing either side of several of these calls to make sure the
     * drawable is properly mapped and copied at the correct times.
     */
    private GLEventListener listener;

    // For saving/restoring of OpenGL state during ReadPixels

    /** Value of GL_PACK_SWAP_BYTES */
    private int[] swapBytes;

    /** Value of */
    private int[] rowLength;

    /** Value of */
    private int[] skipRows;

    /** Value of */
    private int[] skipPixels;

    /** Value of */
    private int[] pixelAlignment;

    // One of either the int or byte form is used to store the read back pixels
    // before storing in the SWT Image

    /** Holder for pixel data if read as ints */
    private IntBuffer readBackInts;

    /** The format to read the pixels from the video card */
    private int glPixelFormat;

    /** The GL byte format type when reading the images */
    private int glDataType;

    /**
     * Static constructor to create the global palette information.
     */
    static
    {
        RGB_PALETTE = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF);
        BGR_PALETTE = new PaletteData(0x0000FF, 0x00FF00, 0xFF0000);
    }

    /**
     * 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 that we'll be creating the images
     *    on
     * @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
     * @param figure The figure that we call back when the buffers swap
     */
    DrawableWrapper(Display device,
                    GLCapabilities capabilities,
                    GLCapabilitiesChooser chooser,
                    GLContext sharedWith,
                    DrawableObserver figure)
    {
        onscreenDisplay = device;
        containingFigure = figure;

        // Works around problems on many vendors' cards; we don't need a
        // back buffer for the offscreen surface anyway
        if(capabilities != null)
            offscreenCaps = (GLCapabilities)capabilities.clone();
        else
            offscreenCaps = new GLCapabilities();

        offscreenCaps.setDoubleBuffered(false);

        offscreenChooser =
            (chooser != null) ? chooser : new DefaultGLCapabilitiesChooser();

        sharedContext = sharedWith;
        changedImage = false;
        isOpaque = true;
        autoSwapImageBuffer = true;

        swapBytes = new int[1];
        rowLength = new int[1];
        skipRows = new int[1];
        skipPixels = new int[1];
        pixelAlignment = new int[1];

        isInitialised = false;

        // This seems to be a good choice on all platforms
        glDataType = GL.GL_UNSIGNED_INT_8_8_8_8_REV;

        contextWrapper = new ContextWrapper(this);
    }

    //----------------------------------------------------------
    // Methods defined by GLEventListener
    //----------------------------------------------------------

    /**
     * Called by the drawable immediately after the OpenGL context is
     * initialized. Can be used to perform one-time OpenGL
     * initialization such as setup of lights and display lists. Note
     * that this method may be called more than once if the underlying
     * OpenGL context for the GLAutoDrawable is destroyed and
     * recreated, for example if a GLCanvas is removed from the widget
     * hierarchy and later added again.
     */
    public void init(GLAutoDrawable drawable)
    {
        try
        {
            if(listener != null)
                listener.init(this);
        }
        catch(Throwable th)
        {
            System.out.println(GLEventListenerMulticaster.INIT_ERROR_MSG +
                               listener);
            System.out.println("Message: " + th.getMessage());
            th.printStackTrace();
        }
    }

    /**
     * Called by the drawable to initiate OpenGL rendering by the
     * client. After all GLEventListeners have been notified of a
     * display event, the drawable will swap its buffers if {@link
     * GLAutoDrawable#setAutoSwapBufferMode setAutoSwapBufferMode} is
     * enabled.
     */
    public void display(GLAutoDrawable drawable)
    {
        // If the figure has been reshaped then we need to send that message
        // out to the real listeners now.

        if(changedImage)
            processReshape();

        if(!isInitialised)
            return;

        GL gl = getGL();
        gl.glDrawBuffer(GL.GL_FRONT);

        try
        {
            if(listener != null)
                listener.display(this);
        }
        catch(Throwable th)
        {
            System.out.println(GLEventListenerMulticaster.DISPLAY_ERROR_MSG +
                               listener);
            System.out.println("Message: " + th.getMessage());
            th.printStackTrace();
        }

        if(autoSwapImageBuffer)
            swapImages();
    }

    /**
     * Called by the drawable during the first repaint after the
     *  component has been resized. The client can update the viewport
     * and view volume of the window appropriately, for example by a
     * call to {@link javax.media.opengl.GL#glViewport}; note that for
     * convenience the component has already called <code>glViewport(x,
     * y, width, height)</code> when this method is called, so the
     * client may not have to do anything in this method.
     */
    public void reshape(GLAutoDrawable source,
                        int x,
                        int y,
                        int width,
                        int height)
    {
          // This is handled above and dispatched directly to the appropriate
          // context. We won't ever see a resize from the pbuffer, as the code
          // just replaces the pbuffer context when needed.
    }

    /**
     * Called by the drawable when the display mode or the display device
     * associated with the GLAutoDrawable has changed. The two boolean
     * parameters indicate the types of change(s) that have occurred.
     * <p>
     *  An example of a display <i>mode</i> change is when the bit depth
     * changes (e.g., from 32-bit to 16-bit color) on monitor upon which the
     * GLAutoDrawable is currently being displayed.
     * <p>
     * An example of a display <i>device</i> change is when the user drags the
     * window containing the GLAutoDrawable from one monitor to another in a
     * multiple-monitor setup. <p>
     * <p>
     * The reason that this function handles both types of changes (instead of
     * handling mode and device changes in separate methods) is so that
     * applications have the opportunity to respond to display changes in the most
     * efficient manner. For example, the application may need make fewer
     * adjustments to compensate for a device change if it knows that the mode
     * on the new device is identical the previous mode.<p>
     */
    public void displayChanged(GLAutoDrawable source,
                               boolean modeChanged,
                               boolean deviceChanged)
    {
        try
        {
            if(listener != null)
                listener.displayChanged(this, modeChanged, deviceChanged);
        }
        catch(Throwable th)
        {
            System.out.println(GLEventListenerMulticaster.CHANGE_ERROR_MSG +
                               listener);
            System.out.println("Message: " + th.getMessage());
            th.printStackTrace();
        }
    }

    //----------------------------------------------------------
    // Methods defined by GLAutoDrawable
    //----------------------------------------------------------

    /**
     * Causes OpenGL rendering to be performed for this GLAutoDrawable by
     * calling {@link GLEventListener#display display} for all registered
     * {@link GLEventListener}s. Called automatically by the window system
     * toolkit upon receiving a repaint() request. this routine may be called
     * manually for better control over the rendering process. It is legal to
     * call another GLAutoDrawable's display method from within the
     * {@link GLEventListener#display display} callback.
     */
    public void display()
    {
        if((realDrawable != null) && (getWidth() != 0) && (getHeight() != 0))
            realDrawable.display();
    }

    /**
     * Schedules a repaint of the component at some point in the future.
     */
    public void repaint()
    {
        display();
    }

    /**
     * Adds a {@link GLEventListener} to this drawable. If multiple listeners
     * are added to a given drawable, they are notified of events in an
     * arbitrary order.
     */
    public void addGLEventListener(GLEventListener l)
    {
         listener = GLEventListenerMulticaster.add(listener, l);
    }

    /**
     * Removes a {@link GLEventListener} from this drawable. Note that if this
     * is done from within a particular drawable's {@link GLEventListener}
     * handler (reshape, display, etc.) that it is not guaranteed that all
     * other listeners will be evaluated properly during this update cycle.
     */
    public void removeGLEventListener(GLEventListener l)
    {
         listener = GLEventListenerMulticaster.remove(listener, l);
    }

    /**
     * Enables or disables automatic buffer swapping for this drawable. By
     * default this property is set to true; when true, after all
     * GLEventListeners have been called for a display() event, the front and
     * back buffers are swapped, displaying the results of the render. When
     * disabled, the user is responsible for calling {@link #swapBuffers}
     * manually.
     */
    public boolean getAutoSwapBufferMode()
    {
        return autoSwapImageBuffer;
    }

    /**
     * Indicates whether automatic buffer swapping is enabled for this
     * drawable. See {@link #setAutoSwapBufferMode}.
     */
    public void setAutoSwapBufferMode(boolean onOrOff)
    {
        autoSwapImageBuffer = onOrOff;
    }

    /**
     * Returns the context associated with this drawable. The returned context
     * will be synchronized.
     */
    public GLContext getContext()
    {
        return contextWrapper;
    }

    /**
     * Returns the {@link GL} pipeline object this GLAutoDrawable uses. If this
     * method is called outside of the {@link GLEventListener}'s callback
     * methods (init, display, etc.) it may return null. Users should not rely
     * on the identity of the returned GL object; for example, users should not
     * maintain a hash table with the GL object as the key. Additionally, the
     * GL object should not be cached in client code, but should be re-fetched
     * from the GLAutoDrawable at the beginning of each call to init, display,
     * etc.
     */
    public GL getGL()
    {
        return contextWrapper.getGL();
    }

    /**
     * Sets the {@link GL} pipeline object this GLAutoDrawable uses.
     * This should only be called from within the GLEventListener's
     * callback methods, and usually only from within the init()
     * method, in order to install a composable pipeline. See the JOGL
     * demos for examples.
     */
    public void setGL(GL gl)
    {
        contextWrapper.setGL(gl);
    }

    //----------------------------------------------------------
    // Methods defined by GLDrawable
    //----------------------------------------------------------

    /**
     * Fetches the {@link GLCapabilities} corresponding to the chosen OpenGL
     * capabilities (pixel format / visual) for this drawable. Some drawables,
     * in particular on-screen drawables, may be created lazily; null is
     * returned if the drawable is not currently created or if its pixel
     * format has not been set yet.
     * <p>
     *
     * On some platforms, the pixel format is not directly associated with
     * the drawable; a best attempt is made to return a reasonable value in
     * this case.
     *
     * @return The capabilities that was chosen, or null if none yet
     */
    public GLCapabilities getChosenGLCapabilities()
    {
        GLCapabilities ret_val = null;

        if(realDrawable != null)
            ret_val = realDrawable.getChosenGLCapabilities();

        return ret_val;
    }

    /**
     * Creates a new context for drawing to this drawable that will
     * optionally share display lists and other server-side OpenGL
     * objects with the specified GLContext.
     * <P>
     * The GLContext <code>share</code> need not be associated with this
     * GLDrawable and may be null if sharing of display lists and other
     * objects is not desired. See the note in the overview
     * documentation on
     * <a href="../../../overview-summary.html#SHARING">context sharing</a>.
     */
    public GLContext createContext(GLContext shareWith)
    {
        if(!isInitialised)
            throw new GLException(FAILED_INIT_MSG);

        return realDrawable.createContext(shareWith);
    }

    /**
     * Indicates to on-screen GLDrawable implementations whether the
     * underlying window has been created and can be drawn into. This
     * method must be called from GLDrawables obtained from the
     * GLDrawableFactory via the {@link GLDrawableFactory#getGLDrawable
     * GLDrawableFactory.getGLDrawable()} method. It must typically be
     * called with an argument of <code>true</code> in the
     * <code>addNotify</code> method of components performing OpenGL
     * rendering and with an argument of <code>false</code> in the
     * <code>removeNotify</code> method. Calling this method has no
     * other effects. For example, if <code>removeNotify</code> is
     * called on a Canvas implementation for which a GLDrawable has been
     * created, it is also necessary to destroy all OpenGL contexts
     * associated with that GLDrawable. This is not done automatically
     * by the implementation. It is not necessary to call
     * <code>setRealized</code> on a GLCanvas, a GLJPanel, or a
     * GLPbuffer, as these perform the appropriate calls on their
     * underlying GLDrawables internally..
     */
    public void setRealized(boolean realized)
    {
        // Ignored for the wrapper. This is taken care of by the guts of the
        // GL implementation.
    }

    /**
     * Requests a new width and height for this GLDrawable. Not all
     * drawables are able to respond to this request and may silently
     * ignore it.
     */
    public void setSize(int width, int height)
    {
        // Ignored for this wrapper. We're doing the size management
        // under the covers.
    }

    /**
     * Get the current width of this GLDrawable.
     */
    public int getWidth()
    {
        if(!isInitialised)
            throw new GLException(FAILED_INIT_MSG);

        return realDrawable.getWidth();
    }

    /**
     * Get the current height of this GLDrawable.
     */
    public int getHeight()
    {
        if(!isInitialised)
            throw new GLException(FAILED_INIT_MSG);

        return realDrawable.getHeight();
    }

    /**
     * Swaps the front and back buffers of this drawable. For {@link
     * GLAutoDrawable} implementations, when automatic buffer swapping
     * is enabled (as is the default), this method is called
     * automatically and should not be called by the end user.
     *
     * Note that calling this assumes that the context is current for
     * the same thread that called the display or made the context
     * current.
     */
    public void swapBuffers()
    {
        if(!isInitialised)
            throw new GLException(FAILED_INIT_MSG);

        if(!autoSwapImageBuffer)
            swapImages();
    }


    //----------------------------------------------------------
    // Methods defined by ComponentEvents
    //----------------------------------------------------------

    // All these are empty because we don't do anything with them. Stupid JSR
    // design decision.

    public void addComponentListener(ComponentListener l)
    {
    }

    public void removeComponentListener(ComponentListener l)
    {
    }

    public void addFocusListener(FocusListener l)
    {
    }

    public void removeFocusListener(FocusListener l)
    {
    }

    public void addHierarchyBoundsListener(HierarchyBoundsListener l)
    {
    }

    public void removeHierarchyBoundsListener(HierarchyBoundsListener l)
    {
    }

    public void addHierarchyListener(HierarchyListener l)
    {
    }

    public void removeHierarchyListener(HierarchyListener l)
    {
    }

    public void addInputMethodListener(InputMethodListener l)
    {
    }

    public void removeInputMethodListener(InputMethodListener l)
    {
    }

    public void addKeyListener(KeyListener l)
    {
    }

    public void removeKeyListener(KeyListener l)
    {
    }

    public void addMouseListener(MouseListener l)
    {
    }

    public void removeMouseListener(MouseListener l)
    {
    }

    public void addMouseMotionListener(MouseMotionListener l)
    {
    }

    public void removeMouseMotionListener(MouseMotionListener l)
    {
    }

    public void addMouseWheelListener(MouseWheelListener l)
    {
    }

    public void removeMouseWheelListener(MouseWheelListener l)
    {
    }

    public void addPropertyChangeListener(PropertyChangeListener listener)
    {
    }

    public void removePropertyChangeListener(PropertyChangeListener listener)
    {
    }

    public void addPropertyChangeListener(String propertyName,
                                          PropertyChangeListener listener)
    {
    }

    public void removePropertyChangeListener(String propertyName,
                                       PropertyChangeListener listener)
    {
    }


    //----------------------------------------------------------
    // 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
     */
    Display getDisplay()
    {
        return onscreenDisplay;
    }

    /**
     * The image has changed size, so here is the new image instance to use.
     * Note that we assume, for sanity's sake, that the image is always a
     * direct colour model. If the pbuffer creation failed (eg the underlying
     * video card does not support them), then this will return false, allowing
     * the user to not proceed any further.
     *
     * @param width The width of the new image
     * @param height The height of the new image
     * @param opaque Set true if this image is to be treated
     * @return true if the image resize succeeded, false otherwise
     */
    void resizeImage(int width, int height, boolean opaque)
    {
        // Destroy the old pbuffer first.
        destroyPbuffer();

        isOpaque = opaque;
        imageWidth = width;
        imageHeight = height;
        imageDepth = isOpaque ? 24 : 32;
        changedImage = true;

        // No point continuing if there is nothing here.
        if(imageWidth == 0 || imageWidth == 0)
            return;

        if(isOpaque)
        {
            imagePalette = RGB_PALETTE;
            glPixelFormat = GL.GL_BGR;
        }
        else
        {
            imagePalette = RGB_PALETTE;
            glPixelFormat = GL.GL_BGRA;
        }

        readBackInts = IntBuffer.allocate(imageWidth * imageHeight);

        createPbuffer();
    }

    /**
     * Send a reshape command to the various listeners
     */
    private void processReshape()
    {
        try
        {
            getGL().glViewport(0, 0, imageWidth, imageHeight);

            if(listener != null)
                listener.reshape(this, 0, 0, imageWidth, imageHeight);

            changedImage = false;
        }
        catch(Throwable th)
        {
            System.out.println(GLEventListenerMulticaster.INIT_ERROR_MSG +
                               listener);
            System.out.println("Message: " + th.getMessage());
            th.printStackTrace();
        }
    }

    /**
     * Process the swap from the back image to the front.
     */
    private void swapImages()
    {
        GL gl = getGL();

        if(gl == null || !isInitialised)
            return;

        // Save current modes
        gl.glGetIntegerv(GL.GL_PACK_SWAP_BYTES, swapBytes, 0);
        gl.glGetIntegerv(GL.GL_PACK_ROW_LENGTH, rowLength, 0);
        gl.glGetIntegerv(GL.GL_PACK_SKIP_ROWS, skipRows, 0);
        gl.glGetIntegerv(GL.GL_PACK_SKIP_PIXELS, skipPixels, 0);
        gl.glGetIntegerv(GL.GL_PACK_ALIGNMENT, pixelAlignment, 0);

        gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, GL.GL_FALSE);
        gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, imageWidth);
        gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS, 0);
        gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, 0);
        gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, 4);

        // Actually read the pixels.
        gl.glReadBuffer(GL.GL_FRONT);

        ImageData data = new ImageData(imageWidth,
                                       imageHeight,
                                       imageDepth,
                                       imagePalette);

        // Use the int version
        gl.glReadPixels(0,
                        0,
                        imageWidth,
                        imageHeight,
                        glPixelFormat,
                        glDataType,
                        readBackInts);

        // Restore saved modes.
        gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, swapBytes[0]);
        gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, rowLength[0]);
        gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS,  skipRows[0]);
        gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, skipPixels[0]);
        gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, pixelAlignment[0]);

        int[] src = readBackInts.array();
        int pos = 0;
        for(int i = imageHeight - 1; i >= 0; i--, pos += imageWidth)
            data.setPixels(0, i, imageWidth, src, pos);

        if(!onscreenDisplay.isDisposed())
        {
            Image img  = new Image(onscreenDisplay, data);
            containingFigure.imageChanged(img);
        }
    }

    /**
     * Create a new pbuffer now based on the current image width and
     * size.
     */
    private void createPbuffer()
    {
        GLDrawableFactory fac = GLDrawableFactory.getFactory();

        if(fac.canCreateGLPbuffer())
        {
            try
            {
                realDrawable = fac.createGLPbuffer(offscreenCaps,
                                                   null,
                                                   imageWidth,
                                                   imageHeight,
                                                   sharedContext);
                realDrawable.addGLEventListener(this);

                contextWrapper.changeWrappedContext(realDrawable.getContext());
                isInitialised = true;
            }
            catch(GLException e)
            {
                System.err.println(PBUFFER_CREATE_FAILURE_MSG);
                isInitialised = false;
            }
        }
    }

    /**
     * Destroy the current drawable, if there is one.
     */
    private void destroyPbuffer()
    {
        if(realDrawable != null)
        {
            isInitialised = false;
            realDrawable.removeGLEventListener(this);
            contextWrapper.changeWrappedContext(null);
            if(realDrawable instanceof GLPbuffer)
                ((GLPbuffer)realDrawable).destroy();

            realDrawable = null;
        }
    }
}
