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

import javax.media.opengl.*;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import static java.util.logging.Level.*;

/**
 * @author dcollins
 * @version $Id: RpfLayer.java 1774 2007-05-08 01:03:37Z dcollins $
 */
public class RpfLayer extends AbstractLayer
{
    public static final Angle DefaultDeltaLat = Angle.fromDegrees(0.05);
    public static final Angle DefaultDeltaLon = Angle.fromDegrees(0.075);
    private final RpfDataSeries dataSeries;
    private final MemoryCache memoryCache;
    private Angle deltaLat;
    private Angle deltaLon;

    public RpfLayer(RpfDataSeries dataSeries)
    {
        this(dataSeries, DefaultDeltaLat, DefaultDeltaLon);
    }

    public RpfLayer(RpfDataSeries dataSeries, Angle deltaLat, Angle deltaLon)
    {
        this(dataSeries, deltaLat, deltaLon, null);
    }

    public RpfLayer(RpfDataSeries dataSeries, Angle deltaLat, Angle deltaLon,
        MemoryCache sharedMemoryCache)
    {
        if (dataSeries == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.RpfDataSeriesIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        if (deltaLat == null || deltaLon == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.AngleIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.dataSeries = dataSeries;
        this.deltaLat = deltaLat;
        this.deltaLon = deltaLon;
        // Initialize the MemoryCache.
        if (sharedMemoryCache != null)
        {
            this.memoryCache = sharedMemoryCache;
        }
        else
        {
            this.memoryCache = new BasicMemoryCache();
            this.memoryCache.addCacheListener(new MemoryCache.CacheListener()
            {
                public void entryRemoved(Object key, Object clientObject)
                {
                    if (clientObject == null || !(clientObject instanceof TextureTile))
                        return;
                    TextureTile textureTile = (TextureTile) clientObject;
                    disposalQueue.offer(textureTile);
                }
            });
        }
        this.updateMemoryCache();
    }

    public static long estimateMemoryCacheCapacity(RpfDataSeries dataSeries, Angle deltaLat, Angle deltaLon)
    {
        RpfZone.ZoneValues zoneValues = RpfZone.Zone1.zoneValues(dataSeries);
        long numRows = (long) Math.ceil(deltaLat.divide(zoneValues.latitudinalFrameExtent));
        long numCols = (long) Math.ceil(deltaLon.divide(zoneValues.longitudinalFrameExtent));
        long capacity = numRows * numCols * 4L * 1024L * 1024L;
        return 32L * (long) Math.ceil(capacity / 32d);
    }

    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append(this.dataSeries.seriesCode);
        sb.append(": ");
        sb.append(this.dataSeries.dataSeries);
        return sb.toString();
    }

    public Angle getViewingDeltaLat()
    {
        return this.deltaLat;
    }

    public Angle getViewingDeltaLon()
    {
        return this.deltaLon;
    }

    public void setViewingDeltaLat(Angle deltaLat)
    {
        this.deltaLat = deltaLat;
        this.updateMemoryCache();
    }

    public void setViewingDeltaLon(Angle deltaLon)
    {
        this.deltaLon = deltaLon;
        this.updateMemoryCache();
    }

    private void updateMemoryCache()
    {
        long capacity = estimateMemoryCacheCapacity(this.dataSeries, this.deltaLat, this.deltaLon);
        if (this.memoryCache.getCapacity() < capacity)
            this.memoryCache.setCapacity(capacity);
    }

    // ============== Frame Directory ======================= //
    // ============== Frame Directory ======================= //
    // ============== Frame Directory ======================= //

    private final static String RPF_OVERVIEW_EXTENSION = ".OVR";
    private final Map<FrameKey, FrameRecord> frameDirectory
        = new HashMap<FrameKey, FrameRecord>();
    private Sector sector = Sector.EMPTY_SECTOR;
    private int modCount = 0;
    private int lastModCount = 0;

    private static class FrameKey
    {
        public final RpfZone zone;
        public final int frameNumber;
        private final int hashCode;

        public FrameKey(RpfZone zone, int frameNumber)
        {
            this.zone = zone;
            this.frameNumber = frameNumber;
            this.hashCode = this.computeHash();
        }

        private int computeHash()
        {
            int hash = 0;
            if (this.zone != null)
                hash = 29 * hash + this.zone.ordinal();
            hash = 29 * hash + this.frameNumber;
            return hash;
        }

        public boolean equals(Object o)
        {
            if (this == o)
                return true;
            if (o == null || !o.getClass().equals(this.getClass()))
                return false;
            final FrameKey that = (FrameKey) o;
            return (this.zone == that.zone) && (this.frameNumber == that.frameNumber);
        }

        public int hashCode()
        {
            return this.hashCode;
        }
    }

    private static class FrameRecord
    {
        public final RpfFrameProperties properties;
        public final Sector sector;
        public final String filePath;
        public final String cacheFilePath;
        private boolean corruptCache = false;
        final Lock fileLock = new ReentrantLock();

        public FrameRecord(RpfFrameProperties properties, Sector sector, String filePath, String cacheFilePath)
        {
            this.properties = properties;
            this.sector = sector;
            this.filePath = filePath;
            this.cacheFilePath = cacheFilePath;
        }

        public boolean equals(Object o)
        {
            if (this == o)
                return true;
            if (o == null || !o.getClass().equals(this.getClass()))
                return false;
            final FrameRecord that = (FrameRecord) o;
            return this.filePath.equals(that.filePath) && (this.properties.equals(that.properties));
        }

        public boolean isCorruptCache()
        {
            return this.corruptCache;
        }

        public void setCorruptCache(boolean corruptCache)
        {
            this.corruptCache = corruptCache;
        }
    }

    public int addAll(Collection<RpfTocFile> tocFiles)
    {
        if (tocFiles == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.CollectionIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        int startModCount = this.modCount;
        // Fill frame directory with contents in 'tocFiles'.
        for (RpfTocFile file : tocFiles)
        {
            if (file != null)
                this.addContents(file);
        }
        return this.modCount - startModCount;
    }

    public int addContents(RpfTocFile tocFile)
    {
        if (tocFile == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.RpfTocFileIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        RpfFrameFileIndexSection indexSection = tocFile.getFrameFileIndexSection();
        if (indexSection == null)
            return 0;
        ArrayList<RpfFrameFileIndexSection.RpfFrameFileIndexRecord> indexTable = indexSection.getFrameFileIndexTable();
        if (indexTable == null)
            return 0;
        int startModCount = this.modCount;
        for (RpfFrameFileIndexSection.RpfFrameFileIndexRecord indexRecord : indexTable)
        {
            if (!indexRecord.getFrameFileName().toUpperCase().endsWith(RPF_OVERVIEW_EXTENSION))
            {
                FrameRecord record = null;
                try
                {
                    record = createRecord(tocFile, indexRecord);
                }
                catch (Exception e)
                {
                    String message = WorldWind.retrieveErrMsg("layers.RpfLayer.ExceptionParsingFileName")
                        + indexRecord.getFrameFileName();
                    WorldWind.logger().log(FINE, message, e);
                }
                if (record != null && this.dataSeries == record.properties.dataSeries)
                {
                    this.addRecord(record);
                }
            }
        }
        return this.modCount - startModCount;
    }

    private void addRecord(FrameRecord record)
    {
        FrameKey key = keyFor(record);
        this.frameDirectory.put(key, record);
        ++this.modCount;
    }

    private static String cachePathFor(RpfFrameProperties properties)
    {
        StringBuilder sb = new StringBuilder();
        sb.append("Earth").append(File.separatorChar);
        sb.append("RPF").append(File.separatorChar);
        sb.append(properties.dataSeries.seriesCode).append(File.separatorChar);
        sb.append(properties.zone.zoneCode).append(File.separatorChar);
        sb.append(RpfFrameFilenameUtil.filenameFor(properties));
        return sb.toString();
    }

    private static String createAbsolutePath(String... pathElem)
    {
        StringBuilder sb = new StringBuilder();
        for (String str : pathElem)
        {
            if (str != null && str.length() > 0)
            {
                int startIndex = 0;
                if (str.startsWith("./") || str.startsWith(".\\"))
                    startIndex = 1;
                else if (!str.startsWith("/") && !str.startsWith("\\"))
                    sb.append(File.separatorChar);
                int endIndex;
                if (str.endsWith("/") || str.endsWith("\\"))
                    endIndex = str.length() - 1;
                else
                    endIndex = str.length();
                sb.append(str, startIndex, endIndex);
            }
        }
        if (sb.length() <= 0)
            return null;
        return sb.toString();
    }

    private static FrameRecord createRecord(RpfTocFile tocFile,
        RpfFrameFileIndexSection.RpfFrameFileIndexRecord indexRecord)
    {
        RpfFrameProperties frameProps = RpfFrameFilenameUtil.parseFilename(indexRecord.getFrameFileName());
        Sector sector = sectorFor(frameProps);
        String filePath = createAbsolutePath(tocFile.getFile().getParentFile().getAbsolutePath(),
            indexRecord.getPathname(), indexRecord.getFrameFileName());
        String cachePath = cachePathFor(frameProps);
        if (frameProps == null || sector == null || filePath == null || cachePath == null)
        {
            String message = WorldWind.retrieveErrMsg("layers.RpfLayer.BadFrameInput") + indexRecord.getFrameFileName();
            WorldWind.logger().log(FINE, message);
            throw new WWRuntimeException(message);
        }
        return new FrameRecord(frameProps, sector, filePath, cachePath);
    }

    private static FrameKey keyFor(FrameRecord record)
    {
        return new FrameKey(record.properties.zone, record.properties.frameNumber);
    }

    public int removeAll(Collection<RpfTocFile> tocFiles)
    {
        if (tocFiles == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.CollectionIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        int startModCount = this.modCount;
        // Fill frame directory with contents in 'tocFiles'.
        for (RpfTocFile file : tocFiles)
        {
            if (file != null)
                this.removeContents(file);
        }
        return startModCount - this.modCount;
    }

    public int removeContents(RpfTocFile tocFile)
    {
        if (tocFile == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.RpfTocFileIsNull");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        RpfFrameFileIndexSection indexSection = tocFile.getFrameFileIndexSection();
        if (indexSection == null)
            return 0;
        ArrayList<RpfFrameFileIndexSection.RpfFrameFileIndexRecord> indexTable = indexSection.getFrameFileIndexTable();
        if (indexTable == null)
            return 0;
        int startModCount = this.modCount;
        for (RpfFrameFileIndexSection.RpfFrameFileIndexRecord indexRecord : indexTable)
        {
            if (!indexRecord.getFrameFileName().toUpperCase().endsWith(RPF_OVERVIEW_EXTENSION))
            {
                RpfFrameProperties frameProps = null;
                try
                {
                    frameProps = RpfFrameFilenameUtil.parseFilename(indexRecord.getFrameFileName());
                }
                catch (IllegalArgumentException e)
                {
                    String message = WorldWind.retrieveErrMsg("layers.RpfLayer.ExceptionParsingFileName")
                        + indexRecord.getFrameFileName();
                    WorldWind.logger().log(FINE, message, e);
                }
                if (frameProps != null && this.dataSeries == frameProps.dataSeries)
                    this.removeKey(new FrameKey(frameProps.zone, frameProps.frameNumber));
            }
        }
        return startModCount - this.modCount;
    }

    private boolean removeKey(FrameKey key)
    {
        FrameRecord value = this.frameDirectory.remove(key);
        --this.modCount;
        return value != null;
    }

//    private boolean removeRecord(FrameRecord record)
//    {
//        FrameKey key = keyFor(record);
//        FrameRecord value = this.frameDirectory.remove(key);
//        --this.modCount;
//        return value != null;
//    }

    private static Sector sectorFor(RpfFrameProperties properties)
    {
        if (properties == null
            || properties.zone == null
            || properties.dataSeries == null)
            return null;
        RpfZone.ZoneValues zoneValues = properties.zone.zoneValues(properties.dataSeries);
        if (properties.frameNumber < 0 || properties.frameNumber > (zoneValues.maximumFrameNumber - 1))
            return null;
        return zoneValues.frameExtent(properties.frameNumber);
    }

    private void updateSector()
    {
        Sector newSector = null;
        for (FrameRecord record : this.frameDirectory.values())
        {
            if (record.sector != null)
                newSector = (newSector != null) ? newSector.union(record.sector) : record.sector;
        }
        this.sector = newSector;
    }

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

    private final Queue<TextureTile> assemblyQueue = new LinkedList<TextureTile>();
    private final Queue<FrameRecord> assemblyRequestQueue = new LinkedList<FrameRecord>();

    private void assembleFrameTiles(DrawContext dc, RpfDataSeries dataSeries, Sector viewingSector,
        Queue<TextureTile> tilesToRender, Queue<FrameRecord> framesToRequest)
    {
        for (RpfZone zone : RpfZone.values())
        {
            RpfZone.ZoneValues zoneValues = zone.zoneValues(dataSeries);
            Sector sector = zoneValues.extent.intersection(viewingSector);
            if (sector != null && isSectorVisible(dc, sector))
                this.assembleZoneTiles(dc, zoneValues, sector, tilesToRender, framesToRequest);
        }
    }

    private void assembleZoneTiles(DrawContext dc, RpfZone.ZoneValues zoneValues, Sector sector,
        Queue<TextureTile> tilesToRender, Queue<FrameRecord> framesToRequest)
    {
        int startRow = zoneValues.frameRowFromLatitude(sector.getMinLatitude());
        int endRow = zoneValues.frameRowFromLatitude(sector.getMaxLatitude());
        int startCol = zoneValues.frameColumnFromLongitude(sector.getMinLongitude());
        int endCol = zoneValues.frameColumnFromLongitude(sector.getMaxLongitude());

        for (int row = startRow; row <= endRow; row++)
        {
            for (int col = startCol; col <= endCol; col++)
            {
                int frameNum = zoneValues.frameNumber(row, col);
                getOrRequestTile(dc, new FrameKey(zoneValues.zone, frameNum), tilesToRender, framesToRequest);
            }
        }
    }

    private void getOrRequestTile(DrawContext dc, FrameKey key, Queue<TextureTile> tilesToRender,
        Queue<FrameRecord> framesToRequest)
    {
        TextureTile tile = this.getTile(key);
        if (tile != null)
        {
            if (isSectorVisible(dc, tile.getSector()))
                tilesToRender.offer(tile);
        }
        else
        {
            FrameRecord record = this.frameDirectory.get(key);
            if (record != null && isSectorVisible(dc, record.sector))
                framesToRequest.offer(record);
        }
    }

    private static Sector[] normalizeSector(Sector sector)
    {
        Angle minLat = clampAngle(sector.getMinLatitude(), Angle.NEG90, Angle.POS90);
        Angle maxLat = clampAngle(sector.getMaxLatitude(), Angle.NEG90, Angle.POS90);
        if (maxLat.degrees < minLat.degrees)
        {
            Angle tmp = minLat;
            minLat = maxLat;
            maxLat = tmp;
        }

        Angle minLon = normalizeAngle(sector.getMinLongitude(), Angle.NEG180, Angle.POS180);
        Angle maxLon = normalizeAngle(sector.getMaxLongitude(), Angle.NEG180, Angle.POS180);
        if (maxLon.degrees < minLon.degrees)
        {
            return new Sector[] {
                new Sector(minLat, maxLat, minLon, Angle.POS180),
                new Sector(minLat, maxLat, Angle.NEG180, maxLon),
            };
        }

        return new Sector[] {new Sector(minLat, maxLat, minLon, maxLon)};
    }

    private static Sector createViewSector(Angle centerLat, Angle centerLon, Angle deltaLat, Angle deltaLon)
    {
        return new Sector(centerLat.subtract(deltaLat), centerLat.add(deltaLat),
            centerLon.subtract(deltaLon), centerLon.add(deltaLon));
    }

    private static Angle clampAngle(Angle angle, Angle min, Angle max)
    {
        return (angle.degrees < min.degrees) ? min : ((angle.degrees > max.degrees) ? max : angle);
    }

    private static Angle normalizeAngle(Angle angle, Angle min, Angle max)
    {
        Angle range = max.subtract(min);
        return (angle.degrees < min.degrees) ?
            angle.add(range) : ((angle.degrees > max.degrees) ? angle.subtract(range) : angle);
    }

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

    private static final BlockingQueue<Disposable> disposalQueue = new LinkedBlockingQueue<Disposable>();
    //    private final List<TextureTile> tileQueue = new ArrayList<TextureTile>();
    private final SurfaceTileRenderer tileRenderer = new SurfaceTileRenderer();
    private final IconRenderer iconRenderer = new IconRenderer();
    private TextureTile coverageTile;
    private WWIcon coverageIcon;
    private int tileGridDrawThreshold = 30;
    private boolean drawCoverage = true;
    private boolean drawCoverageIcon = true;

    private static TextureData createCoverageTextureData(Sector sector, Collection<FrameRecord> records,
        int width, int height, int fgColor, int bgColor)
    {
        IntBuffer buffer = BufferUtil.newIntBuffer(width * height);
        for (int i = 0; i < width * height; i++)
        {
            buffer.put(i, bgColor);
        }
        buffer.rewind();

        Angle latWidth = sector.getMaxLatitude().subtract(sector.getMinLatitude());
        Angle lonWidth = sector.getMaxLongitude().subtract(sector.getMinLongitude());
        for (FrameRecord record : records)
        {
            int x0 = (int) Math.round((width - 1)
                * record.sector.getMinLongitude().subtract(sector.getMinLongitude()).divide(lonWidth));
            int y0 = (int) Math.round((height - 1)
                * record.sector.getMinLatitude().subtract(sector.getMinLatitude()).divide(latWidth));
            int x1 = (int) Math.round((width - 1)
                * record.sector.getMaxLongitude().subtract(sector.getMinLongitude()).divide(lonWidth));
            int y1 = (int) Math.round((width - 1)
                * record.sector.getMaxLatitude().subtract(sector.getMinLatitude()).divide(latWidth));
            for (int y = y0; y <= y1; y++)
            {
                for (int x = x0; x <= x1; x++)
                {
                    buffer.put(x + y * width, fgColor);
                }
            }
        }
        buffer.rewind();
        return new TextureData(GL.GL_RGBA, width, height, 0, GL.GL_RGBA, GL.GL_UNSIGNED_INT_8_8_8_8,
            false, false, false, buffer, null);
    }

    public void dispose()
    {
        if (this.tileRenderer != null)
            disposalQueue.offer(this.tileRenderer);
        if (this.iconRenderer != null)
            disposalQueue.offer(this.iconRenderer);
        if (this.coverageTile != null)
            disposalQueue.offer(this.coverageTile);
        this.coverageTile = null;
        processDisposables();
    }

    protected void doRender(DrawContext dc)
    {
        // Process disposable queue.
        processDisposables();

        if (!isSectorVisible(dc, this.sector))
            return;

        // Update sector and coverage renderables when frame contents change.
        if (this.modCount != this.lastModCount)
        {
            this.updateSector();
            this.updateCoverage();
            this.lastModCount = this.modCount;
        }

        GL gl = dc.getGL();

        // Coverage tile.
        if (this.drawCoverage && this.coverageTile != null)
        {
            int attribBits = GL.GL_ENABLE_BIT | GL.GL_COLOR_BUFFER_BIT | GL.GL_POLYGON_BIT;
            gl.glPushAttrib(attribBits);
            try
            {
                gl.glEnable(GL.GL_BLEND);
                gl.glEnable(GL.GL_CULL_FACE);
                gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
                gl.glCullFace(GL.GL_BACK);
                gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL);
                this.tileRenderer.renderTile(dc, this.coverageTile);
            }
            finally
            {
                gl.glPopAttrib();
            }
        }
        // Assemble frame tiles.
        this.assemblyQueue.clear();
        this.assemblyRequestQueue.clear();
        Position viewPosition = dc.getView().getPosition();
        Sector viewingSector = createViewSector(viewPosition.getLatitude(), viewPosition.getLongitude(),
            this.deltaLat, this.deltaLon);
        for (Sector sector : normalizeSector(viewingSector))
        {
            this.assembleFrameTiles(dc, this.dataSeries, sector, this.assemblyQueue, this.assemblyRequestQueue);
        }
        Sector drawSector = null;
        for (TextureTile tile : this.assemblyQueue)
        {
            drawSector = (drawSector != null) ? drawSector.union(tile.getSector()) : tile.getSector();
        }

        boolean drawFrameTiles = this.tileGridDrawThreshold <= pixelSizeOfSector(dc, viewingSector)
            || (drawSector != null && this.tileGridDrawThreshold <= pixelSizeOfSector(dc, drawSector));
        // Render frame tiles.
        if (drawFrameTiles)
        {
            this.requestAllFrames(this.readQueue, this.assemblyRequestQueue);
            int attribBits = GL.GL_ENABLE_BIT | GL.GL_POLYGON_BIT;
            gl.glPushAttrib(attribBits);
            try
            {
                gl.glEnable(GL.GL_CULL_FACE);
                gl.glCullFace(GL.GL_BACK);
                gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL);
                this.tileRenderer.renderTiles(dc, this.assemblyQueue);
            }
            finally
            {
                gl.glPopAttrib();
            }
        }
        // Render coverage icon.
        else if (this.drawCoverageIcon && this.coverageIcon != null)
        {
            LatLon centroid = (drawSector != null) ? drawSector.getCentroid() : this.sector.getCentroid();
            this.coverageIcon.setPosition(new Position(centroid.getLatitude(), centroid.getLongitude(), 0));
            this.iconRenderer.render(dc, this.coverageIcon, null);
        }

        // Process request queue.
        this.sendRequests(this.assemblyRequestQueue.size());
    }

    public WWIcon getCoverageIcon()
    {
        return this.coverageIcon;
    }

    public int getTileGridDrawThreshold()
    {
        return this.tileGridDrawThreshold;
    }

    private static void initializeFrameTexture(Texture texture)
    {
        texture.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
        texture.setTexParameteri(GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
        texture.setTexParameteri(GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
        texture.setTexParameteri(GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
    }

    private static void initializeOtherTexture(Texture texture)
    {
        texture.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
        texture.setTexParameteri(GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
        texture.setTexParameteri(GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
        texture.setTexParameteri(GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
    }

    public boolean isDrawCoverage()
    {
        return this.drawCoverage;
    }

    public boolean isDrawCoverageIcon()
    {
        return this.drawCoverageIcon;
    }

    private boolean isSectorVisible(DrawContext dc, Sector sector)
    {
        if (dc.getVisibleSector() != null && !sector.intersects(dc.getVisibleSector()))
            return false;

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

    private static int pixelSizeOfSector(DrawContext dc, Sector sector)
    {
        LatLon centroid = sector.getCentroid();
        Globe globe = dc.getGlobe();
        Point centroidPoint = globe.computePointFromPosition(centroid.getLatitude(), centroid.getLongitude(), 0);
        Point minPoint = globe.computePointFromPosition(sector.getMinLatitude(), sector.getMinLongitude(), 0);
        Point maxPoint = globe.computePointFromPosition(sector.getMaxLatitude(), sector.getMaxLongitude(), 0);
        double distanceToEye = centroidPoint.distanceTo(dc.getView().getEyePoint());
        double sectorSize = minPoint.distanceTo(maxPoint);
        double pixelSize = dc.getView().computePixelSizeAtDistance(distanceToEye);
        return (int) Math.round(sectorSize / pixelSize);
    }

    private static void processDisposables()
    {
        Disposable disposable;
        while ((disposable = disposalQueue.poll()) != null)
        {
            disposable.dispose();
        }
    }

    private static int rgbaInt(int r, int g, int b, int a)
    {
        r = (int) (r * (a / 255d));
        g = (int) (g * (a / 255d));
        b = (int) (b * (a / 255d));
        return ((0xFF & r) << 24) + ((0xFF & g) << 16) + ((0xFF & b) << 8) + (0xFF & a);
    }

    public void setCoverageIcon(WWIcon icon)
    {
        if (icon == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.Icon");
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.coverageIcon = icon;
    }

    public void setDrawCoverage(boolean drawCoverage)
    {
        this.drawCoverage = drawCoverage;
    }

    public void setDrawCoverageIcon(boolean drawCoverageIcon)
    {
        this.drawCoverageIcon = drawCoverageIcon;
    }

    public void setTileGridDrawThreshold(int pixelSize)
    {
        if (pixelSize <= 0)
        {
            String message = WorldWind.retrieveErrMsg("generic.ValueOutOfRange") + String.valueOf(pixelSize);
            WorldWind.logger().log(FINE, message);
            throw new IllegalArgumentException(message);
        }
        this.tileGridDrawThreshold = pixelSize;
    }

    private void updateCoverage()
    {
        if (this.coverageTile != null)
            disposalQueue.offer(this.coverageTile);
        TextureData textureData = createCoverageTextureData(this.sector, this.frameDirectory.values(), 1024, 1024,
            rgbaInt(255, 0, 0, 102), rgbaInt(0, 0, 0, 0));
        this.coverageTile = new TextureTile(this.sector)
        {
            public void initializeTexture(DrawContext dc)
            {
                if (this.getTexture() == null)
                {
                    Texture tex = TextureIO.newTexture(this.getTextureData());
                    initializeOtherTexture(tex);
                    this.setTexture(tex);
                }
            }
        };
        this.coverageTile.setTextureData(textureData);
    }

    // ============== Image Reading and Conversion ======================= //
    // ============== Image Reading and Conversion ======================= //
    // ============== Image Reading and Conversion ======================= //

    private final LinkedList<FrameRecord> downloadQueue = new LinkedList<FrameRecord>();
    private final LinkedList<FrameRecord> readQueue = new LinkedList<FrameRecord>();

    private static class RpfRetriever extends WWObjectImpl implements Retriever
    {
        private final RpfLayer layer;
        private final FrameRecord record;
        private volatile RpfImageFile rpfImageFile;
        private volatile ByteBuffer buffer;
        private volatile String state = RETRIEVER_STATE_NOT_STARTED;
        private long submitTime;
        private long beginTime;
        private long endTime;

        public RpfRetriever(RpfLayer layer, FrameRecord record)
        {
            this.layer = layer;
            this.record = record;
        }

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

        public long getBeginTime()
        {
            return this.beginTime;
        }

        public ByteBuffer getBuffer()
        {
            return this.buffer;
        }

        public int getContentLength()
        {
            return 0;
        }

        public int getContentLengthRead()
        {
            return 0;
        }

        public String getContentType()
        {
            return null;
        }

        public long getEndTime()
        {
            return this.endTime;
        }

        public String getName()
        {
            return this.record.filePath;
        }

        public String getState()
        {
            return this.state;
        }

        public long getSubmitTime()
        {
            return this.submitTime;
        }

        private boolean interrupted()
        {
            if (Thread.currentThread().isInterrupted())
            {
                this.setState(RETRIEVER_STATE_INTERRUPTED);
                String message = WorldWind.retrieveErrMsg("layers.RpfLayer.DownloadInterrupted")
                    + this.record.filePath;
                WorldWind.logger().log(FINER, message);
                return true;
            }
            return false;
        }

        public void setBeginTime(long beginTime)
        {
            this.beginTime = beginTime;
        }

        public void setEndTime(long endTime)
        {
            this.endTime = endTime;
        }

        private void setState(String state)
        {
            String oldState = this.state;
            this.state = state;
            this.firePropertyChange(AVKey.RETRIEVER_STATE, oldState, this.state);
        }

        public void setSubmitTime(long submitTime)
        {
            this.submitTime = submitTime;
        }

        public Retriever call() throws Exception
        {
            if (this.interrupted())
                return this;

            if (!this.record.fileLock.tryLock())
            {
                this.setState(RETRIEVER_STATE_SUCCESSFUL);
                return this;
            }
            try
            {
                this.setState(RETRIEVER_STATE_STARTED);

                if (!this.interrupted())
                {
                    if (isFileResident(this.record.cacheFilePath))
                    {
                        this.setState(RETRIEVER_STATE_SUCCESSFUL);
                        return this;
                    }
                }

                if (!this.interrupted())
                {
                    this.setState(RETRIEVER_STATE_CONNECTING);
                    File file = new File(this.record.filePath);
                    if (!file.exists())
                    {
                        String message = WorldWind.retrieveErrMsg("generic.fileNotFound") + this.record.filePath;
                        throw new IOException(message);
                    }
                    this.rpfImageFile = RpfImageFile.load(file);
                }

                if (!this.interrupted())
                {
                    this.setState(RETRIEVER_STATE_READING);
                    File file = WorldWind.dataFileCache().newFile(this.record.cacheFilePath);
                    if (file == null)
                    {
                        String message = WorldWind.retrieveErrMsg("generic.CantCreateCacheFile")
                            + this.record.cacheFilePath;
                        throw new IOException(message);
                    }
                    this.buffer = this.rpfImageFile.getImageAsDdsTexture();
                    WWIO.saveBuffer(this.buffer, file);
                }

                if (!this.interrupted())
                {
                    this.setState(RETRIEVER_STATE_SUCCESSFUL);
                    this.layer.firePropertyChange(AVKey.LAYER, null, this.layer);
                }
            }
            catch (Exception e)
            {
                this.setState(RETRIEVER_STATE_ERROR);
                throw e;
            }
            finally
            {
                this.record.fileLock.unlock();
            }

            return this;
        }
    }

    private static class ReadTask implements Runnable
    {
        public final RpfLayer layer;
        public final FrameRecord record;

        public ReadTask(RpfLayer layer, FrameRecord record)
        {
            this.layer = layer;
            this.record = record;
        }

        private void deleteCorruptFrame(RpfLayer layer, FrameRecord record)
        {
            URL file = WorldWind.dataFileCache().findFile(record.cacheFilePath, false);
            if (file != null)
                WorldWind.dataFileCache().removeFile(file);
            record.setCorruptCache(false);
            layer.firePropertyChange(AVKey.LAYER, null, layer);
            String message = WorldWind.retrieveErrMsg("generic.DeletedCorruptDataFile")
                + ((file != null) ? file.getFile() : "null");
            WorldWind.logger().log(FINE, message);
        }

        public boolean equals(Object o)
        {
            if (this == o)
                return true;
            if (o == null || !o.getClass().equals(this.getClass()))
                return false;
            final ReadTask that = (ReadTask) o;
            return (this.record != null) ? this.record.equals(that.record) : (that.record == null);
        }

        public void run()
        {
            FrameKey key = keyFor(this.record);
            if (!this.layer.isTileResident(key))
            {
                this.readFrame(key, this.record);
                this.layer.firePropertyChange(AVKey.LAYER, null, this.layer);
            }
        }

        public void readFrame(FrameKey key, FrameRecord record)
        {
            URL dataFileURL = WorldWind.dataFileCache().findFile(record.cacheFilePath, false);
            if (dataFileURL == null)
            {
                this.layer.requestFrame(this.layer.downloadQueue, this.record);
                return; // Return when cached file does not exist.
            }

            if (!record.fileLock.tryLock())
                return;
            try
            {
                if (this.layer.isTileResident(key))
                    return;

                TextureData textureData = null;
                try
                {
                    textureData = TextureIO.newTextureData(dataFileURL, false, "dds");
                }
                catch (IOException e)
                {
                    String message = WorldWind.retrieveErrMsg("generic.TextureIOException") + record.cacheFilePath;
                    WorldWind.logger().log(FINE, message, e);
                }

                if (textureData == null)
                {
                    this.deleteCorruptFrame(this.layer, this.record);
                    return;
                }

                TextureTile textureTile = new TextureTile(record.sector)
                {
                    public void initializeTexture(DrawContext dc)
                    {
                        if (this.getTexture() == null)
                        {
                            Texture tex = TextureIO.newTexture(this.getTextureData());
                            initializeFrameTexture(tex);
                            this.setTexture(tex);
                        }
                    }
                };
                textureTile.setTextureData(textureData);
                this.layer.makeTileResident(key, textureTile);
            }
            finally
            {
                record.fileLock.unlock();
            }
        }
    }

    private TextureTile getTile(FrameKey key)
    {
        synchronized (this.memoryCache)
        {
            Object obj = this.memoryCache.getObject(key);
            if (obj != null && obj instanceof TextureTile)
                return (TextureTile) obj;
            return null;
        }
    }

    private static boolean isFileResident(String fileName)
    {
        return WorldWind.dataFileCache().findFile(fileName, false) != null;
    }

    private boolean isTileResident(FrameKey key)
    {
        synchronized (this.memoryCache)
        {
            return this.memoryCache.getObject(key) != null;
        }
    }

    private void makeTileResident(FrameKey key, TextureTile tile)
    {
        synchronized (this.memoryCache)
        {
            this.memoryCache.add(key, tile);
        }
    }

    private void requestFrame(LinkedList<FrameRecord> queue, FrameRecord record)
    {
        synchronized (queue)
        {
            if (queue.contains(record))
                return;
            queue.addFirst(record);
        }
    }

    private void requestAllFrames(LinkedList<FrameRecord> queue, Collection<FrameRecord> frameRecords)
    {
        for (FrameRecord record : frameRecords)
        {
            this.requestFrame(queue, record);
        }
    }

    private void sendRequests(int maxRequests)
    {
        synchronized (this.readQueue)
        {
            while (this.readQueue.size() > maxRequests)
            {
                this.readQueue.removeLast();
            }
            // Send threaded read tasks.
            FrameRecord record;
            while (!WorldWind.threadedTaskService().isFull() && (record = this.readQueue.poll()) != null)
            {
                WorldWind.threadedTaskService().addTask(new ReadTask(this, record));
            }
        }

        synchronized (this.downloadQueue)
        {
            while (this.downloadQueue.size() > maxRequests)
            {
                this.downloadQueue.removeLast();
            }
            // Send retriever tasks.
            FrameRecord record;
            while (!WorldWind.retrievalService().isFull() && (record = this.downloadQueue.poll()) != null)
            {
                WorldWind.retrievalService().runRetriever(new RpfRetriever(this, record));
            }
        }
    }
}
