/**
 * Copyright 2013 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 * 
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 * 
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 * 
 * ---------------------
 * 
 * Based on Brian Paul's tile rendering library, found
 * at <a href = "http://www.mesa3d.org/brianp/TR.html">http://www.mesa3d.org/brianp/TR.html</a>.
 * 
 * Copyright (C) 1997-2005 Brian Paul. 
 * Licensed under BSD-compatible terms with permission of the author. 
 * See LICENSE.txt for license information.
 */
package com.jogamp.opengl.util;

import javax.media.nativewindow.util.Dimension;
import javax.media.nativewindow.util.DimensionImmutable;
import javax.media.opengl.GL2ES3;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;

/**
 * A fairly direct port of Brian Paul's tile rendering library, found
 * at <a href = "http://www.mesa3d.org/brianp/TR.html">
 * http://www.mesa3d.org/brianp/TR.html </a> . I've java-fied it, but
 * the functionality is the same.
 * <p>
 * Original code Copyright (C) 1997-2005 Brian Paul. Licensed under
 * BSD-compatible terms with permission of the author. See LICENSE.txt
 * for license information.
 * </p>
 * <p>
 * Enhanced for {@link GL2ES3}, abstracted to suit {@link TileRenderer} and {@link RandomTileRenderer}.
 * </p>
 * <a name="pmvmatrix"><h5>PMV Matrix Considerations</h5></a>
 * <p>
 * The PMV matrix needs to be reshaped in user code
 * after calling {@link #beginTile(GL2ES3)}, See {@link #beginTile(GL2ES3)}.
 * </p>
 * <p> 
 * If {@link #attachToAutoDrawable(GLAutoDrawable) attaching to} an {@link GLAutoDrawable},
 * the {@link GLEventListener#reshape(GLAutoDrawable, int, int, int, int)} method
 * is being called after {@link #beginTile(GL2ES3)}.
 * It's implementation shall reshape the PMV matrix according to {@link #beginTile(GL2ES3)}. 
 * </p>
 * 
 * @author ryanm, sgothel
 */
public abstract class TileRendererBase {
    /**
     * The width of the final image. See {@link #getParam(int)}.
     */
    public static final int TR_IMAGE_WIDTH = 1;
    /**
     * The height of the final image. See {@link #getParam(int)}.
     */
    public static final int TR_IMAGE_HEIGHT = 2;
    /**
     * The width of the current tile. See {@link #getParam(int)}.
     */
    public static final int TR_CURRENT_TILE_X_POS = 3;
    /**
     * The height of the current tile. See {@link #getParam(int)}.
     */
    public static final int TR_CURRENT_TILE_Y_POS = 4;
    /**
     * The width of the current tile. See {@link #getParam(int)}.
     */
    public static final int TR_CURRENT_TILE_WIDTH = 5;
    /**
     * The height of the current tile. See {@link #getParam(int)}.
     */
    public static final int TR_CURRENT_TILE_HEIGHT = 6;
    
    /** 
     * Notifies {@link GLEventListener} implementing this interface
     * that the owning {@link GLAutoDrawable} is {@link TileRendererBase#attachToAutoDrawable(GLAutoDrawable) attached} 
     * to a tile renderer or {@link TileRendererBase#detachFromAutoDrawable() detached} from it.
     */
    public static interface TileRendererNotify {
        /** The owning {@link GLAutoDrawable} is {@link TileRendererBase#attachToAutoDrawable(GLAutoDrawable) attached} to a {@link TileRendererBase}. */
        public void addTileRendererNotify(TileRendererBase tr);
        /** The owning {@link GLAutoDrawable} is {@link TileRendererBase#detachFromAutoDrawable() detached} from a {@link TileRendererBase}. */
        public void removeTileRendererNotify(TileRendererBase tr);
    }
    
    protected final Dimension imageSize = new Dimension(0, 0);
    protected final GLPixelStorageModes psm = new GLPixelStorageModes();
    protected GLPixelBuffer imageBuffer;
    protected GLPixelBuffer tileBuffer;
    protected boolean beginCalled = false;
    protected int currentTileXPos;
    protected int currentTileYPos;
    protected int currentTileWidth;
    protected int currentTileHeight;
    protected GLAutoDrawable glad;
    protected GLEventListener[] listeners;
    protected boolean[] listenersInit;
    protected GLEventListener glEventListenerPre = null;
    protected GLEventListener glEventListenerPost = null;

    public String toString() {
        final int gladListenerCount = null != listeners ? listeners.length : 0;
        return getClass().getSimpleName()+
                "[tile["+currentTileXPos+"/"+currentTileYPos+" "+currentTileWidth+"x"+currentTileHeight+", buffer "+tileBuffer+"], "+
                ", image[size "+imageSize+", buffer "+imageBuffer+"], glad["+
                gladListenerCount+" listener, pre "+(null!=glEventListenerPre)+", post "+(null!=glEventListenerPost)+"]]";
    }
    
    protected TileRendererBase() {
    }

    /**
     * Gets the parameters of this TileRenderer object
     * 
     * @param pname The parameter name that is to be retrieved
     * @return the value of the parameter
     * @throws IllegalArgumentException if <code>pname</code> is not handled
     */
    public abstract int getParam(int pname) throws IllegalArgumentException;
    
    /**
     * Specify a buffer the tiles to be copied to. This is not
     * necessary for the creation of the final image, but useful if you
     * want to inspect each tile in turn.
     * 
     * @param buffer The buffer itself. Must be large enough to contain a random tile
     */
    public final void setTileBuffer(GLPixelBuffer buffer) {
        tileBuffer = buffer;
    }

    /** @see #setTileBuffer(GLPixelBuffer) */
    public final GLPixelBuffer getTileBuffer() { return tileBuffer; }

    /**
     * Sets the desired size of the final image
     * 
     * @param width The width of the final image
     * @param height The height of the final image
     */
    public final void setImageSize(int width, int height) {
        imageSize.setWidth(width);
        imageSize.setHeight(height);
    }

    /** @see #setImageSize(int, int) */
    public final DimensionImmutable getImageSize() { return imageSize; }

    /**
     * Sets the buffer in which to store the final image
     * 
     * @param buffer the buffer itself, must be large enough to hold the final image
     */
    public final void setImageBuffer(GLPixelBuffer buffer) {
        imageBuffer = buffer;
    }

    /** @see #setImageBuffer(GLPixelBuffer) */
    public final GLPixelBuffer getImageBuffer() { return imageBuffer; }

    /**
     * Begins rendering a tile.
     * <p>
     * Methods modifies the viewport, see below.
     * User shall reset the viewport when finishing all tile rendering,
     * i.e. after very last call of {@link #endTile(GL2ES3)}!
     * </p>
     * <p>
     * The <a href="#pmvmatrix">PMV Matrix</a>
     * must be reshaped after this call using:
     * <ul>
     *   <li>Current Viewport
     *   <ul>
     *      <li>x 0</li>
     *      <li>y 0</li>
     *      <li>{@link #TR_CURRENT_TILE_WIDTH tile width}</li>
     *      <li>{@link #TR_CURRENT_TILE_HEIGHT tile height}</li>
     *   </ul></li>
     *   <li>{@link #TR_CURRENT_TILE_X_POS tile x-pos}</li>
     *   <li>{@link #TR_CURRENT_TILE_Y_POS tile y-pos}</li>
     *   <li>{@link #TR_IMAGE_WIDTH image width}</li>
     *   <li>{@link #TR_IMAGE_HEIGHT image height}</li>
     * </ul>
     * </p>
     * <p>
     * Use shall render the scene afterwards, concluded with a call to
     * this renderer {@link #endTile(GL2ES3)}. 
     * </p>
     * 
     * @param gl The gl context
     * @throws IllegalStateException if image-size or pmvMatrixCB has not been set
     */
    public abstract void beginTile(GL2ES3 gl) throws IllegalStateException;
    
    /**
     * Must be called after rendering the scene,
     * see {@link #beginTile(GL2ES3)}.
     * 
     * @param gl the gl context
     * @throws IllegalStateException if beginTile(gl) has not been called
     */
    public abstract void endTile( GL2ES3 gl ) throws IllegalStateException;
    
    /**
     * Attaches this renderer to the {@link GLAutoDrawable}.
     * <p>
     * The {@link GLAutoDrawable}'s original {@link GLEventListener} are moved to this tile renderer.<br>
     * It is <i>highly recommended</i> that the original {@link GLEventListener} implement 
     * {@link TileRendererNotify}, so they get {@link TileRendererNotify#addTileRendererNotify(TileRendererBase) notified}
     * about this event.
     * </p>
     * <p>
     * This tile renderer's {@link GLEventListener} is then added to handle the tile rendering
     * for the original {@link GLEventListener}, i.e. it's {@link GLEventListener#display(GLAutoDrawable) display} issues:
     * <ul>
     *   <li>Optional {@link #setGLEventListener(GLEventListener, GLEventListener) pre-glel}.{@link GLEventListener#display(GLAutoDrawable) display(..)}</li>
     *   <li>{@link #beginTile(GL2ES3)}</li>
     *   <li>for all original {@link GLEventListener}:
     *   <ul> 
     *     <li>{@link GLEventListener#reshape(GLAutoDrawable, int, int, int, int) reshape(0, 0, tile-width, tile-height)}</li>
     *     <li>{@link GLEventListener#display(GLAutoDrawable) display(autoDrawable)}</li>
     *   </ul></li>
     *   <li>{@link #endTile(GL2ES3)}</li>
     *   <li>Optional {@link #setGLEventListener(GLEventListener, GLEventListener) post-glel}.{@link GLEventListener#display(GLAutoDrawable) display(..)}</li>
     * </ul>
     * </p>
     * <p>
     * The <a href="#pmvmatrix">PMV Matrix</a> shall be reshaped in the 
     * original {@link GLEventListener}'s {@link GLEventListener#reshape(GLAutoDrawable, int, int, int, int) reshape} method
     * according to the tile-position, -size and image-size<br>
     * The {@link GLEventListener#reshape(GLAutoDrawable, int, int, int, int) reshape} method is called for each tile 
     * w/ the current viewport of tile-size, where the tile-position and image-size can be retrieved by this tile renderer,
     * see  details in {@link #beginTile(GL2ES3)}.<br>
     * The original {@link GLEventListener} implementing {@link TileRendererNotify} is aware of this
     * tile renderer instance.
     * </p>
     * <p>
     * Consider using {@link #setGLEventListener(GLEventListener, GLEventListener)} to add pre- and post
     * hooks to be performed on this renderer {@link GLEventListener}.<br>
     * The pre-hook is able to allocate memory and setup parameters, since it's called before {@link #beginTile(GL2ES3)}.<br>
     * The post-hook is able to use the rendering result and can even shutdown tile-rendering,
     * since it's called after {@link #endTile(GL2ES3)}.
     * </p>
     * <p>
     * Call {@link #detachFromAutoDrawable()} to remove this renderer from the {@link GLAutoDrawable} 
     * and to restore it's original {@link GLEventListener}.
     * </p>
     * @param glad
     * @throws IllegalStateException if an {@link GLAutoDrawable} is already attached
     */
    public void attachToAutoDrawable(GLAutoDrawable glad) throws IllegalStateException {
        if( null != this.glad ) {
            throw new IllegalStateException("GLAutoDrawable already attached");
        }
        this.glad = glad;
        
        final int aSz = glad.getGLEventListenerCount();
        listeners = new GLEventListener[aSz];
        listenersInit = new boolean[aSz];
        for(int i=0; i<aSz; i++) {
            final GLEventListener l = glad.getGLEventListener(0);
            listenersInit[i] = glad.getGLEventListenerInitState(l);
            listeners[i] = glad.removeGLEventListener( l );
            if( listeners[i] instanceof TileRendererNotify ) {
                ((TileRendererNotify)listeners[i]).addTileRendererNotify(this);
            }
        }
        glad.addGLEventListener(tiledGLEL);
    }

    /**
     * Detaches this renderer from the {@link GLAutoDrawable}.
     * <p>
     * It is <i>highly recommended</i> that the original {@link GLEventListener} implement 
     * {@link TileRendererNotify}, so they get {@link TileRendererNotify#removeTileRendererNotify(TileRendererBase) notified}
     * about this event.
     * </p>
     * <p>
     * See {@link #attachToAutoDrawable(GLAutoDrawable)}.
     * </p>
     */
    public void detachFromAutoDrawable() {
        if( null != glad ) {
            glad.removeGLEventListener(tiledGLEL);
            final int aSz = listenersInit.length;
            for(int i=0; i<aSz; i++) {
                final GLEventListener l = listeners[i];
                if( l instanceof TileRendererNotify ) {
                    ((TileRendererNotify)l).removeTileRendererNotify(this);
                }
                glad.addGLEventListener(l);
                glad.setGLEventListenerInitState(l, listenersInit[i]);
            }
            listeners = null;
            listenersInit = null;
            glad = null;
        }
    }
    
    /**
     * Set {@link GLEventListener} for pre- and post operations when used w/ 
     * {@link #attachAutoDrawable(GLAutoDrawable, int, PMVMatrixCallback)}
     * for each {@link GLEventListener} callback.
     * @param preTile the pre operations
     * @param postTile the post operations
     */
    public void setGLEventListener(GLEventListener preTile, GLEventListener postTile) {
        glEventListenerPre = preTile;
        glEventListenerPost = postTile;
    }
    
    /**
     * Rendering one tile, by simply calling {@link GLAutoDrawable#display()}.
     * 
     * @throws IllegalStateException if no {@link GLAutoDrawable} is {@link #attachToAutoDrawable(GLAutoDrawable) attached}
     *                               or imageSize is not set
     */
    public void display() throws IllegalStateException {
        if( null == glad ) {
            throw new IllegalStateException("No GLAutoDrawable attached");
        }
        glad.display();
    }
    
    private final GLEventListener tiledGLEL = new GLEventListener() {
        @Override
        public void init(GLAutoDrawable drawable) {
            if( null != glEventListenerPre ) {
                glEventListenerPre.init(drawable);
            }
            final int aSz = listenersInit.length;
            for(int i=0; i<aSz; i++) {
                final GLEventListener l = listeners[i];
                l.init(drawable);
                listenersInit[i] = true;
            }
            if( null != glEventListenerPost ) {
                glEventListenerPost.init(drawable);
            }
        }
        @Override
        public void dispose(GLAutoDrawable drawable) {
            if( null != glEventListenerPre ) {
                glEventListenerPre.dispose(drawable);
            }
            final int aSz = listenersInit.length;
            for(int i=0; i<aSz; i++) {
                listeners[i].dispose(drawable);
            }
            if( null != glEventListenerPost ) {
                glEventListenerPost.dispose(drawable);
            }
        }
        @Override
        public void display(GLAutoDrawable drawable) {
            if( null != glEventListenerPre ) {
                glEventListenerPre.display(drawable);
            }
            final GL2ES3 gl = drawable.getGL().getGL2ES3();

            beginTile(gl);

            final int aSz = listenersInit.length;
            for(int i=0; i<aSz; i++) {
                listeners[i].reshape(drawable, 0, 0, currentTileWidth, currentTileHeight);
                listeners[i].display(drawable);
            }

            endTile(gl);
            if( null != glEventListenerPost ) {
                glEventListenerPost.display(drawable);
            }
        }
        @Override
        public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
            if( null != glEventListenerPre ) {
                glEventListenerPre.reshape(drawable, x, y, width, height);
            }
            final int aSz = listenersInit.length;
            for(int i=0; i<aSz; i++) {
                listeners[i].reshape(drawable, x, y, width, height);
            }
            if( null != glEventListenerPost ) {
                glEventListenerPost.reshape(drawable, x, y, width, height);
            }
        }
    };
}