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

import gov.nasa.worldwind.geom.*;

import java.net.*;

/**
 * @author tag
 * @version $Id: Level.java 1732 2007-05-05 07:47:37Z tgaskins $
 */
public class Level implements Comparable<Level>
{
    public static final String NUM_LEVELS = "gov.nasa.worldwind.Level.NumLevels";
    public static final String NUM_EMPTY_LEVELS = "gov.nasa.worldwind.Level.NumEmptyLevels";
    public static final String LEVEL_ZERO_TILE_DELTA = "gov.nasa.worldwind.Level.LevelZeroTileDelta";
    public static final String LEVEL_NUMBER = "gov.nasa.worldwind.Level.LevelNumberKey";
    public static final String LEVEL_NAME = "gov.nasa.worldwind.Level.LevelNameKey";
    public static final String TILE_DELTA = "gov.nasa.worldwind.Level.TileDeltaKey";
    public static final String TILE_WIDTH = "gov.nasa.worldwind.Level.TileWidthKey";
    public static final String TILE_HEIGHT = "gov.nasa.worldwind.Level.TileHeightKey";
    public static final String CACHE_NAME = "gov.nasa.worldwind.Level.CacheNameKey";
    public static final String SERVICE = "gov.nasa.worldwind.Level.ServiceURLKey";
    public static final String DATASET_NAME = "gov.nasa.worldwind.Level.DatasetNameKey";
    public static final String FORMAT_SUFFIX = "gov.nasa.worldwind.Level.FormatSuffixKey";
    public static final String EXPIRY_TIME = "gov.nasa.worldwind.Level.ExpiryTime";
    public static final String TILE_URL_BUILDER = "gov.nasa.worldwind.Level.TileURLBuilder";
    public static final String MAX_ABSENT_TILE_ATTEMPTS = "gov.nasa.worldwind.Level.MaxAbsentTileAttempts";
    public static final String MIN_ABSENT_TILE_CHECK_INTERVAL = "gov.nasa.worldwind.Level.MinAbsentTileCheckInterval";

    private final AVList params;
    private final int levelNumber;
    private final String levelName; // null or empty level name signifies no data resources associated with this level
    private final LatLon tileDelta;
    private final int tileWidth;
    private final int tileHeight;
    private final String cacheName;
    private final String service;
    private final String dataset;
    private final String formatSuffix;
    private final double averageTexelSize;
    private final String path;
    private final TileURLBuilder urlbuilder;
    private long expiryTime = 0;

    // Absent tiles: A tile is deemed absent if a specified maximum number of attempts have been made to retrieve it.
    // Retrieval attempts are governed by a minimum time interval between successive attempts. If an attempt is made
    // within this interval, the tile is still deemed to be absent until the interval expires.
    private final AbsentResourceList absentTiles;
    private static final int DEFAULT_MAX_ABSENT_TILE_ATTEMPTS = 2;
    private static final int DEFAULT_MIN_ABSENT_TILE_CHECK_INTERVAL = 10000; // milliseconds

    public Level(AVList params)
    {
        if (params == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.LayerParams");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        this.params = params.copy(); // Private copy to insulate from subsequent changes by the app
        String message = this.validate(params);
        if (message != null)
        {
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        String ln = this.params.getStringValue(LEVEL_NAME);
        this.levelName = ln != null ? ln : "";

        this.levelNumber = (Integer) this.params.getValue(LEVEL_NUMBER);
        this.tileDelta = (LatLon) this.params.getValue(TILE_DELTA);
        this.tileWidth = (Integer) this.params.getValue(TILE_WIDTH);
        this.tileHeight = (Integer) this.params.getValue(TILE_HEIGHT);
        this.cacheName = this.params.getStringValue(CACHE_NAME);
        this.service = this.params.getStringValue(SERVICE);
        this.dataset = this.params.getStringValue(DATASET_NAME);
        this.formatSuffix = this.params.getStringValue(FORMAT_SUFFIX);
        this.urlbuilder = (TileURLBuilder) this.params.getValue(TILE_URL_BUILDER);

        double averageTileSize = 0.5 * (this.tileWidth + this.tileHeight);
        double averageTileDelta =
            0.5 * (this.tileDelta.getLatitude().getRadians() + this.tileDelta.getLongitude().getRadians());
        this.averageTexelSize = averageTileDelta / averageTileSize;

        this.path = this.cacheName + "/" + this.levelName;

        Integer maxAbsentTileAttempts = (Integer) this.params.getValue(MAX_ABSENT_TILE_ATTEMPTS);
        if (maxAbsentTileAttempts == null)
            maxAbsentTileAttempts = DEFAULT_MAX_ABSENT_TILE_ATTEMPTS;

        Integer minAbsentTileCheckInterval = (Integer) this.params.getValue(MIN_ABSENT_TILE_CHECK_INTERVAL);
        if (minAbsentTileCheckInterval == null)
            minAbsentTileCheckInterval = DEFAULT_MIN_ABSENT_TILE_CHECK_INTERVAL;

        this.absentTiles = new AbsentResourceList(maxAbsentTileAttempts, minAbsentTileCheckInterval);
    }

    /**
     * Determines whether the constructor arguments are valid.
     * @param params the list of parameters to validate.
     * @return null if valid, otherwise a <code>String</code> containing a description of why it's invalid.
     */
    protected String validate(AVList params)
    {
        StringBuffer sb = new StringBuffer();

        Object o = params.getValue(LEVEL_NUMBER);
        if (o == null || !(o instanceof Integer) || ((Integer) o) < 0)
            sb.append(WorldWind.retrieveErrMsg("term.levelNumber"));

        o = params.getValue(LEVEL_NAME);
        if (o == null || !(o instanceof String))
            sb.append(WorldWind.retrieveErrMsg("term.levelName"));

        o = params.getValue(TILE_WIDTH);
        if (o == null || !(o instanceof Integer) || ((Integer) o) < 0)
            sb.append(WorldWind.retrieveErrMsg("term.tileWidth"));

        o = params.getValue(TILE_HEIGHT);
        if (o == null || !(o instanceof Integer) || ((Integer) o) < 0)
            sb.append(WorldWind.retrieveErrMsg("term.tileHeight"));

        o = params.getValue(TILE_DELTA);
        if (o == null || !(o instanceof LatLon))
            sb.append(WorldWind.retrieveErrMsg("term.tileDelta"));

        o = params.getValue(CACHE_NAME);
        if (o == null || !(o instanceof String) || ((String) o).length() < 1)
            sb.append(WorldWind.retrieveErrMsg("term.cacheFolder"));

        o = params.getValue(TILE_URL_BUILDER);
        if (o == null || !(o instanceof TileURLBuilder))
            sb.append(WorldWind.retrieveErrMsg("term.tileURLBuilder"));

        o = params.getValue(EXPIRY_TIME);
        if (o != null && (!(o instanceof Long) || ((Long) o) < 1))
            sb.append(WorldWind.retrieveErrMsg("term.expiryTime"));

        if (params.getStringValue(LEVEL_NAME).length() > 0)
        {
            o = params.getValue(SERVICE);
            if (o == null || !(o instanceof String) || ((String) o).length() < 1)
                sb.append(WorldWind.retrieveErrMsg("term.service"));

            o = params.getValue(DATASET_NAME);
            if (o == null || !(o instanceof String) || ((String) o).length() < 1)
                sb.append(WorldWind.retrieveErrMsg("term.datasetName"));

            o = params.getValue(FORMAT_SUFFIX);
            if (o == null || !(o instanceof String) || ((String) o).length() < 1)
                sb.append(WorldWind.retrieveErrMsg("term.formatSuffix"));
        }

        if (sb.length() == 0)
            return null;

        return sb.insert(0, WorldWind.retrieveErrMsg("layers.LevelSet.InvalidLevelDescriptorFields")).toString();
    }

    public final AVList getParams()
    {
        return params;
    }

    public String getPath()
    {
        return this.path;
    }

    public final int getLevelNumber()
    {
        return this.levelNumber;
    }

    public String getLevelName()
    {
        return this.levelName;
    }

    public LatLon getTileDelta()
    {
        return this.tileDelta;
    }

    public final int getTileWidth()
    {
        return this.tileWidth;
    }

    public final int getTileHeight()
    {
        return this.tileHeight;
    }

    public final String getFormatSuffix()
    {
        return this.formatSuffix;
    }

    public final String getService()
    {
        return this.service;
    }

    public final String getDataset()
    {
        return this.dataset;
    }

    public final String getCacheName()
    {
        return this.cacheName;
    }

    public final double getTexelSize(double radius)
    {
        return radius * this.averageTexelSize;
    }

    public final boolean isEmpty()
    {
        return this.levelName == null || this.levelName.equals("");
    }

    public final void markResourceAbsent(long tileNumber)
    {
        this.absentTiles.markResourceAbsent(tileNumber);
    }

    public final boolean isResourceAbsent(long tileNumber)
    {
        return this.absentTiles.isResourceAbsent(tileNumber);
    }

    public final void unmarkResourceAbsent(long tileNumber)
    {
        this.absentTiles.unmarkResourceAbsent(tileNumber);
    }

    public final long getExpiryTime()
    {
        return expiryTime;
    }

    public void setExpiryTime(long expiryTime) // TODO: remove
    {
        this.expiryTime = expiryTime;
    }

    public interface TileURLBuilder
    {
        public URL getURL(Tile tile) throws java.net.MalformedURLException;
    }

    /**
     * Returns the URL necessary to retrieve the specified tile.
     * @param tile the tile who's resources will be retrieved.
     * @return the resource URL.
     * @throws java.net.MalformedURLException if the URL cannot be formed from the tile's parameters.
     * @throws IllegalArgumentException if <code>tile</code> is null.
     */
    public java.net.URL getTileResourceURL(Tile tile) throws java.net.MalformedURLException
    {
        if (tile == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.TileIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        return this.urlbuilder.getURL(tile);
    }

    public int compareTo(Level that)
    {
        if (that == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.LevelIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }
        return this.levelNumber < that.levelNumber ? -1 : this.levelNumber == that.levelNumber ? 0 : 1;
    }

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

        final gov.nasa.worldwind.Level level = (gov.nasa.worldwind.Level) o;

        if (levelNumber != level.levelNumber)
            return false;
        if (tileHeight != level.tileHeight)
            return false;
        if (tileWidth != level.tileWidth)
            return false;
        if (cacheName != null ? !cacheName.equals(level.cacheName) : level.cacheName != null)
            return false;
        if (dataset != null ? !dataset.equals(level.dataset) : level.dataset != null)
            return false;
        if (formatSuffix != null ? !formatSuffix.equals(level.formatSuffix) : level.formatSuffix != null)
            return false;
        if (levelName != null ? !levelName.equals(level.levelName) : level.levelName != null)
            return false;
        if (service != null ? !service.equals(level.service) : level.service != null)
            return false;
        //noinspection RedundantIfStatement
        if (tileDelta != null ? !tileDelta.equals(level.tileDelta) : level.tileDelta != null)
            return false;

        return true;
    }

    public int hashCode()
    {
        int result;
        result = levelNumber;
        result = 29 * result + (levelName != null ? levelName.hashCode() : 0);
        result = 29 * result + (tileDelta != null ? tileDelta.hashCode() : 0);
        result = 29 * result + tileWidth;
        result = 29 * result + tileHeight;
        result = 29 * result + (formatSuffix != null ? formatSuffix.hashCode() : 0);
        result = 29 * result + (service != null ? service.hashCode() : 0);
        result = 29 * result + (dataset != null ? dataset.hashCode() : 0);
        result = 29 * result + (cacheName != null ? cacheName.hashCode() : 0);
        return result;
    }

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