/*
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.j2d.*;
import com.sun.opengl.util.texture.*;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.geom.Point;

import javax.imageio.*;
import javax.media.opengl.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * @author tag
 * @version $Id: TiledImageLayer.java 1774 2007-05-08 01:03:37Z dcollins $
 */
public class TiledImageLayer extends AbstractLayer
{
    // Infrastructure
    private static final LevelComparer levelComparer = new LevelComparer();
    private final LevelSet levels;
    private ArrayList<TextureTile> topLevels;
    private final Object fileLock = new Object();
    private boolean forceLevelZeroLoads = false;
    private boolean retainLevelZeroTiles = false;

    // Diagnostic flags
    private boolean showImageTileOutlines = false;
    private boolean drawTileBoundaries = false;
    private boolean drawWireframe = false;
    private boolean useTransparentTextures = false;
    private boolean drawTileIDs = false;
    private boolean drawBoundingVolumes = false;
    private TextRenderer textRenderer = null;

    // Stuff computed each frame
    private ArrayList<TextureTile> currentTiles = new ArrayList<TextureTile>();
    private TextureTile currentResourceTile;
    private Point referencePoint;
    private PriorityBlockingQueue<RequestTask> requestQ = new PriorityBlockingQueue<RequestTask>(200);

    public TiledImageLayer(LevelSet levelSet)
    {
        if (levelSet == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.LevelSetIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        this.levels = new LevelSet(levelSet); // the caller's levelSet may change internally, so we copy it.

        this.createTopLevelTiles();
        if (this.forceLevelZeroLoads)
            this.loadAllTopLevelTextures();

        this.setPickEnabled(false); // textures are assumed to be terrain unless specifically indicated otherwise.
    }

    @Override
    public void dispose()
    {
        if (!this.retainLevelZeroTiles)
            return;

        for (TextureTile tile : this.topLevels)
        {
            tile.dispose();
        }
    }

    public boolean isUseTransparentTextures()
    {
        return this.useTransparentTextures;
    }

    public void setUseTransparentTextures(boolean useTransparentTextures)
    {
        this.useTransparentTextures = useTransparentTextures;
    }

    public boolean isForceLevelZeroLoads()
    {
        return this.forceLevelZeroLoads;
    }

    public void setForceLevelZeroLoads(boolean forceLevelZeroLoads)
    {
        this.forceLevelZeroLoads = forceLevelZeroLoads;
        if (this.forceLevelZeroLoads)
            this.loadAllTopLevelTextures();
    }

    public boolean isRetainLevelZeroTiles()
    {
        return retainLevelZeroTiles;
    }

    public void setRetainLevelZeroTiles(boolean retainLevelZeroTiles)
    {
        this.retainLevelZeroTiles = retainLevelZeroTiles;
    }

    public boolean isDrawTileIDs()
    {
        return drawTileIDs;
    }

    public void setDrawTileIDs(boolean drawTileIDs)
    {
        this.drawTileIDs = drawTileIDs;
    }

    public boolean isDrawTileBoundaries()
    {
        return drawTileBoundaries;
    }

    public void setDrawTileBoundaries(boolean drawTileBoundaries)
    {
        this.drawTileBoundaries = drawTileBoundaries;
    }

    public boolean isDrawWireframe()
    {
        return drawWireframe;
    }

    public void setDrawWireframe(boolean drawWireframe)
    {
        this.drawWireframe = drawWireframe;
    }

    public boolean isShowImageTileOutlines()
    {
        return showImageTileOutlines;
    }

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

    public boolean isDrawBoundingVolumes()
    {
        return drawBoundingVolumes;
    }

    public void setDrawBoundingVolumes(boolean drawBoundingVolumes)
    {
        this.drawBoundingVolumes = drawBoundingVolumes;
    }

    private void createTopLevelTiles()
    {
        Sector sector = this.levels.getSector();

        Angle dLat = this.levels.getLevelZeroTileDelta().getLatitude();
        Angle dLon = this.levels.getLevelZeroTileDelta().getLongitude();

        // Determine the row and column offset from the common World Wind global tiling origin.
        Level level = levels.getFirstLevel();
        int firstRow = Tile.computeRow(level.getTileDelta().getLatitude(), sector.getMinLatitude());
        int firstCol = Tile.computeColumn(level.getTileDelta().getLongitude(), sector.getMinLongitude());
        int lastRow = Tile.computeRow(level.getTileDelta().getLatitude(), sector.getMaxLatitude());
        int lastCol = Tile.computeColumn(level.getTileDelta().getLongitude(), sector.getMaxLongitude());

        int nLatTiles = lastRow - firstRow + 1;
        int nLonTiles = lastCol - firstCol + 1;

        this.topLevels = new ArrayList<TextureTile>(nLatTiles * nLonTiles);

        Angle p1 = Tile.computeRowLatitude(firstRow, dLat);
        for (int row = firstRow; row <= lastRow; row++)
        {
            Angle p2;
            p2 = p1.add(dLat);

            Angle t1 = Tile.computeColumnLongitude(firstCol, dLon);
            for (int col = firstCol; col <= lastCol; col++)
            {
                Angle t2;
                t2 = t1.add(dLon);

                this.topLevels.add(new TextureTile(new Sector(p1, p2, t1, t2), level, row, col));
                t1 = t2;
            }
            p1 = p2;
        }
    }

    private void loadAllTopLevelTextures()
    {
        for (TextureTile tile : this.topLevels)
        {
            if (!tile.holdsTexture())
                this.forceTextureLoad(tile);
        }
    }

    // ============== Tile Assembly ======================= //
    // ============== Tile Assembly ======================= //
    // ============== Tile Assembly ======================= //

    private void assembleTiles(DrawContext dc)
    {
        this.currentTiles.clear();
//        this.currentSpan = null;

        for (TextureTile tile : this.topLevels)
        {
            if (this.isTileVisible(dc, tile))
            {
                this.currentResourceTile = null;
                this.addTileOrDescendants(dc, tile);
            }
        }
    }

    private void addTileOrDescendants(DrawContext dc, TextureTile tile)
    {
        if (this.meetsRenderCriteria(dc, tile))
        {
            this.addTile(dc, tile);
            return;
        }

        // The incoming tile does not meet the rendering criteria, so it must be subdivided and those
        // subdivisions tested against the criteria.

        // All tiles that meet the selection criteria are drawn, but some of those tiles will not have
        // textures associated with them either because their texture isn't loaded yet or because they
        // are finer grain than the layer has textures for. In these cases the tiles use the texture of
        // the closest ancestor that has a texture loaded. This ancestor is called the currentResourceTile.
        // A texture transform is applied during rendering to align the sector's texture coordinates with the
        // appropriate region of the ancestor's texture.

        TextureTile ancestorResource = null;

        try
        {
            if (tile.holdsTexture() || tile.getLevelNumber() == 0)
            {
                ancestorResource = this.currentResourceTile;
                this.currentResourceTile = tile;
            }

            // Ensure that levels finer than the finest image have the finest image around
            // TODO: find finest level with a non-missing tile
            if (this.levels.isFinalLevel(tile.getLevelNumber()) && !this.isTextureInMemory(tile))
                this.requestTexture(dc, tile);

            TextureTile[] subTiles = tile.createSubTiles(this.levels.getLevel(tile.getLevelNumber() + 1));
            for (TextureTile child : subTiles)
            {
                if (this.isTileVisible(dc, child))
                    this.addTileOrDescendants(dc, child);
            }
        }
        finally
        {
            if (ancestorResource != null) // Pop this tile as the currentResource ancestor
                this.currentResourceTile = ancestorResource;
        }
    }

    private void addTile(DrawContext dc, TextureTile tile)
    {
        tile.setFallbackTile(null);

        if (tile.holdsTexture())
        {
            this.addTileToCurrent(tile);
            return;
        }

        // Level 0 loads may be forced
        if (tile.getLevelNumber() == 0 && this.forceLevelZeroLoads)
        {
            this.forceTextureLoad(tile);
            if (tile.holdsTexture())
            {
                this.addTileToCurrent(tile);
                return;
            }
        }

        // Tile's texture isn't available, so request it
        if (tile.getLevelNumber() < this.levels.getNumLevels())
        {
            // Request only tiles with data associated at this level
            if (!this.levels.isResourceAbsent(tile))
                this.requestTexture(dc, tile);
        }

        // Set up to use the currentResource tile's texture
        if (this.currentResourceTile != null)
        {
            if (this.currentResourceTile.getLevelNumber() == 0 && this.forceLevelZeroLoads
                && !this.currentResourceTile.holdsTexture())
                this.forceTextureLoad(this.currentResourceTile);

            if (this.currentResourceTile.holdsTexture())
            {
                tile.setFallbackTile(currentResourceTile);
                this.addTileToCurrent(tile);
            }
        }
    }

    private void addTileToCurrent(TextureTile tile)
    {
        this.currentTiles.add(tile);
    }

    private boolean isTileVisible(DrawContext dc, TextureTile tile)
    {
        return tile.getExtent(dc).intersects(dc.getView().getFrustumInModelCoordinates())
            && (dc.getVisibleSector() == null || dc.getVisibleSector().intersects(tile.getSector()));
    }

    private boolean meetsRenderCriteria(DrawContext dc, TextureTile tile)
    {
        return this.levels.isFinalLevel(tile.getLevelNumber()) || !needToSplit(dc, tile.getSector(), 20);
    }

    private static boolean needToSplit(DrawContext dc, Sector sector, int density)
    {
        Point[] corners = sector.computeCornerPoints(dc.getGlobe());
        Point centerPoint = sector.computeCenterPoint(dc.getGlobe());

        View view = dc.getView();
        double d1 = view.getEyePoint().distanceTo(corners[0]);
        double d2 = view.getEyePoint().distanceTo(corners[1]);
        double d3 = view.getEyePoint().distanceTo(corners[2]);
        double d4 = view.getEyePoint().distanceTo(corners[3]);
        double d5 = view.getEyePoint().distanceTo(centerPoint);

        double minDistance = d1;
        if (d2 < minDistance)
            minDistance = d2;
        if (d3 < minDistance)
            minDistance = d3;
        if (d4 < minDistance)
            minDistance = d4;
        if (d5 < minDistance)
            minDistance = d5;

        double cellSize = (Math.PI * sector.getDeltaLatRadians() * dc.getGlobe().getRadius()) / density;

        return !(Math.log10(cellSize) <= (Math.log10(minDistance) - 1));
    }

    // ============== Rendering ======================= //
    // ============== Rendering ======================= //
    // ============== Rendering ======================= //

    @Override
    protected final void doRender(DrawContext dc)
    {
        if (dc.getSurfaceGeometry() == null || dc.getSurfaceGeometry().size() < 1)
            return; // TODO: throw an illegal state exception?

        dc.getSurfaceTileRenderer().setShowImageTileOutlines(this.showImageTileOutlines);

        draw(dc);
    }

    private void draw(DrawContext dc)
    {
        TextureTile.disposeTextures(); // Clean up any unused textures while within a OpenGl context thread.

        if (!this.isEnabled())
            return; // Don't check for arg errors if we're disabled

        if (!this.isLayerActive(dc))
            return;

        if (!this.isLayerInView(dc))
            return;

        this.referencePoint = this.computeReferencePoint(dc);

        this.assembleTiles(dc); // Determine the tiles to draw.

        if (this.currentTiles.size() >= 1)
        {
            TextureTile[] sortedTiles = new TextureTile[this.currentTiles.size()];
            sortedTiles = this.currentTiles.toArray(sortedTiles);
            Arrays.sort(sortedTiles, levelComparer);

            GL gl = dc.getGL();

            gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT | GL.GL_POLYGON_BIT);

            if (this.isUseTransparentTextures())
            {
                gl.glEnable(GL.GL_BLEND);
                gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
            }

            gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL);
            gl.glEnable(GL.GL_CULL_FACE);
            gl.glCullFace(GL.GL_BACK);

//            System.out.println(this.getName() + " " + this.currentTiles.size()); // **************
            dc.getSurfaceTileRenderer().renderTiles(dc, this.currentTiles);

            gl.glPopAttrib();

            if (this.drawTileIDs)
                this.drawTileIDs(dc, this.currentTiles);

            if (this.drawBoundingVolumes)
                this.drawBoundingVolumes(dc, this.currentTiles);

            this.currentTiles.clear();
        }

        this.sendRequests();
        this.requestQ.clear();
    }

    private void sendRequests()
    {
        RequestTask task = this.requestQ.poll();
        while (task != null)
        {
            if (!WorldWind.threadedTaskService().isFull())
            {
                WorldWind.threadedTaskService().addTask(task);
            }
            task = this.requestQ.poll();
        }
    }

    public boolean isLayerInView(DrawContext dc)
    {
        if (dc == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalStateException(message);
        }

        if (dc.getView() == null)
        {
            String message = WorldWind.retrieveErrMsg("layers.AbstractLayer.NoViewSpecifiedInDrawingContext");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalStateException(message);
        }

        if (dc.getVisibleSector() != null && !this.levels.getSector().intersects(dc.getVisibleSector()))
            return false;

        Extent e = Sector.computeBoundingCylinder(dc.getGlobe(), dc.getVerticalExaggeration(), this.levels.getSector());
        return e.intersects(dc.getView().getFrustumInModelCoordinates());
    }

    private Point computeReferencePoint(DrawContext dc)
    {
        java.awt.geom.Rectangle2D viewport = dc.getView().getViewport();
        int x = (int) viewport.getWidth() / 2;
        for (int y = (int) (0.75 * viewport.getHeight()); y >= 0; y--)
        {
            Position pos = dc.getView().computePositionFromScreenPoint(x, y);
            if (pos == null)
                continue;

            return dc.getGlobe().computePointFromPosition(pos.getLatitude(), pos.getLongitude(), 0d);
        }

        return null;
    }

    private static class LevelComparer implements Comparator<TextureTile>
    {
        public int compare(TextureTile ta, TextureTile tb)
        {
            int la;
            int lb;

            if (ta.holdsTexture())
                la = ta.getLevelNumber();
            else
                la = ta.getFallbackTile().getLevelNumber();

            if (tb.holdsTexture())
                lb = tb.getLevelNumber();
            else
                lb = tb.getFallbackTile().getLevelNumber();

            return la < lb ? -1 : la == lb ? 0 : 1;
        }
    }

    private void drawTileIDs(DrawContext dc, ArrayList<TextureTile> tiles)
    {
        java.awt.Rectangle viewport = dc.getView().getViewport();
        if (this.textRenderer == null)
            this.textRenderer = new TextRenderer(java.awt.Font.decode("Arial-Plain-13"), true, true);

        dc.getGL().glDisable(GL.GL_DEPTH_TEST);
        dc.getGL().glDisable(GL.GL_BLEND);
        dc.getGL().glDisable(GL.GL_TEXTURE_2D);

        this.textRenderer.setColor(java.awt.Color.YELLOW);
        this.textRenderer.beginRendering(viewport.width, viewport.height);
        for (TextureTile tile : tiles)
        {
            String tileLabel = tile.getLabel();

            if (tile.getFallbackTile() != null)
                tileLabel += "/" + tile.getFallbackTile().getLabel();

            LatLon ll = tile.getSector().getCentroid();
            Point pt = dc.getGlobe().computePointFromPosition(ll.getLatitude(), ll.getLongitude(),
                dc.getGlobe().getElevation(ll.getLatitude(), ll.getLongitude()));
            pt = dc.getView().project(pt);
            this.textRenderer.draw(tileLabel, (int) pt.x(), (int) pt.y());
        }
        this.textRenderer.endRendering();
    }

    private void drawBoundingVolumes(DrawContext dc, ArrayList<TextureTile> tiles)
    {
        float[] previousColor = new float[4];
        dc.getGL().glGetFloatv(GL.GL_CURRENT_COLOR, previousColor, 0);
        dc.getGL().glColor3d(0, 1, 0);

        for (TextureTile tile : tiles)
        {
            ((Cylinder) tile.getExtent(dc)).render(dc);
        }

        dc.getGL().glColor4fv(previousColor, 0);
    }

    // ============== Image Reading and Downloading ======================= //
    // ============== Image Reading and Downloading ======================= //
    // ============== Image Reading and Downloading ======================= //

    private void requestTexture(DrawContext dc, TextureTile tile)
    {
        Point centroid = tile.getCentroidPoint(dc.getGlobe());
        if (this.referencePoint != null)
            tile.setPriority(centroid.distanceTo(this.referencePoint));

        RequestTask task = new RequestTask(tile, this);
        this.requestQ.add(task);
    }

    private void forceTextureLoad(TextureTile tile)
    {
        final java.net.URL textureURL = WorldWind.dataFileCache().findFile(tile.getPath(), true);

        if (textureURL != null && WWIO.isFileOutOfDate(textureURL, tile.getLevel().getExpiryTime()))
        {
            // The file has expired. Delete it.
            gov.nasa.worldwind.WorldWind.dataFileCache().removeFile(textureURL);
            String message = WorldWind.retrieveErrMsg("generic.DataFileExpired") + textureURL;
            WorldWind.logger().log(java.util.logging.Level.FINER, message);
        }
        else if (textureURL != null)
        {
            this.loadTexture(tile, textureURL);
        }
    }

    private boolean loadTexture(TextureTile tile, java.net.URL textureURL)
    {
        TextureData textureData;

        synchronized (this.fileLock)
        {
            textureData = readTexture(textureURL);
        }

        if (textureData == null)
            return false;

        tile.setTextureData(textureData);
        if (tile.getLevelNumber() != 0 || !this.retainLevelZeroTiles)
            this.addTileToCache(tile);

        return true;
    }

    private static TextureData readTexture(java.net.URL url)
    {
        try
        {
            return TextureIO.newTextureData(url, false, null);
        }
        catch (Exception e)
        {
            String message = WorldWind.retrieveErrMsg("layers.TextureLayer.ExceptionAttemptingToReadTextureFile");
            WorldWind.logger().log(java.util.logging.Level.FINE, message + url, e);
            return null;
        }
    }

    private void addTileToCache(TextureTile tile)
    {
        WorldWind.memoryCache().add(tile.getTileKey(), tile);
    }

    private boolean isTextureInMemory(TextureTile tile)
    {
        return ((tile.getLevelNumber() == 0 && tile.holdsTexture())
            || WorldWind.memoryCache().getObject(tile.getTileKey()) != null);
    }

    private void downloadTexture(final TextureTile tile)
    {
        if (WorldWind.retrievalService().isFull())
            return;

        java.net.URL url;
        try
        {
            url = tile.getResourceURL();
        }
        catch (java.net.MalformedURLException e)
        {
            String message = WorldWind.retrieveErrMsg("layers.TextureLayer.ExceptionCreatingTextureUrl");
            WorldWind.logger().log(java.util.logging.Level.FINE, message + tile, e);
            return;
        }

        URLRetriever retriever = new HTTPRetriever(url, new DownloadPostProcessor(tile, this));
        WorldWind.retrievalService().runRetriever(retriever, tile.getPriority());
    }

    private void saveBuffer(java.nio.ByteBuffer buffer, java.io.File outFile) throws java.io.IOException
    {
        synchronized (this.fileLock) // sychronized with read of file in RequestTask.run()
        {
            gov.nasa.worldwind.WWIO.saveBuffer(buffer, outFile);
        }
    }

    // ============== Inner classes ======================= //
    // ============== Inner classes ======================= //
    // ============== Inner classes ======================= //

    private static class RequestTask implements Runnable, Comparable<RequestTask>
    {
        private final TiledImageLayer layer;
        private final TextureTile tile;

        private RequestTask(TextureTile tile, TiledImageLayer layer)
        {
            this.layer = layer;
            this.tile = tile;
        }

        public void run()
        {
            // check to ensure load is still needed
            if (this.layer.isTextureInMemory(this.tile))
                return;

            final java.net.URL textureURL = WorldWind.dataFileCache().findFile(tile.getPath(), false);
            if (textureURL != null)
            {
                if (WWIO.isFileOutOfDate(textureURL, this.tile.getLevel().getExpiryTime()))
                {
                    // The file has expired. Delete it then request download of newer.
                    gov.nasa.worldwind.WorldWind.dataFileCache().removeFile(textureURL);
                    String message = WorldWind.retrieveErrMsg("generic.DataFileExpired") + textureURL;
                    WorldWind.logger().log(java.util.logging.Level.FINER, message);
                }
                else if (this.layer.loadTexture(tile, textureURL))
                {
                    layer.levels.unmarkResourceAbsent(tile);
                    this.layer.firePropertyChange(gov.nasa.worldwind.AVKey.LAYER, null, this);
                    return;
                }
                else
                {
                    // Assume that something's wrong with the file and delete it.
                    gov.nasa.worldwind.WorldWind.dataFileCache().removeFile(textureURL);
                    layer.levels.markResourceAbsent(tile);
                    String message = WorldWind.retrieveErrMsg("generic.DeletedCorruptDataFile") + textureURL;
                    WorldWind.logger().log(java.util.logging.Level.FINE, message);
                }
            }

            this.layer.downloadTexture(this.tile);
        }

        /**
         * @param that the task to compare
         * @return -1 if <code>this</code> less than <code>that</code>, 1 if greater than, 0 if equal
         * @throws IllegalArgumentException if <code>that</code> is null
         */
        public int compareTo(RequestTask that)
        {
            if (that == null)
            {
                String msg = WorldWind.retrieveErrMsg("nullValue.RequestTaskIsNull");
                WorldWind.logger().log(java.util.logging.Level.FINE, msg);
                throw new IllegalArgumentException(msg);
            }
            return this.tile.getPriority() == that.tile.getPriority() ? 0 :
                this.tile.getPriority() < that.tile.getPriority() ? -1 : 1;
        }

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

            final RequestTask that = (RequestTask) o;

            // Don't include layer in comparison so that requests are shared among layers
            return !(tile != null ? !tile.equals(that.tile) : that.tile != null);
        }

        public int hashCode()
        {
            return (tile != null ? tile.hashCode() : 0);
        }

        public String toString()
        {
            return this.tile.toString();
        }
    }

    private static class DownloadPostProcessor implements RetrievalPostProcessor
    {
        // TODO: Rewrite this inner class, factoring out the generic parts.
        private final TextureTile tile;
        private final TiledImageLayer layer;

        public DownloadPostProcessor(TextureTile tile, TiledImageLayer layer)
        {
            this.tile = tile;
            this.layer = layer;
        }

        public ByteBuffer run(Retriever retriever)
        {
            if (retriever == null)
            {
                String msg = WorldWind.retrieveErrMsg("nullValue.RetrieverIsNull");
                WorldWind.logger().log(java.util.logging.Level.FINE, msg);
                throw new IllegalArgumentException(msg);
            }

            try
            {
                if (!retriever.getState().equals(Retriever.RETRIEVER_STATE_SUCCESSFUL))
                    return null;

                URLRetriever r = (URLRetriever) retriever;
                ByteBuffer buffer = r.getBuffer();

                if (retriever instanceof HTTPRetriever)
                {
                    HTTPRetriever htr = (HTTPRetriever) retriever;
                    if (htr.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT)
                    {
                        // Mark tile as missing to avoid excessive attempts
                        this.layer.levels.markResourceAbsent(this.tile);
                        return null;
                    }
                    else if (htr.getResponseCode() != HttpURLConnection.HTTP_OK)
                    {
                        // Also mark tile as missing, but for an unknown reason.
                        this.layer.levels.markResourceAbsent(this.tile);
                        return null;
                    }
                }

                final File outFile = WorldWind.dataFileCache().newFile(this.tile.getPath());
                if (outFile == null)
                {
                    String msg = WorldWind.retrieveErrMsg("generic.CantCreateCacheFile") + this.tile.getPath();
                    WorldWind.logger().log(java.util.logging.Level.FINE, msg);
                    return null;
                }

                if (outFile.exists())
                    return buffer;

                // TODO: Better, more generic and flexible handling of file-format type
                if (buffer != null)
                {
                    String contentType = r.getContentType().toLowerCase();
                    if (contentType.contains("xml") || contentType.contains("html") || contentType.contains("text"))
                    {
                        this.layer.levels.markResourceAbsent(this.tile);

                        StringBuffer sb = new StringBuffer();
                        while (buffer.hasRemaining())
                        {
                            sb.append((char) buffer.get());
                        }
                        // TODO: parse out the message if the content is xml or html.
                        WorldWind.logger().log(java.util.logging.Level.FINE, sb.toString());

                        return null;
                    }
                    else if (contentType.contains("dds"))
                    {
                        this.layer.saveBuffer(buffer, outFile);
                    }
                    else if (contentType.contains("zip"))
                    {
                        // Assume it's zipped DDS, which the retriever would have unzipped into the buffer.
                        this.layer.saveBuffer(buffer, outFile);
                    }
                    else if (outFile.getName().endsWith(".dds"))
                    {
                        // Convert to DDS
                        if (this.layer.isUseTransparentTextures())
                        {
                            buffer = DDSConverter.convertToDxt3(buffer, contentType);
                        }
                        else
                        {
                            buffer = DDSConverter.convertToDxt1NoTransparency(buffer,
                                contentType);
                        }

                        if (buffer != null)
                            this.layer.saveBuffer(buffer, outFile);
                    }
                    else if (contentType.contains("image"))
                    {
                        // Just save whatever it is to the cache.
                        this.layer.saveBuffer(buffer, outFile);
                    }

                    if (buffer != null)
                    {
                        this.layer.firePropertyChange(AVKey.LAYER, null, this);
                    }
                    return buffer;
                }
            }
            catch (java.io.IOException e)
            {
                this.layer.levels.markResourceAbsent(this.tile);
                String message = WorldWind.retrieveErrMsg("layers.TextureLayer.ExceptionSavingRetrievedTextureFile");
                WorldWind.logger().log(java.util.logging.Level.FINE, message + tile.getPath(), e);
            }
            return null;
        }
    }

    public Color getColor(Angle latitude, Angle longitude, int levelNumber)
    {
        // TODO: check args

        // Find the tile containing the position in the specified level.
        TextureTile containingTile = null;
        for (TextureTile tile : this.topLevels)
        {
            containingTile = this.getContainingTile(tile, latitude, longitude, levelNumber);
            if (containingTile != null)
                break;
        }
        if (containingTile == null)
            return null;

        String pathBase = containingTile.getPath().substring(0, containingTile.getPath().lastIndexOf("."));
        String cacheKey = pathBase + ".BufferedImage";

        // Look up the color if the image is in memory.
        BufferedImage image = (BufferedImage) WorldWind.memoryCache().getObject(cacheKey);
        if (image != null)
            return this.resolveColor(containingTile, image, latitude, longitude);

        // Read the image from disk since it's not in memory.
        image = this.requestImage(containingTile, cacheKey);
        if (image != null)
            return this.resolveColor(containingTile, image, latitude, longitude);

        // Retrieve it from the net since it's not on disk.
        this.downloadImage(containingTile);

        // Try to read from disk again after retrieving it from the net.
        image = this.requestImage(containingTile, cacheKey);
        if (image != null)
            return this.resolveColor(containingTile, image, latitude, longitude);

        // All attempts to find the image have failed.
        return null;
    }

    private final static String[] formats = new String[] {"jpg", "jpeg", "png", "tiff"};
    private final static String[] suffixes = new String[] {".jpg", ".jpg", ".png", ".tiff"};

    private BufferedImage requestImage(TextureTile tile, String cacheKey)
    {
        URL url = null;
        String pathBase = tile.getPath().substring(0, tile.getPath().lastIndexOf("."));
        for (String suffix : suffixes)
        {
            String path = pathBase + suffix;
            url = WorldWind.dataFileCache().findFile(path, false);
            if (url != null)
                break;
        }

        if (url == null)
            return null;

        if (WWIO.isFileOutOfDate(url, tile.getLevel().getExpiryTime()))
        {
            // The file has expired. Delete it then request download of newer.
            gov.nasa.worldwind.WorldWind.dataFileCache().removeFile(url);
            String message = WorldWind.retrieveErrMsg("generic.DataFileExpired") + url;
            WorldWind.logger().log(java.util.logging.Level.FINER, message);
        }
        else
        {
            try
            {
                BufferedImage image = ImageIO.read(new File(url.toURI()));
                if (image == null)
                {
                    return null; // TODO: warn
                }

                WorldWind.memoryCache().add(cacheKey, image, image.getRaster().getDataBuffer().getSize());
                this.levels.unmarkResourceAbsent(tile);
                return image;
            }
            catch (IOException e)
            {
                // Assume that something's wrong with the file and delete it.
                gov.nasa.worldwind.WorldWind.dataFileCache().removeFile(url);
                this.levels.markResourceAbsent(tile);
                String message = WorldWind.retrieveErrMsg("generic.DeletedCorruptDataFile") + url;
                WorldWind.logger().log(java.util.logging.Level.FINE, message);
            }
            catch (URISyntaxException e)
            {
                e.printStackTrace(); // TODO
            }
        }

        return null;
    }

    private void downloadImage(final TextureTile tile)
    {
        try
        {
            String urlString = tile.getResourceURL().toExternalForm().replace("dds", "");
            final URL resourceURL = new URL(urlString);

            URLRetriever retriever = new HTTPRetriever(resourceURL,
                new RetrievalPostProcessor()
                {
                    public ByteBuffer run(Retriever retriever)
                    {
                        if (!retriever.getState().equals(Retriever.RETRIEVER_STATE_SUCCESSFUL))
                            return null;

                        HTTPRetriever htr = (HTTPRetriever) retriever;
                        if (htr.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT)
                        {
                            // Mark tile as missing to avoid excessive attempts
                            TiledImageLayer.this.levels.markResourceAbsent(tile);
                            return null;
                        }

                        URLRetriever r = (URLRetriever) retriever;
                        ByteBuffer buffer = r.getBuffer();

                        String suffix = null;
                        for (int i = 0; i < formats.length; i++)
                        {
                            if (htr.getContentType().toLowerCase().contains(formats[i]))
                            {
                                suffix = suffixes[i];
                                break;
                            }
                        }
                        if (suffix == null)
                        {
                            return null; // TODO: log error
                        }

                        String path = tile.getPath().substring(0, tile.getPath().lastIndexOf("."));
                        path += suffix;

                        final File outFile = WorldWind.dataFileCache().newFile(path);
                        if (outFile == null)
                        {
                            String msg = WorldWind.retrieveErrMsg("generic.CantCreateCacheFile")
                                + tile.getPath();
                            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
                            return null;
                        }

                        try
                        {
                            WWIO.saveBuffer(buffer, outFile);
                            return buffer;
                        }
                        catch (IOException e)
                        {
                            e.printStackTrace(); // TODO: log error
                            return null;
                        }
                    }
                });

            retriever.call();
        }
        catch (Exception e)
        {
            e.printStackTrace(); // TODO
        }
    }

    private TextureTile getContainingTile(TextureTile tile, Angle latitude, Angle longitude, int levelNumber)
    {
        if (!tile.getSector().contains(latitude, longitude))
            return null;

        if (tile.getLevelNumber() == levelNumber || this.levels.isFinalLevel(tile.getLevelNumber()))
            return tile;

        TextureTile containingTile;
        TextureTile[] subTiles = tile.createSubTiles(this.levels.getLevel(tile.getLevelNumber() + 1));
        for (TextureTile child : subTiles)
        {
            containingTile = this.getContainingTile(child, latitude, longitude, levelNumber);
            if (containingTile != null)
                return containingTile;
        }

        return null;
    }

    private Color resolveColor(TextureTile tile, BufferedImage image, Angle latitude, Angle longitude)
    {
        Sector sector = tile.getSector();

        final double dLat = sector.getMaxLatitude().degrees - latitude.degrees;
        final double dLon = longitude.degrees - sector.getMinLongitude().degrees;
        final double sLat = dLat / sector.getDeltaLat().degrees;
        final double sLon = dLon / sector.getDeltaLon().degrees;

        final int tileHeight = tile.getLevel().getTileHeight();
        final int tileWidth = tile.getLevel().getTileWidth();
        int x = (int) ((tileWidth - 1) * sLon);
        int y = (int) ((tileHeight - 1) * sLat);
        int w = x < (tileWidth - 1) ? 1 : 0;
        int h = y < (tileHeight - 1) ? 1 : 0;

        double dh = sector.getDeltaLat().degrees / (tileHeight - 1);
        double dw = sector.getDeltaLon().degrees / (tileWidth - 1);
        double ssLat = (dLat - y * dh) / dh;
        double ssLon = (dLon - x * dw) / dw;

        int sw = image.getRGB(x, y);
        int se = image.getRGB(x + w, y);
        int ne = image.getRGB(x + w, y + h);
        int nw = image.getRGB(x, y + h);

        Color csw = new Color(sw);
        Color cse = new Color(se);
        Color cne = new Color(ne);
        Color cnw = new Color(nw);

        Color ctop = interpolateColors(cnw, cne, ssLon);
        Color cbot = interpolateColors(csw, cse, ssLon);

        return interpolateColors(cbot, ctop, ssLat);
    }

    private Color interpolateColors(Color ca, Color cb, double s)
    {
        int r = (int) (s * ca.getRed() + (1 - s) * cb.getRed());
        int g = (int) (s * ca.getGreen() + (1 - s) * cb.getGreen());
        int b = (int) (s * ca.getBlue() + (1 - s) * cb.getBlue());

        return new Color(r, g, b);
    }

    @Override
    public String toString()
    {
        return WorldWind.retrieveErrMsg("layers.TextureLayer.Name");
    }
}
//
//    private void renderTiles2(DrawContext dc)
//    {
//        // Render all the tiles collected during assembleTiles()
//        GL gl = dc.getGL();
//
//        gl.glPushAttrib(
//            GL.GL_COLOR_BUFFER_BIT // for blend func, current color, alpha func, color mask
//                | GL.GL_POLYGON_BIT // for face culling, polygon mode
//                | GL.GL_ENABLE_BIT
//                | GL.GL_CURRENT_BIT
//                | GL.GL_DEPTH_BUFFER_BIT // for depth mask
//                | GL.GL_TEXTURE_BIT // for texture env
//                | GL.GL_TRANSFORM_BIT);
//
//        try
//        {
//            gl.glEnable(GL.GL_DEPTH_TEST);
//            gl.glDepthFunc(GL.GL_LEQUAL);
//
//            if (this.useTransparentTextures)
//            {
//                gl.glEnable(GL.GL_BLEND);
//                gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
//            }
//
//            gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL);
//            gl.glEnable(GL.GL_CULL_FACE);
//            gl.glCullFace(GL.GL_BACK);
//            gl.glColor4d(0, 0, 0, 0);
//
//            gl.glEnable(GL.GL_ALPHA_TEST);
//            gl.glAlphaFunc(GL.GL_GREATER, 0.01f);
//
//            gl.glMatrixMode(javax.media.opengl.GL.GL_TEXTURE);
//            gl.glPushMatrix();
//            gl.glLoadIdentity();
//
////            System.out.printf("%d geo tiles, image tiles: ", dc.getSurfaceGeometry().size());
//            for (SectorGeometry sg : dc.getSurfaceGeometry())
//            {
//                TextureTile[] tilesToRender = this.getIntersectingTiles(sg);
//                if (tilesToRender == null)
//                    continue;
////                System.out.printf("%d, ", tilesToRender.length);
//
//                int numTilesRendered = 0;
//                while (numTilesRendered < tilesToRender.length)
//                {
//                    int numTexUnitsUsed = 0;
//                    while (numTexUnitsUsed < dc.getNumTextureUnits() && numTilesRendered < tilesToRender.length)
//                    {
//                        if (this.setupTileTexture(dc, tilesToRender[numTilesRendered++],
//                            GL.GL_TEXTURE0 + numTexUnitsUsed))
//                        {
//                            ++numTexUnitsUsed;
//                            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_TEXTURE);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_SRC1_RGB, GL.GL_PREVIOUS);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_SRC2_RGB, GL.GL_TEXTURE);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_OPERAND2_RGB, GL.GL_SRC_ALPHA);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_RGB, GL.GL_INTERPOLATE);
//
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_SRC0_ALPHA, GL.GL_TEXTURE);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_SRC1_ALPHA, GL.GL_PREVIOUS);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_SRC2_ALPHA, GL.GL_TEXTURE);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_OPERAND2_ALPHA, GL.GL_SRC_ALPHA);
//                            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_ALPHA, GL.GL_INTERPOLATE);
//                        }
//                    }
//
//                    sg.renderMultiTexture(dc, numTexUnitsUsed);
//
//                    // Turn all the multi-texture units off
//                    for (int i = 0; i < dc.getNumTextureUnits(); i++)
//                    {
//                        gl.glActiveTexture(GL.GL_TEXTURE0 + i);
//                        gl.glDisable(GL.GL_TEXTURE_2D);
//                    }
//                    gl.glActiveTexture(GL.GL_TEXTURE0);
//                }
//            }
////            System.out.println();
//
//            gl.glMatrixMode(javax.media.opengl.GL.GL_TEXTURE);
//            gl.glPopMatrix();
//        }
//        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 static String[] buildFragmentShader(int numTexUnits)
//    {
//        ArrayList<String> lines = new ArrayList<String>();
//
//        lines.add("uniform sampler2D tex[" + numTexUnits + "];");
//        lines.add("uniform int maxTexUnit;");
//        lines.add("void main()");
//        lines.add("{");
//        lines.add("    vec2 zero = vec2(0.0);");
//        lines.add("    vec2 one = vec2(1.0);");
//        lines.add("    vec4 color = vec4(0.0);");
//        lines.add("    bool hasColor = false;");
//
//        for (int i = 0; i < numTexUnits; i++)
//        {
//            lines.add("if (" + i + " <= maxTexUnit)");
//            lines.add("{");
//            lines.add("if (all(greaterThanEqual(gl_TexCoord[" + i + "].st, zero))"
//                + " && all(lessThanEqual(gl_TexCoord[" + i + "].st, one)))");
//            lines.add("{");
//            lines.add("    color = texture2D(tex[" + i + "], gl_TexCoord[" + i + "].st);");
//            lines.add("    hasColor = true;");
//            lines.add("}");
//        }
//
//        for (int i = 0; i < numTexUnits; i++)
//        {
//            lines.add("}");
//        }
//
//        lines.add("    if(hasColor)");
//        lines.add("        gl_FragColor = color;");
//        lines.add("    else");
//        lines.add("        discard;");
//        lines.add("}");
//
//        return lines.toArray(new String[lines.size()]);
//    }
//
//    private int fProgram = -1;
//
//    private void initFragmentProgram(int numTextureUnits)
//    {
//        String[] lines = buildFragmentShader(numTextureUnits);
//
//        GL gl = GLContext.getCurrent().getGL();
//
//        int fShader = gl.glCreateShader(GL.GL_FRAGMENT_SHADER);
//        int[] lineCounts = new int[lines.length];
//        for (int i = 0; i < lines.length; i++)
//        {
//            lineCounts[i] = lines[i].length();
//        }
//        gl.glShaderSource(fShader, lines.length, lines, lineCounts, 0);
//        gl.glCompileShader(fShader);
//
//        int[] status = new int[1];
//        byte[] log = new byte[4096];
//        int[] logLength = new int[1];
//        gl.glGetShaderiv(fShader, GL.GL_COMPILE_STATUS, status, 0);
//        gl.glGetShaderInfoLog(fShader, log.length, logLength, 0, log, 0);
//        if (status[0] != 1)
//        {
//            gl.glGetShaderInfoLog(fShader, log.length, logLength, 0, log, 0);
//            String sLog = new String(log);
//            System.out.println("Compile : " + sLog);
//        }
//
//        this.fProgram = gl.glCreateProgram();
//        gl.glAttachShader(this.fProgram, fShader);
//        gl.glLinkProgram(this.fProgram);
//
//        gl.glGetProgramiv(this.fProgram, GL.GL_LINK_STATUS, status, 0);
//        if (status[0] != 1)
//        {
//            gl.glGetShaderInfoLog(fShader, log.length, logLength, 0, log, 0);
//            String sLog = new String(log);
//            System.out.println("Link : " + sLog);
//        }
//    }
//
//    private String[] texUnitPositions;
//    private int[] texSamplers;
//
//    private void renderTiles3(DrawContext dc)
//    {
//        // Render all the tiles collected during assembleTiles()
//        GL gl = dc.getGL();
//
//        gl.glPushAttrib(
//            GL.GL_COLOR_BUFFER_BIT // for blend func, current color, alpha func, color mask
//                | GL.GL_POLYGON_BIT // for face culling, polygon mode
//                | GL.GL_ENABLE_BIT
//                | GL.GL_CURRENT_BIT
//                | GL.GL_DEPTH_BUFFER_BIT // for depth mask
//                | GL.GL_TEXTURE_BIT // for texture env
//                | GL.GL_TRANSFORM_BIT);
//
//        try
//        {
//            dc.setNumTextureUnits(1);
//            if (this.fProgram == -1)
//                this.initFragmentProgram(dc.getNumTextureUnits());
//
//            if (this.texUnitPositions == null)
//            {
//                this.texUnitPositions = new String[dc.getNumTextureUnits()];
//                for (int i = 0; i < dc.getNumTextureUnits(); i++)
//                {
//                    this.texUnitPositions[i] = "tex[" + i + "]";
//                }
//            }
//
//            if (this.texSamplers == null)
//                this.texSamplers = new int[dc.getNumTextureUnits()];
//
//            gl.glUseProgram(this.fProgram);
//
//            for (int i = 0; i < dc.getNumTextureUnits(); i++)
//            {
//                this.texSamplers[i] = gl.glGetUniformLocation(fProgram, "tex[" + i + "]");
//                gl.glUniform1i(this.texSamplers[i], i);
//            }
//
//            int maxTexUnit = gl.glGetUniformLocation(fProgram, "maxTexUnit");
//
//            gl.glEnable(GL.GL_DEPTH_TEST);
//            gl.glDepthFunc(GL.GL_LEQUAL);
//
//            if (this.useTransparentTextures)
//            {
//                gl.glEnable(GL.GL_BLEND);
//                gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
//            }
//
//            gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL);
//            gl.glEnable(GL.GL_CULL_FACE);
//            gl.glCullFace(GL.GL_BACK);
//            gl.glColor4d(0, 0, 0, 0);
//
//            gl.glMatrixMode(GL.GL_TEXTURE);
//            gl.glPushMatrix();
//
////            System.out.printf("%d geo tiles, image tiles: ", dc.getSurfaceGeometry().size());
//            for (SectorGeometry sg : dc.getSurfaceGeometry())
//            {
//                TextureTile[] tilesToRender = this.getIntersectingTiles(sg);
//                if (tilesToRender == null)
//                    continue;
////                System.out.printf("%d, ", tilesToRender.length);
//
//                int numTilesRendered = 0;
//                while (numTilesRendered < tilesToRender.length)
//                {
//                    int numTexUnitsUsed = 0;
//                    while (numTexUnitsUsed < dc.getNumTextureUnits() && numTilesRendered < tilesToRender.length)
//                    {
//                        if (this.setupTileTexture(dc, tilesToRender[numTilesRendered++],
//                            GL.GL_TEXTURE0 + numTexUnitsUsed))
//                        {
//                            ++numTexUnitsUsed;
//                        }
//                    }
//
//                    gl.glUniform1i(maxTexUnit, numTexUnitsUsed - 1);
//                    sg.renderMultiTexture(dc, numTexUnitsUsed);
//                }
//            }
////            System.out.println();
//
//            gl.glActiveTexture(GL.GL_TEXTURE0);
//            gl.glUseProgram(0);
//            gl.glMatrixMode(javax.media.opengl.GL.GL_TEXTURE);
//            gl.glPopMatrix();
//        }
//        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();
//        }
//    }
