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

import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.cache.MemoryCache;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.terrain.ElevationModelConfiguration;
import gov.nasa.worldwind.util.*;
import org.w3c.dom.Document;

import java.io.IOException;

/**
 * @author dcollins
 * @version $Id: TiledElevationProducer.java 13251 2010-04-02 19:17:46Z dcollins $
 */
public class TiledElevationProducer extends TiledRasterProducer
{
    // Extreme elevations computed during production.
    protected double[] extremes = null;
    // Default production parameter values.
    protected static final String DEFAULT_IMAGE_FORMAT = "application/bil32";
    protected static final double DEFAULT_MISSING_DATA_SIGNAL = (double) Short.MIN_VALUE;
    // Statically reference the readers used to for unknown data sources. This drastically improves the performance of
    // reading large quantities of sources. Since the readers are invoked from a single thread, they can be
    // safely re-used.
    protected static DataRasterReader[] readers = new DataRasterReader[]
        {
            new BILRasterReader(),
            new GeotiffRasterReader()
        };

    public TiledElevationProducer(MemoryCache cache, int writeThreadPoolSize)
    {
        super(cache, writeThreadPoolSize);
    }

    public TiledElevationProducer()
    {
    }

    /**
     * Overridden to initialize this producer's extreme elevations prior to creating and installing elevation tiles.
     *
     * @param parameters the installation parameters.
     *
     * @throws Exception if production fails for any reason.
     */
    @Override
    protected void doStartProduction(AVList parameters) throws Exception
    {
        this.extremes = null;

        super.doStartProduction(parameters);
    }

    public String getDataSourceDescription()
    {
        StringBuilder sb = new StringBuilder();
        sb.append(Logging.getMessage("TiledElevationProducer.Description"));
        sb.append(" (").append(super.getDataSourceDescription()).append(")");
        return sb.toString();
    }

    protected DataRaster createDataRaster(int width, int height, Sector sector, AVList params)
    {
        // Create a BIL elevation raster to hold the tile's data.
        AVList bufferParams = new AVListImpl();

        // ByteBufferRaster is expected buffer type in "DATA_TYPE" key,
        // but WorldFile specifies data type in the "PIXEL_TYPE" key.
        bufferParams.setValue(AVKey.DATA_TYPE, params.getValue(AVKey.PIXEL_TYPE));
        bufferParams.setValue(AVKey.BYTE_ORDER, params.getValue(AVKey.BYTE_ORDER));
        ByteBufferRaster bufferRaster = new ByteBufferRaster(width, height, sector, bufferParams);

        // Clear the raster with the missing data replacment.
        // This code expects the string "gov.nasa.worldwind.avkey.MissingDataValue", which now corresponds to the key 
        // MISSING_DATA_REPLACEMENT.
        Object o = params.getValue(AVKey.MISSING_DATA_REPLACEMENT);
        if (o != null && o instanceof Double)
        {
            Double missingDataValue = (Double) o;
            bufferRaster.fill(missingDataValue);
            bufferRaster.setTransparentValue(missingDataValue);
        }

        return bufferRaster;
    }

    protected DataRasterReader[] getDataRasterReaders()
    {
        return readers;
    }

    protected DataRasterWriter[] getDataRasterWriters()
    {
        return new DataRasterWriter[]
            {
                // Configure the BIL writer to disable writing of georeference files. Georeferencing files are redundant
                // for tiled elevations. The elevation format is defined in the data configuration file, and each
                // tile's georeferencing information is implicit in the tile structure.
                new BILRasterWriter(false)
            };
    }

    protected String validateDataSource(Object source, AVList params)
    {
        // TiledElevationProducer does not accept null data sources.
        if (source == null)
            return Logging.getMessage("nullValue.SourceIsNull");

        // TiledElevationProducer accepts BufferedDataRaster as a data source. If the data source is a DataRaster, then
        // check that it's a BufferedDataRaster.
        if (source instanceof DataRaster)
        {
            DataRaster raster = (DataRaster) source;

            if (!(raster instanceof BufferedDataRaster))
                return Logging.getMessage("TiledRasterProducer.UnrecognizedDataSource", raster);

            String s = this.validateDataSourceParams(raster, String.valueOf(raster));
            if (s != null)
                return s;
        }
        // For any other data source, attempt to find a reader for the data source. If the reader know's the data
        // source's raster type, then check that it's elevation data.
        else
        {
            DataRasterReader reader = ReadableDataRaster.findReaderFor(source, params, this.getDataRasterReaders());
            if (reader == null)
                return Logging.getMessage("TiledRasterProducer.UnrecognizedDataSource", source);

            // Copy the parameter list to insulate changes from the caller.
            params = (params != null) ? params.copy() : new AVListImpl();

            try
            {
                reader.readMetadata(source, params);
            }
            catch (IOException e)
            {
                return Logging.getMessage("TiledRasterProducer.ExceptionWhileReading", source, e.getMessage());
            }

            String s = this.validateDataSourceParams(params, String.valueOf(source));
            if (s != null)
                return s;
        }

        return null;
    }

    protected String validateDataSourceParams(AVList params, String name)
    {
        if (params.hasKey(AVKey.RASTER_TYPE) &&
            params.getValue(AVKey.RASTER_TYPE) != AVKey.RASTER_TYPE_ELEVATION)
        {
            return Logging.getMessage("TiledRasterProducer.UnrecognizedRasterType",
                params.getValue(AVKey.RASTER_TYPE), name);
        }

        if (params.hasKey(AVKey.COORDINATE_SYSTEM) &&
            params.getValue(AVKey.COORDINATE_SYSTEM) != AVKey.COORDINATE_SYSTEM_GEOGRAPHIC)
        {
            return Logging.getMessage("TiledRasterProducer.UnrecognizedCoordinateSystem",
                params.getValue(AVKey.COORDINATE_SYSTEM), name);
        }

        if (params.hasKey(AVKey.ELEVATION_UNIT) &&
            params.getValue(AVKey.ELEVATION_UNIT) != AVKey.ELEVATION_UNIT_METER)
        {
            return Logging.getMessage("TiledElevationProducer.UnrecognizedElevationUnit",
                params.getValue(AVKey.ELEVATION_UNIT), name);
        }

        if (params.getValue(AVKey.SECTOR) == null)
            return Logging.getMessage("TiledRasterProducer.NoSector", name);

        return null;
    }

    protected void initProductionParameters(AVList params)
    {
        // Preserve backward compatibility with previous verisons of TiledElevationProducer. If the caller specified a
        // format suffix parameter, use it to compute the image format properties. This gives priority to the format
        // suffix property to ensure applications which use format suffix continue to work.
        if (params.getValue(AVKey.FORMAT_SUFFIX) != null)
        {
            String s = WWIO.makeMimeTypeForSuffix(params.getValue(AVKey.FORMAT_SUFFIX).toString());
            if (s != null)
            {
                params.setValue(AVKey.IMAGE_FORMAT, s);
                params.setValue(AVKey.AVAILABLE_IMAGE_FORMATS, new String[] {s});
            }
        }

        // Use the default image format if none exists.
        if (params.getValue(AVKey.IMAGE_FORMAT) == null)
            params.setValue(AVKey.IMAGE_FORMAT, DEFAULT_IMAGE_FORMAT);

        // Compute the available image formats if none exists.
        if (params.getValue(AVKey.AVAILABLE_IMAGE_FORMATS) == null)
        {
            params.setValue(AVKey.AVAILABLE_IMAGE_FORMATS,
                new String[] {params.getValue(AVKey.IMAGE_FORMAT).toString()});
        }

        // Compute the format suffix if none exists.        
        if (params.getValue(AVKey.FORMAT_SUFFIX) == null)
        {
            params.setValue(AVKey.FORMAT_SUFFIX,
                WWIO.makeSuffixForMimeType(params.getValue(AVKey.IMAGE_FORMAT).toString()));
        }

        // Compute the pixel type from the image format.
        if (params.getValue(AVKey.PIXEL_TYPE) == null && params.getValue(AVKey.IMAGE_FORMAT) != null)
        {
            String s = WWIO.makePixelTypeForMimeType(params.getValue(AVKey.IMAGE_FORMAT).toString());
            if (s != null)
                params.setValue(AVKey.PIXEL_TYPE, s);
        }

        // Use the default pixel type if none exists.
        if (params.getValue(AVKey.PIXEL_TYPE) == null)
            params.setValue(AVKey.PIXEL_TYPE, AVKey.INT16);

        // Use the default byte order if none exists.
        if (params.getValue(AVKey.BYTE_ORDER) == null)
            params.setValue(AVKey.BYTE_ORDER, AVKey.LITTLE_ENDIAN);

        // This code expects the string "gov.nasa.worldwind.avkey.MissingDataValue", which now corresponds to the key
        // MISSING_DATA_REPLACEMENT.
        if (params.getValue(AVKey.MISSING_DATA_REPLACEMENT) == null)
            params.setValue(AVKey.MISSING_DATA_REPLACEMENT, DEFAULT_MISSING_DATA_SIGNAL);
    }

    protected LatLon computeRasterTileDelta(int tileWidth, int tileHeight, Iterable<? extends DataRaster> rasters)
    {
        LatLon pixelSize = this.computeSmallestPixelSize(rasters);
        // Compute the tile size in latitude and longitude, given a raster's sector and dimension, and the tile
        // dimensions. In this computation a pixel is assumed to have no dimension. We measure the distance between
        // pixels rather than some pixel dimension.
        double latDelta = (tileHeight - 1) * pixelSize.getLatitude().degrees;
        double lonDelta = (tileWidth - 1) * pixelSize.getLongitude().degrees;
        return LatLon.fromDegrees(latDelta, lonDelta);
    }

    /**
     * Overridden to compute the extreme elevations prior to installing a tile in the filesystem.
     *
     * @param tile       the tile to install to the filesystem.
     * @param tileRaster the raster containing the raster's data content.
     * @param params     the installation parameters.
     */
    @Override
    protected void installTileRasterLater(LevelSet levelSet, Tile tile, DataRaster tileRaster, AVList params)
    {
        // If this tile is from the lowest level, use the tile's raster to compute the extreme elevations. Since each
        // level above the lowest is computed as a function of the lowest level, we can ignore all but the lowest level
        // tiles when computing the extreme elevations.
        if (levelSet.isFinalLevel(tile.getLevelNumber()))
        {
            this.updateExtremeElevations(tileRaster);
        }

        super.installTileRasterLater(levelSet, tile, tileRaster, params);
    }

    protected void updateExtremeElevations(DataRaster raster)
    {
        if (!(raster instanceof BufferedDataRaster))
        {
            String message = Logging.getMessage("DataRaster.IncompatibleRaster", raster);
            Logging.logger().severe(message);
            return;
        }

        // Compute the raster's extreme elevations. If the returned array is null, the tile is either empty or contains
        // only missing data values. In either case, this tile does not contribute to the overall extreme elevations.
        double[] tileExtremes = ((BufferedDataRaster) raster).getExtremes();
        if (tileExtremes == null || tileExtremes.length < 2)
            return;

        if (this.extremes == null)
            this.extremes = WWUtil.defaultMinMix();

        if (this.extremes[0] > tileExtremes[0])
            this.extremes[0] = tileExtremes[0];
        if (this.extremes[1] < tileExtremes[1])
            this.extremes[1] = tileExtremes[1];
    }

    /**
     * Returns an ElevationModel configuration document which describes the tiled elevation data produced by this
     * TiledElevationProducer. The document's contents are based on the configuration document for a basic
     * ElevationModel, except this document describes an offline dataset. This returns null if the parameter list is
     * null, or if the configuration document cannot be created for any reason.
     *
     * @param params the parameters which describe an ElevationModel configuration document's contents.
     *
     * @return the configuration document, or null if the parameter list is null or does not contain the required
     *         parameters.
     */
    protected Document createConfigDoc(AVList params)
    {
        AVList configParams = params.copy();

        // Determine a default display name if none exists.
        if (configParams.getValue(AVKey.DISPLAY_NAME) == null)
            configParams.setValue(AVKey.DISPLAY_NAME, params.getValue(AVKey.DATASET_NAME));

        // Set the SERVICE_NAME and NETWORK_RETRIEVAL_ENABLED parameters to indicate this dataset is offline.
        configParams.setValue(AVKey.SERVICE_NAME, "Offline");
        configParams.setValue(AVKey.NETWORK_RETRIEVAL_ENABLED, Boolean.FALSE);

        // TiledElevationProducer and the DataRaster classes use MISSING_DATA_REPLACEMENT to denote the missing data
        // value, whereas all other WWWJ code uses MISSING_DATA_SIGNAL. Replace the MISSING_DATA_REPLACEMENT with the
        // MISSING_DATA_SIGNAL here to ensure this discrapancy is limited to data installation code.
        configParams.removeKey(AVKey.MISSING_DATA_REPLACEMENT);
        configParams.setValue(AVKey.MISSING_DATA_SIGNAL, params.getValue(AVKey.MISSING_DATA_REPLACEMENT));

        // If we able to successfully compute extreme values for this elevation data, then set the extreme elevation
        // values ELEVATION_MIN and ELEVATION_MAX. If the extremes array is null or has length less than 2, the imported
        // elevations are either empty or contain only missing data values. In either case we cannot determine the
        // extreme values.
        if (this.extremes != null && this.extremes.length >= 2)
        {
            configParams.setValue(AVKey.ELEVATION_MIN, this.extremes[0]);
            configParams.setValue(AVKey.ELEVATION_MAX, this.extremes[1]);
        }

        // Return a configuration file for a BasicElevationModel. BasicElevationModel is the standard WWJ component
        // which consumes tiled elevation data.
        return ElevationModelConfiguration.createBasicElevationModelDocument(configParams);
    }
}
