/*
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;

import gov.nasa.worldwind.layers.*;
import gov.nasa.worldwind.geom.*;

import javax.media.opengl.*;

import com.sun.opengl.util.texture.*;

import java.nio.*;
import java.util.*;

/**
 * @author tag
 * @version $Id: SurfaceTileRenderer.java 1664 2007-04-30 20:07:11Z tgaskins $
 */
public class SurfaceTileRenderer implements Disposable
{
    private static final int DEFAULT_ALPHA_TEXTURE_SIZE = 2;

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

    public void dispose() // TODO: but waiting to implement a global texture disposal system
    {
    }

    public boolean isShowImageTileOutlines()
    {
        return showImageTileOutlines;
    }

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

    public void renderTile(DrawContext dc, TextureTile tile)
    {
        if (tile == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.TileIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalStateException(message);
        }

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

    public void renderTiles(DrawContext dc, Iterable<TextureTile> tiles)
    {
        if (tiles == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.TileIterableIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalStateException(message);
        }

        if (dc == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, 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
        {
            if (this.alphaTexture == null)
                this.initAlphaTexture(DEFAULT_ALPHA_TEXTURE_SIZE); // TODO: choose size to match incoming tile sizes?

            boolean showOutlines = this.showImageTileOutlines && dc.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_REPLACE);
            }
            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.
//            System.out.printf("%d geo tiles\n", dc.getSurfaceGeometry().size());
            for (SectorGeometry sg : dc.getSurfaceGeometry())
            {
                Iterable<TextureTile> tilesToRender = this.getIntersectingTiles(sg, tiles);
                if (tilesToRender == null)
                    continue;
//                System.out.printf("%d, ", tilesToRender.length);

                // Pre-load info to compute the texture transform below
                Sector st = sg.getSector();
                double geoDeltaLat = st.getDeltaLatRadians();
                double geoDeltaLon = st.getDeltaLonRadians();
                double geoMinLat = st.getMinLatitude().radians;
                double geoMinLon = st.getMinLongitude().radians;

                // 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, most likely via a
                // fragment shader.
                for (TextureTile tile : tilesToRender)
                {
                    gl.glActiveTexture(GL.GL_TEXTURE0);

                    if (tile.bindTexture(dc))
                    {
                        gl.glMatrixMode(GL.GL_TEXTURE);
                        gl.glLoadIdentity();
                        tile.applyTextureTransform(dc);

                        // Determine and apply texture transform to map image tile into geometry tile's texture space
                        Sector si = tile.getSector();
                        double latScale = si.getDeltaLatRadians() > 0 ? geoDeltaLat / si.getDeltaLatRadians() : 1;
                        double lonScale = si.getDeltaLonRadians() > 0 ? geoDeltaLon / si.getDeltaLonRadians() : 1;
                        gl.glScaled(lonScale, latScale, 1d);

                        double latShift = -(si.getMinLatitude().radians - geoMinLat) / geoDeltaLat;
                        double lonShift = -(si.getMinLongitude().radians - geoMinLon) / geoDeltaLon;
                        gl.glTranslated(lonShift, latShift, 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(lonScale, latScale, 1d);
                            gl.glTranslated(lonShift, latShift, 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(lonScale, latScale, 1d);
                        gl.glTranslated(lonShift, latShift, 0d);

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

            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)
        {
            String message = WorldWind.retrieveErrMsg("generic.ExceptionWhileRenderingLayer");
            message += this.getClass().getName();
            WorldWind.logger().log(java.util.logging.Level.FINE, message, e);
        }
        finally
        {
            gl.glPopAttrib();
        }
    }

    private Iterable<TextureTile> getIntersectingTiles(SectorGeometry sg, Iterable<TextureTile> tiles)
    {
        ArrayList<TextureTile> intersectingTiles = null;

        for (TextureTile tile : tiles)
        {
            if (!tile.getSector().intersects(sg.getSector()))
                continue;

            if (intersectingTiles == null)
                intersectingTiles = new ArrayList<TextureTile>();

            intersectingTiles.add(tile);
        }

        if (intersectingTiles == null)
            return null;

        return intersectingTiles;
    }

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

    private void initAlphaTexture(int size)
    {
        ByteBuffer textureBytes = com.sun.opengl.util.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).
    }

    private void initOutlineTexture(int size)
    {
        ByteBuffer textureBytes = com.sun.opengl.util.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);
    }
}
