/*
Copyright (C) 2001, 2006 United States Government
as represented by the Administrator of the
National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.render;

import com.sun.opengl.util.*;
import com.sun.opengl.util.texture.*;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.terrain.*;
import gov.nasa.worldwind.util.Logging;

import javax.media.opengl.*;
import java.nio.*;
import java.util.*;
import java.util.logging.Level;

/**
 * @author tag
 * @version $Id: SurfaceTileRenderer.java 12641 2009-09-24 08:45:41Z dcollins $
 */
public abstract class SurfaceTileRenderer implements Disposable
{
    private static final int DEFAULT_ALPHA_TEXTURE_SIZE = 1024;

    protected Texture alphaTexture;
    protected Texture outlineTexture;
    
    private boolean showImageTileOutlines = false;

    /**
     * Free internal resources held by this surface tile renderer.
     * A GL context must be current when this method is called.
     *
     * @throws javax.media.opengl.GLException - If an OpenGL context is not current when this method is called.
     */
    public void dispose()
    {
        if (GLContext.getCurrent() == null)
            return;
        
        if (this.alphaTexture != null)
            this.alphaTexture.dispose();
        this.alphaTexture = null;
        if (this.outlineTexture != null)
            this.outlineTexture.dispose();
        this.outlineTexture = null;
    }

    public boolean isShowImageTileOutlines()
    {
        return showImageTileOutlines;
    }

    public void setShowImageTileOutlines(boolean showImageTileOutlines)
    {
        this.showImageTileOutlines = showImageTileOutlines;
    }

    public void renderTile(DrawContext dc, SurfaceTile tile)
    {
        if (tile == null)
        {
            String message = Logging.getMessage("nullValue.TileIsNull");
            Logging.logger().severe(message);
            throw new IllegalStateException(message);
        }

        ArrayList<SurfaceTile> al = new ArrayList<SurfaceTile>(1);
        al.add(tile);
        this.renderTiles(dc, al);
        al.clear();
    }

    protected static class Transform
    {
        double HScale;
        double VScale;
        double HShift;
        double VShift;
        double rotationDegrees;
    }

    abstract protected void preComputeTextureTransform(DrawContext dc, SectorGeometry sg, Transform t);

    abstract protected void computeTextureTransform(DrawContext dc, SurfaceTile tile, Transform t);

    abstract protected Iterable<SurfaceTile> getIntersectingTiles(DrawContext dc, SectorGeometry sg,
                                                                  Iterable<? extends SurfaceTile> tiles);

    public void renderTiles(DrawContext dc, Iterable<? extends SurfaceTile> tiles)
    {
        if (tiles == null)
        {
            String message = Logging.getMessage("nullValue.TileIterableIsNull");
            Logging.logger().severe(message);
            throw new IllegalStateException(message);
        }

        if (dc == null)
        {
            String message = Logging.getMessage("nullValue.DrawContextIsNull");
            Logging.logger().severe(message);
            throw new IllegalStateException(message);
        }

        GL gl = dc.getGL();

        gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT // for alpha func
            | GL.GL_ENABLE_BIT
            | GL.GL_CURRENT_BIT
            | GL.GL_DEPTH_BUFFER_BIT // for depth func
            | GL.GL_TEXTURE_BIT // for texture env
            | GL.GL_TRANSFORM_BIT);

        try
        {
            this.alphaTexture = dc.getTextureCache().get(this);
            if (this.alphaTexture == null)
            {
                this.initAlphaTexture(DEFAULT_ALPHA_TEXTURE_SIZE); // TODO: choose size to match incoming tile sizes?
                dc.getTextureCache().put(this, this.alphaTexture);
            }

            boolean showOutlines = this.showImageTileOutlines && dc.getGLRuntimeCapabilities().getNumTextureUnits() > 2;
            if (showOutlines && this.outlineTexture == null)
                this.initOutlineTexture(128);

            gl.glEnable(GL.GL_DEPTH_TEST);
            gl.glDepthFunc(GL.GL_LEQUAL);

            gl.glEnable(GL.GL_ALPHA_TEST);
            gl.glAlphaFunc(GL.GL_GREATER, 0.01f);

            gl.glActiveTexture(GL.GL_TEXTURE0);
            gl.glEnable(GL.GL_TEXTURE_2D);
            gl.glMatrixMode(GL.GL_TEXTURE);
            gl.glPushMatrix();
            if (!dc.isPickingMode())
            {
                gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE);
            }
            else
            {
                gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_COMBINE);
                gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_SRC0_RGB, GL.GL_PREVIOUS);
                gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_RGB, GL.GL_REPLACE);
            }

            int numTexUnitsUsed = 2;
            int alphaTextureUnit = GL.GL_TEXTURE1;
            if (showOutlines)
            {
                numTexUnitsUsed = 3;
                alphaTextureUnit = GL.GL_TEXTURE2;
                gl.glActiveTexture(GL.GL_TEXTURE1);
                gl.glEnable(GL.GL_TEXTURE_2D);
                gl.glMatrixMode(GL.GL_TEXTURE);
                gl.glPushMatrix();
                gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_ADD);
            }

            gl.glActiveTexture(alphaTextureUnit);
            gl.glEnable(GL.GL_TEXTURE_2D);
            gl.glMatrixMode(GL.GL_TEXTURE);
            gl.glPushMatrix();
            gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE);

            // For each current geometry tile, find the intersecting image tiles and render the geometry
            // tile once for each intersecting image tile.
            Transform transform = new Transform();
            for (SectorGeometry sg : dc.getSurfaceGeometry())
            {
                Iterable<SurfaceTile> tilesToRender = this.getIntersectingTiles(dc, sg, tiles);
                if (tilesToRender == null)
                    continue;

                // Pre-load info to compute the texture transform
                this.preComputeTextureTransform(dc, sg, transform);

                // For each interesecting tile, establish the texture transform necessary to map the image tile
                // into the geometry tile's texture space. Use an alpha texture as a mask to prevent changing the
                // frame buffer where the image tile does not overlap the geometry tile. Render both the image and
                // alpha textures via multi-texture rendering.
                // TODO: Figure out how to apply multi-texture to more than one tile at a time. Use fragment shader?
                for (SurfaceTile tile : tilesToRender)
                {
                    gl.glActiveTexture(GL.GL_TEXTURE0);

                    if (tile.bind(dc))
                    {
                        gl.glMatrixMode(GL.GL_TEXTURE);
                        gl.glLoadIdentity();
                        tile.applyInternalTransform(dc);

                        // Determine and apply texture transform to map image tile into geometry tile's texture space
                        this.computeTextureTransform(dc, tile, transform);
                        gl.glScaled(transform.HScale, transform.VScale, 1d);
                        gl.glTranslated(transform.HShift, transform.VShift, 0d);

                        if (showOutlines)
                        {
                            gl.glActiveTexture(GL.GL_TEXTURE1);
                            this.outlineTexture.bind();

                            // Apply the same texture transform to the outline texture. The outline textures uses a
                            // different texture unit than the tile, so the transform made above does not carry over.
                            gl.glMatrixMode(GL.GL_TEXTURE);
                            gl.glLoadIdentity();
                            gl.glScaled(transform.HScale, transform.VScale, 1d);
                            gl.glTranslated(transform.HShift, transform.VShift, 0d);
                        }

                        // Prepare the alpha texture to be used as a mask where texture coords are outside [0,1]
                        gl.glActiveTexture(alphaTextureUnit);
                        this.alphaTexture.bind();

                        // Apply the same texture transform to the alpha texture. The alpha texture uses a
                        // different texture unit than the tile, so the transform made above does not carry over.
                        gl.glMatrixMode(GL.GL_TEXTURE);
                        gl.glLoadIdentity();
                        gl.glScaled(transform.HScale, transform.VScale, 1d);
                        gl.glTranslated(transform.HShift, transform.VShift, 0d);

                        // Render the geometry tile
                        sg.renderMultiTexture(dc, numTexUnitsUsed);
                    }
                }
            }

            gl.glActiveTexture(alphaTextureUnit);
            gl.glMatrixMode(GL.GL_TEXTURE);
            gl.glPopMatrix();
            gl.glDisable(GL.GL_TEXTURE_2D);

            gl.glActiveTexture(GL.GL_TEXTURE0);
            gl.glMatrixMode(GL.GL_TEXTURE);
            gl.glPopMatrix();
            gl.glDisable(GL.GL_TEXTURE_2D);

            if (showOutlines)
            {
                gl.glActiveTexture(GL.GL_TEXTURE1);
                gl.glMatrixMode(GL.GL_TEXTURE);
                gl.glPopMatrix();
                gl.glDisable(GL.GL_TEXTURE_2D);
            }
        }
        catch (Exception e)
        {
            Logging.logger().log(Level.SEVERE,
                Logging.getMessage("generic.ExceptionWhileRenderingLayer", this.getClass().getName()), e);
        }
        finally
        {
            // TODO: pop matrix stack too, for all texture units
            gl.glPopAttrib();
        }
    }

    private static void fillByteBuffer(ByteBuffer buffer, byte value)
    {
        for (int i = 0; i < buffer.capacity(); i++)
        {
            buffer.put(value);
        }
    }

    protected void initAlphaTexture(int size)
    {
        ByteBuffer textureBytes = BufferUtil.newByteBuffer(size * size);
        fillByteBuffer(textureBytes, (byte) 0xff);

        TextureData textureData = new TextureData(GL.GL_ALPHA, size, size, 0, GL.GL_ALPHA,
            GL.GL_UNSIGNED_BYTE, false, false, false, textureBytes.rewind(), null);
        this.alphaTexture = TextureIO.newTexture(textureData);

        this.alphaTexture.bind();
        this.alphaTexture.setTexParameteri(GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
        this.alphaTexture.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
        this.alphaTexture.setTexParameteri(GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_BORDER);
        this.alphaTexture.setTexParameteri(GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_BORDER);
        // Assume the default border color of (0, 0, 0, 0).
    }

    protected void initOutlineTexture(int size)
    {
        ByteBuffer textureBytes = BufferUtil.newByteBuffer(size * size);
        for (int row = 0; row < size; row++)
        {
            for (int col = 0; col < size; col++)
            {
                byte p;
                if (row == 0 || col == 0 || row == size - 1 || col == size - 1)
                    p = (byte) 0xff;
                else
                    p = (byte) 0;
                textureBytes.put(row * size + col, p);
            }
        }

        TextureData textureData = new TextureData(GL.GL_LUMINANCE, size, size, 0, GL.GL_LUMINANCE,
            GL.GL_UNSIGNED_BYTE, false, false, false, textureBytes.rewind(), null);
        this.outlineTexture = TextureIO.newTexture(textureData);

        this.outlineTexture.bind();
        this.outlineTexture.setTexParameteri(GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
        this.outlineTexture.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
        this.outlineTexture.setTexParameteri(GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
        this.outlineTexture.setTexParameteri(GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
    }
}
