/*
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.*;
import java.util.*;

/**
 * @author tag
 * @version $Id: LevelSet.java 1746 2007-05-06 17:21:30Z tgaskins $
 */
public class LevelSet extends WWObjectImpl
{
    private final Sector sector;
    private final LatLon levelZeroTileDelta;
    private final int numLevelZeroColumns;
    private final java.util.ArrayList<Level> levels = new java.util.ArrayList<Level>();

    public LevelSet(AVList params)
    {
        StringBuffer sb = new StringBuffer();

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

        o = params.getValue(AVKey.SECTOR);
        if (o == null || !(o instanceof Sector))
            sb.append(WorldWind.retrieveErrMsg("term.sector"));

        int numLevels = 0;
        o = params.getValue(Level.NUM_LEVELS);
        if (o == null || !(o instanceof Integer) || (numLevels = (Integer) o) < 1)
            sb.append(WorldWind.retrieveErrMsg("term.numLevels"));

        int numEmptyLevels = 0;
        o = params.getValue(Level.NUM_EMPTY_LEVELS);
        if (o == null || !(o instanceof Integer) || (numEmptyLevels = (Integer) o) < 0)
            sb.append(WorldWind.retrieveErrMsg("term.numEMptyLevels"));

        if (sb.length() > 0)
        {
            String message = WorldWind.retrieveErrMsg("layers.LevelSet.InvalidLevelDescriptorFields")
                + " " + sb.toString();
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        this.levelZeroTileDelta = (LatLon) params.getValue(Level.LEVEL_ZERO_TILE_DELTA);
        this.sector = (Sector) params.getValue(AVKey.SECTOR);

        params = params.copy(); // copy so as not to modify the user's params
        
        Level.TileURLBuilder tub = (Level.TileURLBuilder) params.getValue(Level.TILE_URL_BUILDER);
        if (tub == null)
        {
            params.setValue(Level.TILE_URL_BUILDER, new Level.TileURLBuilder()
            {
                public URL getURL(Tile tile) throws MalformedURLException
                {
                    StringBuffer sb = new StringBuffer(tile.getLevel().getService());
                    if (sb.lastIndexOf("?") != sb.length() - 1)
                        sb.append("?");
                    sb.append("T=");
                    sb.append(tile.getLevel().getDataset());
                    sb.append("&L=");
                    sb.append(tile.getLevel().getLevelName());
                    sb.append("&X=");
                    sb.append(tile.getColumn());
                    sb.append("&Y=");
                    sb.append(tile.getRow());

                    return new URL(sb.toString());
                }
            });
        }

        for (int i = 0; i < numLevels; i++)
        {
            params.setValue(Level.LEVEL_NAME, i < numEmptyLevels ? "" : Integer.toString(i - numEmptyLevels));
            params.setValue(Level.LEVEL_NUMBER, i);

            Angle latDelta = this.levelZeroTileDelta.getLatitude().divide(Math.pow(2, i));
            Angle lonDelta = this.levelZeroTileDelta.getLongitude().divide(Math.pow(2, i));
            params.setValue(Level.TILE_DELTA, new LatLon(latDelta, lonDelta));

            this.levels.add(new Level(params));
        }

        this.numLevelZeroColumns =
            (int) Math.round(this.sector.getDeltaLon().divide(this.levelZeroTileDelta.getLongitude()));
    }

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

        this.levelZeroTileDelta = source.levelZeroTileDelta;
        this.sector = source.sector;
        this.numLevelZeroColumns = source.numLevelZeroColumns;

        for (Level level : source.levels)
        {
            this.levels.add(level); // Levels are final, so it's safe to copy references.
        }
    }

    public final Sector getSector()
    {
        return this.sector;
    }

    public final LatLon getLevelZeroTileDelta()
    {
        return this.levelZeroTileDelta;
    }

    public final ArrayList<Level> getLevels()
    {
        return this.levels;
    }

    public final Level getLevel(int levelNumber)
    {
        return (levelNumber >= 0 && levelNumber < this.levels.size()) ? this.levels.get(levelNumber) : null;
    }

    public final int getNumLevels()
    {
        return this.levels.size();
    }

    public final Level getFirstLevel()
    {
        return this.getLevel(0);
    }

    public final Level getLastLevel()
    {
        return this.getLevel(this.getNumLevels() - 1);
    }

    public final boolean isFinalLevel(int levelNum)
    {
        return levelNum == this.getNumLevels() - 1;
    }

    public final boolean isLevelEmpty(int levelNumber)
    {
        return this.levels.get(levelNumber).isEmpty();
    }

    private int numColumnsInLevel(Level level)
    {
        int levelDelta = level.getLevelNumber() - this.getFirstLevel().getLevelNumber();
        double twoToTheN = Math.pow(2, levelDelta);
        return (int) (twoToTheN * this.numLevelZeroColumns);
    }

    private long getTileNumber(Tile tile)
    {
        return tile.getRow() * this.numColumnsInLevel(tile.getLevel()) + tile.getColumn();
    }

    /**
     * Instructs the level set that a tile is likely to be absent.
     *
     * @param tile The tile to mark as having an absent resource.
     * @throws IllegalArgumentException if <code>tile</code> is null
     */
    public final void markResourceAbsent(Tile tile)
    {
        if (tile == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.TileIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        tile.getLevel().markResourceAbsent(this.getTileNumber(tile));
    }

    /**
     * Indicates whether a tile has been marked as absent.
     *
     * @param tile The tile in question.
     * @return <code>true</code> if the tile is marked absent, otherwise <code>false</code>.
     * @throws IllegalArgumentException if <code>tile</code> is null
     */
    public final boolean isResourceAbsent(Tile tile)
    {
        if (tile == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.TileIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        if (tile.getLevel().isEmpty())
            return true;

        int tileNumber = tile.getRow() * this.numColumnsInLevel(tile.getLevel()) + tile.getColumn();
        return tile.getLevel().isResourceAbsent(tileNumber);
    }

    /**
     * Removes the absent-tile mark associated with a tile, if one is associatied.
     *
     * @param tile The tile to unmark.
     * @throws IllegalArgumentException if <code>tile</code> is null
     */
    public final void unmarkResourceAbsent(Tile tile)
    {
        if (tile == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.TileIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        tile.getLevel().unmarkResourceAbsent(this.getTileNumber(tile));
    }
}
