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

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

import javax.media.opengl.*;
import java.util.concurrent.*;

/**
 * @author tag
 * @version $Id: TextureTile.java 1767 2007-05-07 21:36:12Z tgaskins $
 */
public class TextureTile extends Tile implements Disposable
{
    private volatile TextureData textureData;
    private Texture texture;
    private TextureTile fallbackTile = null; // holds texture to use if own texture not available
    private Point centroid; // Cartesian coordinate of lat/lon center
    private Point[] corners; // Cartesian coordinate of lat/lon corners
    private Extent extent = null; // bounding volume
    private double extentVerticalExaggertion = Double.MIN_VALUE; // VE used to calculate the extent
    private double minDistanceToEye = Double.MAX_VALUE;

    private static ConcurrentLinkedQueue<TextureTile> texturesToDispose = new ConcurrentLinkedQueue<TextureTile>();

    static
    {
        WorldWind.memoryCache().addCacheListener(new MemoryCache.CacheListener()
        {
            public synchronized void entryRemoved(Object key, Object clientObject)
            {
                // Unbind a tile's texture when the tile leaves the cache.
                if (clientObject != null && clientObject instanceof TextureTile)
                {
                    TextureTile tile = (TextureTile) clientObject;
                    if (tile.texture != null)
                    {
                        // Textures must be disposed of on a thread with a current OpenGL context,
                        // so just capture those to dispose here and perform the actual dispose during
                        // a rendering pass.
                        texturesToDispose.add(tile);
                    }
                }
            }
        });
    }

    public synchronized static void disposeTextures()
    {
        TextureTile tile;
        for (tile = texturesToDispose.poll(); tile != null && tile.texture != null; tile = texturesToDispose.poll())
        {
            tile.texture.dispose();
            tile.texture = null;
        }
    }

    public TextureTile(Sector sector)
    {
        super(sector);
    }

    public TextureTile(Sector sector, Level level, int row, int col)
    {
        super(sector, level, row, col);
    }

    @Override
    public final long getSizeInBytes()
    {
        long size = super.getSizeInBytes();

        if (this.textureData != null)
            size += this.textureData.getEstimatedMemorySize();

        return size;
    }

    public void dispose()
    {
        if (this.texture == null)
            return;

        if (GLContext.getCurrent() != null)
        {
            this.texture.dispose();
            this.texture = null;
        }
        else if (!texturesToDispose.contains(this))
        {
            texturesToDispose.add(this);
        }
    }

    public TextureTile getFallbackTile()
    {
        return this.fallbackTile;
    }

    public void setFallbackTile(TextureTile fallbackTile)
    {
        this.fallbackTile = fallbackTile;
    }

    public TextureData getTextureData()
    {
        return this.textureData;
    }

    public void setTextureData(TextureData textureData)
    {
        this.textureData = textureData;
    }

    public Texture getTexture()
    {
        return this.texture;
    }

    public boolean holdsTexture()
    {
        return this.getTexture() != null || this.getTextureData() != null;
    }

    public void setTexture(Texture texture)
    {
        if (texture == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.TextureIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        this.texture = texture;
        this.textureData = null; // no more need for texture data; allow garbage collector to reclaim it
    }

    public Point getCentroidPoint(Globe globe)
    {
        if (globe == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.GlobeIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        if (this.centroid == null)
        {
            gov.nasa.worldwind.geom.LatLon c = this.getSector().getCentroid();
            this.centroid = globe.computePointFromPosition(c.getLatitude(), c.getLongitude(), 0);
        }

        return this.centroid;
    }

    public Point[] getCornerPoints(Globe globe)
    {
        if (globe == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.GlobeIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        if (this.corners == null)
        {
            Sector s = this.getSector();
            this.corners = new gov.nasa.worldwind.geom.Point[4];
            this.corners[0] = globe.computePointFromPosition(s.getMinLatitude(), s.getMinLongitude(), 0); // sw
            this.corners[1] = globe.computePointFromPosition(s.getMinLatitude(), s.getMaxLongitude(), 0); // se
            this.corners[2] = globe.computePointFromPosition(s.getMaxLatitude(), s.getMaxLongitude(), 0); // nw
            this.corners[3] = globe.computePointFromPosition(s.getMaxLatitude(), s.getMinLongitude(), 0); // ne
        }

        return this.corners;
    }

    public double getMinDistanceToEye()
    {
        return this.minDistanceToEye;
    }

    public void setMinDistanceToEye(double minDistanceToEye)
    {
        if (minDistanceToEye < 0)
        {
            String msg = WorldWind.retrieveErrMsg("layers.TextureTile.MinDistanceToEyeNegative");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }
        this.minDistanceToEye = minDistanceToEye;
    }

    public Extent getExtent(DrawContext dc)
    {
        if (dc == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        if (this.extent == null || this.extentVerticalExaggertion != dc.getVerticalExaggeration())
        {
            this.extent = Sector.computeBoundingCylinder(dc.getGlobe(), dc.getVerticalExaggeration(), this.getSector());
            this.extentVerticalExaggertion = dc.getVerticalExaggeration();
        }

        return this.extent;
    }

    public TextureTile[] createSubTiles(Level nextLevel)
    {
        if (nextLevel == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.LevelIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }
        Angle p0 = this.getSector().getMinLatitude();
        Angle p2 = this.getSector().getMaxLatitude();
        Angle p1 = Angle.midAngle(p0, p2);

        Angle t0 = this.getSector().getMinLongitude();
        Angle t2 = this.getSector().getMaxLongitude();
        Angle t1 = Angle.midAngle(t0, t2);

        String nextLevelCacheName = nextLevel.getCacheName();
        int nextLevelNum = nextLevel.getLevelNumber();
        int row = this.getRow();
        int col = this.getColumn();

        TextureTile[] subTiles = new TextureTile[4];

        TileKey key = new TileKey(nextLevelNum, 2 * row, 2 * col, nextLevelCacheName);
        TextureTile subTile = this.getTileFromMemoryCache(key);
        if (subTile != null)
            subTiles[0] = subTile;
        else
            subTiles[0] = new TextureTile(new Sector(p0, p1, t0, t1), nextLevel, 2 * row, 2 * col);

        key = new TileKey(nextLevelNum, 2 * row, 2 * col + 1, nextLevelCacheName);
        subTile = this.getTileFromMemoryCache(key);
        if (subTile != null)
            subTiles[1] = subTile;
        else
            subTiles[1] = new TextureTile(new Sector(p0, p1, t1, t2), nextLevel, 2 * row, 2 * col + 1);

        key = new TileKey(nextLevelNum, 2 * row + 1, 2 * col, nextLevelCacheName);
        subTile = this.getTileFromMemoryCache(key);
        if (subTile != null)
            subTiles[2] = subTile;
        else
            subTiles[2] = new TextureTile(new Sector(p1, p2, t0, t1), nextLevel, 2 * row + 1, 2 * col);

        key = new TileKey(nextLevelNum, 2 * row + 1, 2 * col + 1, nextLevelCacheName);
        subTile = this.getTileFromMemoryCache(key);
        if (subTile != null)
            subTiles[3] = subTile;
        else
            subTiles[3] = new TextureTile(new Sector(p1, p2, t1, t2), nextLevel, 2 * row + 1, 2 * col + 1);

        return subTiles;
    }

    public void initializeTexture(DrawContext dc)
    {
        if (this.getTexture() == null)
        {
            int filter = GL.GL_LINEAR;
//            int filter = GL.GL_LINEAR_MIPMAP_LINEAR;

            this.setTexture(TextureIO.newTexture(this.getTextureData()));
            this.getTexture().bind();
            GL gl = dc.getGL();
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, filter);
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, filter);
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
        }
    }

    public boolean bindTexture(DrawContext dc)
    {
        if (this.holdsTexture()) // use the tile's texture
        {
            if (this.getTexture() == null)
                this.initializeTexture(dc);

            if (this.getTexture() == null)
                return false; // bad texture or something, skip it

            this.getTexture().bind();
        }
        else if (this.getFallbackTile() != null) // use texture of resource tile
        {
            TextureTile resourceTile = this.getFallbackTile();
            if (resourceTile.getTexture() == null)
                resourceTile.initializeTexture(dc);

            if (resourceTile.getTexture() == null)
                return false; // bad texture or something, skip it

            resourceTile.getTexture().bind();
        }

        return true;
    }

    public void applyTextureTransform(DrawContext dc)
    {
        GL gl = GLContext.getCurrent().getGL();

        gl.glMatrixMode(GL.GL_TEXTURE);
        gl.glLoadIdentity();

        if (this.holdsTexture()) // use the tile's texture
        {
            if (this.getTexture() == null)
                this.initializeTexture(dc);

            if (this.getTexture() == null)
                return; // bad texture or something, skip it

            if (this.getTexture().getMustFlipVertically())
            {
                gl.glScaled(1, -1, 1);
                gl.glTranslated(0, -1, 0);
            }

//            this.getTexture().bind();
        }
        else if (this.getFallbackTile() != null) // use texture of resource tile
        {
            TextureTile resourceTile = this.getFallbackTile();
            if (resourceTile.getTexture() == null)
                resourceTile.initializeTexture(dc);

            if (resourceTile.getTexture() == null)
                return; // bad texture or something, skip it

            if (resourceTile.getTexture().getMustFlipVertically())
            {
                gl.glScaled(1, -1, 1);
                gl.glTranslated(0, -1, 0);
            }

            this.applyResourceTextureTransform(dc);
//            resourceTile.getTexture().bind();
        }
    }

    private void applyResourceTextureTransform(DrawContext dc)
    {
        if (this.getLevel() == null)
            return;

        int levelDelta = this.getLevelNumber() - this.getFallbackTile().getLevelNumber();
        if (levelDelta <= 0)
            return;

        double twoToTheN = Math.pow(2, levelDelta);
        double oneOverTwoToTheN = 1 / twoToTheN;

        double sShift = oneOverTwoToTheN * (this.getColumn() % twoToTheN);
        double tShift = oneOverTwoToTheN * (this.getRow() % twoToTheN);

        dc.getGL().glTranslated(sShift, tShift, 0);
        dc.getGL().glScaled(oneOverTwoToTheN, oneOverTwoToTheN, 1);
    }

    private TextureTile getTileFromMemoryCache(TileKey tileKey)
    {
        return (TextureTile) WorldWind.memoryCache().getObject(tileKey);
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        final TextureTile tile = (TextureTile) o;

        return !(this.getTileKey() != null ? !this.getTileKey().equals(tile.getTileKey()) : tile.getTileKey() != null);
    }

    @Override
    public int hashCode()
    {
        return (this.getTileKey() != null ? this.getTileKey().hashCode() : 0);
    }

    @Override
    public String toString()
    {
        return this.getSector().toString();
    }
}
