/*
 *    Geotools2 - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, GeoTools Project Managment Committee (PMC)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 *
 */
package org.geotools.gce.gtopo30;

import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.DataBuffer;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.ParameterBlock;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.media.jai.Histogram;
import javax.media.jai.ImageLayout;
import javax.media.jai.InterpolationBilinear;
import javax.media.jai.JAI;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;

import org.geotools.coverage.Category;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.grid.GeneralGridRange;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridCoverageWriter;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.imageio.GeoToolsWriteParams;
import org.geotools.coverage.processing.operation.Resample;
import org.geotools.coverage.processing.operation.SelectSampleDimension;
import org.geotools.data.DataSourceException;
import org.geotools.factory.Hints;
import org.geotools.parameter.Parameter;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.resources.CRSUtilities;
import org.geotools.resources.image.CoverageUtilities;
import org.geotools.util.NumberRange;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridCoverageWriter;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;

import com.sun.media.jai.operator.ImageWriteDescriptor;

/**
 * Class used for encoding a {@link GridCoverage2D} into a GTOPO30 file.
 * 
 * 
 * @author Simone Giannecchini
 * @author jeichar
 * @author mkraemer
 * @source $URL:
 *         http://svn.geotools.org/geotools/trunk/gt/plugin/gtopo30/src/org/geotools/gce/gtopo30/GTopo30Writer.java $
 */
final public class GTopo30Writer extends AbstractGridCoverageWriter implements
		GridCoverageWriter {

	/** Logger. */
	private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.gce.gtopo30");

	static {
		// register new JAI operation
		NoDataReplacerOpImage.register(JAI.getDefaultInstance());
	}

	/** Cached factory for a {@link SelectSampleDimension} operation. */
	private final static SelectSampleDimension sdFactory = new SelectSampleDimension();





	/** Cached factory for {@link Resample} operation. */
	private final static Resample resampleFactory = new Resample();

	/**
	 * Standard width for the GIF image.
	 */
	private final static int GIF_WIDTH = 640;

	/** Standard height for the GIF image. */
	private final static int GIF_HEIGHT = 480;

	/**
	 * Creates a {@link GTopo30Writer}.
	 * 
	 * @param dest
	 *            The destination object can be a {@link File} (a directory
	 *            actually), an {@link URL} to a directory, or a String
	 *            representing a directory or an {@link URL} to a directory.
	 * @throws DataSourceException
	 */
	public GTopo30Writer(final Object dest) throws DataSourceException {
		this(dest, null);
	}

	/**
	 * Creates a {@link GTopo30Writer}.
	 * 
	 * @param dest
	 *            The destination object can be a File (a directory actually),
	 *            an URL to a directory, or a String representing a directory or
	 *            an URL to a directory.
	 * @throws DataSourceException
	 */
	public GTopo30Writer(final Object dest, final Hints hints)
			throws DataSourceException {

		// /////////////////////////////////////////////////////////////////////
		//
		// Initial checks
		//
		// /////////////////////////////////////////////////////////////////////
		if (dest == null) {
			throw new NullPointerException("The provided destination is null.");
		}

		destination = dest;
		final File temp;
		final URL url;

		// we only accept a directory as a path
		if (dest instanceof String) {
			temp = new File((String) dest);

			// if it exists and it is not a directory that 's not good
			if ((temp.exists() && !temp.isDirectory()) || !temp.exists()) {
				destination = null; // we cannot write
			} else if (!temp.exists()) {
				// well let's create it!
				if (!temp.mkdir()) {
					destination = null;
				} else {
					destination = temp.getAbsolutePath();
				}
			}
		} else if (dest instanceof File) {
			temp = (File) dest;

			if (temp.exists() && !temp.isDirectory()) {
				this.destination = null;
			} else if (!temp.exists()) {
				// let's create it
				if (temp.mkdir()) {
					destination = temp.getAbsolutePath();
				} else {
					destination = null;
				}
			}
		} else if (dest instanceof URL) {
			url = (URL) dest;

			if (url.getProtocol().compareToIgnoreCase("file") != 0) {
				destination = null;
			}

			try {
				temp = new File(URLDecoder.decode(url.getFile(), "UTF-8)"));
			} catch (UnsupportedEncodingException e) {
				LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
				final DataSourceException ex = new DataSourceException(e);
				throw ex;
			}

			if (temp.exists() && !temp.isDirectory()) {
				destination = null;
			} else if (!temp.exists()) {
				// let's create it
				if (temp.mkdir()) {
					destination = temp.getAbsolutePath();
				} else {
					destination = null;
				}
			}
		} else if (dest instanceof ImageOutputStream) {
			this.destination = dest;
		} else {
			throw new IllegalArgumentException(
					"The provided destination is of an incorrect type.");

		}

		// /////////////////////////////////////////////////////////////////////
		//
		// managing hints
		//
		// /////////////////////////////////////////////////////////////////////
		if (super.hints == null)
		{
			this.hints = new Hints(Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE);
		}
		if (hints != null) {

			this.hints.add(hints);
		}
	}

	/**
	 * @see org.opengis.coverage.grid.GridCoverageWriter#getFormat()
	 */
	public Format getFormat() {
		return new GTopo30Format();
	}

	/**
	 * @see org.opengis.coverage.grid.GridCoverageWriter#write(org.opengis.coverage.grid.GridCoverage,
	 *      org.opengis.parameter.GeneralParameterValue[])
	 */
	public void write(final GridCoverage coverage,
			final GeneralParameterValue[] params)
			throws java.lang.IllegalArgumentException, java.io.IOException {

		// /////////////////////////////////////////////////////////////////////
		//
		// Checking input params
		//
		// /////////////////////////////////////////////////////////////////////
		if (coverage == null)
			throw new NullPointerException(
					"The provided source coverage is null");
		// the source GridCoverage2D
		GridCoverage2D gc2D = (GridCoverage2D) coverage;

		// /////////////////////////////////////////////////////////////////////
		//
		// Checking writing params
		//
		// /////////////////////////////////////////////////////////////////////
		GeoToolsWriteParams gtParams = null;
		if (params != null) {

			if (params != null) {
				Parameter param;
				final int length = params.length;
				for (int i = 0; i < length; i++) {
					param = (Parameter) params[i];
					if (param.getDescriptor().getName().getCode().equals(
							AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName()
									.toString())) {
						gtParams = (GeoToolsWriteParams) param.getValue();
					}
				}
			}
		}
		if (gtParams == null)
			gtParams = new GTopo30WriteParams();
		// compression
		final boolean compressed = gtParams.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT;
		// write band
		int[] writeBands = gtParams.getSourceBands();
		int writeBand = CoverageUtilities.getVisibleBand(gc2D
				.getRenderedImage());
		if ((writeBands == null || writeBands.length == 0 || writeBands.length > 1)
				&& (writeBand < 0 || writeBand > gc2D.getNumSampleDimensions()))
			throw new IllegalArgumentException(
					"You need to supply a valid index for deciding which band to write.");
		if (!((writeBands == null || writeBands.length == 0 || writeBands.length > 1)))
			writeBand = writeBands[0];

		// destination file name
		String fileName = gc2D.getName().toString();

		// destination XXX HAS to be a dir
		if (compressed) {
			destination = new ZipOutputStream(new BufferedOutputStream(
					new FileOutputStream(new File((File) destination,
							new StringBuffer(fileName).append(".zip")
									.toString()))));
		}

		// /////////////////////////////////////////////////////////////////////
		//
		// STEP 1
		//
		// We might need to do a band select in order to cope with the GTOPO30
		// limitation.
		//
		// /////////////////////////////////////////////////////////////////////
		final ParameterValueGroup pvg = sdFactory.getParameters();
		pvg.parameter("Source").setValue(gc2D);
		pvg.parameter("SampleDimensions").setValue(new int[]{writeBand});
		pvg.parameter("VisibleSampleDimension").setValue(writeBand);
		gc2D = (GridCoverage2D) sdFactory.doOperation(pvg, hints);

		// /////////////////////////////////////////////////////////////////////
		//
		// STEP 2
		//
		// We might need to reformat the selected band for this coverage in
		// order to cope with the GTOPO30 limitation.
		//
		// /////////////////////////////////////////////////////////////////////
		final PlanarImage reFormattedData2Short = reFormatCoverageImage(gc2D,
				DataBuffer.TYPE_SHORT);

		// /////////////////////////////////////////////////////////////////////
		//
		// STEP 2
		//
		// Start with writing things out.
		//
		// /////////////////////////////////////////////////////////////////////
		// //
		//
		// write DEM
		//
		// //
		this.writeDEM(reFormattedData2Short, fileName, destination);

		// //
		//
		// write statistics
		//
		// //
		this.writeStats(reFormattedData2Short, fileName, destination, gc2D);
		// we won't use this image anymore let's release the resources.

		// //
		//
		// write world file
		//
		// //
		this.writeWorldFile(gc2D, fileName, destination);

		// //
		//
		// write projection
		//
		// //
		this.writePRJ(gc2D, fileName, destination);

		// //
		//
		// write HDR
		//
		// //
		this.writeHDR(gc2D, fileName, destination);

		// //
		//
		// write gif
		//
		// //
		this.writeGIF(gc2D, fileName, destination);

		// //
		//
		// write src
		//
		// //
		this.writeSRC(gc2D, fileName, destination);

		if (compressed) {

			((ZipOutputStream) destination).close();
		}
	}

	/**
	 * Reformats the input single banded planar image to having
	 * {@link DataBuffer#TYPE_SHORT} sample type as requested by the GTOPO30
	 * format.
	 * 
	 * @param gc2D
	 *            source {@link GridCoverage2D} from which to take the input
	 *            {@link PlanarImage}.
	 * 
	 * @return the reformated {@link PlanarImage}.
	 */
	private PlanarImage reFormatCoverageImage(final GridCoverage2D gc2D,
			int writeBand) {
		// internal image
		PlanarImage image = (PlanarImage) gc2D.getRenderedImage();
		// sample dimension type
		final int origDataType = image.getSampleModel().getDataType();
		// short?
		if (DataBuffer.TYPE_SHORT == origDataType) {
			return image;
		}

		final GridSampleDimension visibleSD = ((GridSampleDimension) gc2D
				.getSampleDimension(0)).geophysics(true);

		// getting categories
		final List oldCategories = visibleSD.getCategories();

		// removing old nodata category
		// candidate
		Category candidate = null;
		NumberRange candidateRange = null;
		final Iterator it = oldCategories.iterator();

		while (it.hasNext()) {
			candidate = (Category) it.next();

			// removing candidate for NaN
			if (candidate.getName().toString().equalsIgnoreCase("no data")) {
				candidateRange = candidate.getRange();

				break;
			}
		}

		// new no data category
		final double oldNoData = candidateRange.getMinimum();
		final ParameterBlockJAI pbjM = new ParameterBlockJAI(
				"org.geotools.gce.gtopo30.NoDataReplacer");
		pbjM.addSource(image);
		pbjM.setParameter("oldNoData", oldNoData);
		image = JAI.create("org.geotools.gce.gtopo30.NoDataReplacer", pbjM,
				hints);
		return image;
	}

	/**
	 * Writing down the header file for the gtopo30 format:
	 * 
	 * @param coverage
	 *            The GridCoverage to write
	 * @param file
	 *            The destination object (can be a File or ZipOutputStream)
	 * 
	 * @throws IOException
	 *             If the file could not be written
	 */
	private void writeHDR(final GridCoverage2D gc, final String name,
			final Object dest) throws IOException {

		// final GeneralEnvelope envelope = (GeneralEnvelope) gc.getEnvelope();
		final double noData = -9999.0;

		try {
			// checking the directions of the axes.
			// we need to understand how the axes of this gridcoverage are
			// specified
			final CoordinateReferenceSystem crs = CRSUtilities.getCRS2D(gc
					.getCoordinateReferenceSystem());
/*
 * Note from Martin: I suggest to replace all the above lines by the commented code below.
 * The change need to be tested in order to make sure that I didn't made a mistake in the
 * mathematic. Note that 'lonFirst' totally vanish.
 */
			final AffineTransform gridToWorld = (AffineTransform) gc
					.getGridGeometry().getGridToCoordinateSystem();
			boolean lonFirst = (XAffineTransform.getSwapXY(gridToWorld) != -1);

			final double geospatialDx = Math.abs((lonFirst) ? gridToWorld
					.getScaleX() : gridToWorld.getShearY());
			final double geospatialDy = Math.abs((lonFirst) ? gridToWorld
					.getScaleY() : gridToWorld.getShearX());

			// getting corner coordinates of the left upper corner
			final double xUpperLeft = lonFirst ? gridToWorld.getTranslateX()
					: gridToWorld.getTranslateY();
			final double yUpperLeft = lonFirst ? gridToWorld.getTranslateY()
					: gridToWorld.getTranslateX();
/*
			final AffineTransform worldToGrid = (AffineTransform) gc
					.getGridGeometry().getGridToCoordinateSystem().inverse();

			final double geospatialDx = 1 / XAffineTransform.getScaleX0(worldToGrid);
			final double geospatialDy = 1 / XAffineTransform.getScaleY0(worldToGrid);

			// getting corner coordinates of the left upper corner
			final double xUpperLeft = -worldToGrid.getTranslateX() * geospatialDx;
			final double yUpperLeft = -worldToGrid.getTranslateY() * geospatialDy;
*/

			// calculating the physical resolution over x and y.
			final int geometryWidth = gc.getGridGeometry().getGridRange()
					.getLength(0);
			final int geometryHeight = gc.getGridGeometry().getGridRange()
					.getLength(1);

			if (dest instanceof File) {

				final PrintWriter out = new PrintWriter(
						new BufferedOutputStream(new FileOutputStream(new File(
								(File) dest, new StringBuffer(name).append(
										".HDR").toString()))));

				// output header and assign header fields
				out.print("BYTEORDER");
				out.print(" ");
				out.println("M");

				out.print("LAYOUT");
				out.print(" ");
				out.println("BIL");

				out.print("NROWS");
				out.print(" ");
				out.println(geometryHeight);

				out.print("NCOLS");
				out.print(" ");
				out.println(geometryWidth);

				out.print("NBANDS");
				out.print(" ");
				out.println("1");

				out.print("NBITS");
				out.print(" ");
				out.println("16");

				out.print("BANDROWBYTES");
				out.print(" ");
				out.println(geometryWidth * 2);

				out.print("TOTALROWBYTES");
				out.print(" ");
				out.println(geometryWidth * 2);

				out.print("BANDGAPBYTES");
				out.print(" ");
				out.println(0);

				out.print("NODATA");
				out.print(" ");
				out.println((int) noData);

				out.print("ULXMAP");
				out.print(" ");
				out.println(xUpperLeft);

				out.print("ULYMAP");
				out.print(" ");
				out.println(yUpperLeft);

				out.print("XDIM");
				out.print(" ");
				out.println(geospatialDx);

				out.print("YDIM");
				out.print(" ");
				out.println(geospatialDy);
				out.flush();
				out.close();
			} else {
				final ZipOutputStream outZ = (ZipOutputStream) dest;
				final ZipEntry e = new ZipEntry(gc.getName().toString()
						+ ".HDR");
				outZ.putNextEntry(e);

				// writing world file
				outZ.write("BYTEORDER".getBytes());
				outZ.write(" ".getBytes());
				outZ.write("M".getBytes());
				outZ.write("\n".getBytes());

				outZ.write("LAYoutZ".getBytes());
				outZ.write(" ".getBytes());
				outZ.write("BIL".getBytes());
				outZ.write("\n".getBytes());

				outZ.write("NROWS".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Integer.toString(geometryHeight).getBytes());
				outZ.write("\n".getBytes());

				outZ.write("NCOLS".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Integer.toString(geometryWidth).getBytes());
				outZ.write("\n".getBytes());

				outZ.write("NBANDS".getBytes());
				outZ.write(" ".getBytes());
				outZ.write("1".getBytes());
				outZ.write("\n".getBytes());

				outZ.write("NBITS".getBytes());
				outZ.write(" ".getBytes());
				outZ.write("16".getBytes());
				outZ.write("\n".getBytes());

				outZ.write("BANDROWBYTES".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Integer.toString(geometryWidth * 2).getBytes());
				outZ.write("\n".getBytes());

				outZ.write("TOTALROWBYTES".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Integer.toString(geometryWidth * 2).getBytes());
				outZ.write("\n".getBytes());

				outZ.write("BANDGAPBYTES".getBytes());
				outZ.write(" ".getBytes());
				outZ.write("0".getBytes());
				outZ.write("\n".getBytes());

				outZ.write("NODATA".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Integer.toString((int) noData).getBytes());
				outZ.write("\n".getBytes());

				outZ.write("ULXMAP".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Double.toString(xUpperLeft + (geospatialDx / 2))
						.getBytes());
				outZ.write("\n".getBytes());

				outZ.write("ULYMAP".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Double.toString(yUpperLeft - (geospatialDy / 2))
						.getBytes());
				outZ.write("\n".getBytes());

				outZ.write("XDIM".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Double.toString(geospatialDx).getBytes());
				outZ.write("\n".getBytes());

				outZ.write("YDIM".getBytes());
				outZ.write(" ".getBytes());
				outZ.write(Double.toString(geospatialDy).toString().getBytes());
				outZ.write("\n".getBytes());

				outZ.closeEntry();

				((ZipOutputStream) dest).closeEntry();
			}
		} catch (TransformException e) {
			final IOException ioe = new IOException(
					"Unable to write world file");
			ioe.initCause(e);
			throw ioe;
		}
	}

	/**
	 * Writes the source file (.SRC). The default byte order is BIG_ENDIAN.
	 * 
	 * @param gc
	 *            The GridCoverage to write
	 * @param file
	 *            The destination object (can be a File or ZipOutputStream)
	 * @param dest
	 * 
	 * @throws FileNotFoundException
	 *             If the destination file could not be found
	 * @throws IOException
	 *             If the file could not be written
	 */
	private void writeSRC(GridCoverage2D gc, final String name, Object dest)
			throws FileNotFoundException, IOException {

		// /////////////////////////////////////////////////////////////////////
		// TODO @task @todo
		// Here I am making the assumption that the non geophysiscs view is 8
		// bit but it can also be 16. I should do something more general like a
		// clamp plus a format but for the moment this is enough.
		//
		// We need also to get the one visible band
		//
		// /////////////////////////////////////////////////////////////////////
		gc = gc.geophysics(false);
		ImageOutputStream out = null;

		if (dest instanceof File) {
			out = ImageIO.createImageOutputStream(new File((File) dest,
					new StringBuffer(name).append(".SRC").toString()));
		} else {
			final ZipOutputStream outZ = (ZipOutputStream) dest;
			final ZipEntry e = new ZipEntry(gc.getName().toString() + ".SRC");
			outZ.putNextEntry(e);

			out = ImageIO.createImageOutputStream(outZ);
		}

		// setting byte order
		out.setByteOrder(java.nio.ByteOrder.BIG_ENDIAN);

		// /////////////////////////////////////////////////////////////////////
		//
		// Prepare to write
		//
		// /////////////////////////////////////////////////////////////////////
		RenderedImage image = gc.getRenderedImage();
		image = untileImage(image);

		final ParameterBlockJAI pbj = new ParameterBlockJAI("imagewrite");
		pbj.addSource(image);
		pbj.setParameter("Format", "raw");
		pbj.setParameter("Output", out);
		final RenderedOp wOp = JAI.create("ImageWrite", pbj);

		// /////////////////////////////////////////////////////////////////////
		//
		// Dispose things
		//
		// /////////////////////////////////////////////////////////////////////
		final Object o = wOp
				.getProperty(ImageWriteDescriptor.PROPERTY_NAME_IMAGE_WRITER);
		if (o instanceof ImageWriter)
			((ImageWriter) o).dispose();

		if (!(dest instanceof File)) {
			((ZipOutputStream) dest).closeEntry();
		}
		out.flush();
		out.close();

	}

	/**
	 * Writing a gif file as an overview for this GTopo30.
	 * 
	 * @param gc
	 *            The GridCoverage to write
	 * @param file
	 *            The destination object (can be a File or ZipOutputStream)
	 * @param dest
	 * 
	 * @throws IOException
	 *             If the file could not be written
	 */
	private void writeGIF(final GridCoverage2D gc, final String name,
			Object dest) throws IOException {
		ImageOutputStream out = null;

		if (dest instanceof File) {
			// writing gif image
			out = ImageIO.createImageOutputStream(new File((File) dest,
					new StringBuffer(name).append(".GIF").toString()));
		} else {
			final ZipOutputStream outZ = (ZipOutputStream) dest;
			final ZipEntry e = new ZipEntry(gc.getName().toString() + ".GIF");
			outZ.putNextEntry(e);

			out = ImageIO.createImageOutputStream(outZ);
		}

		// rescaling to a smaller resolution in order to save space on storage
		final GridCoverage2D gc1 = rescaleCoverage(gc);

		// get the non-geophysiscs view
		final GridCoverage2D gc2 = gc1.geophysics(false);

		// get the underlying image
		final RenderedImage image = gc2.getRenderedImage();

		// write it down as a gif
		final ParameterBlockJAI pbj = new ParameterBlockJAI("ImageWrite");
		pbj.addSource(image);
		pbj.setParameter("Output", out);
		pbj.setParameter("Format", "gif");
		JAI.create("ImageWrite", pbj, new RenderingHints(JAI.KEY_TILE_CACHE,
				null));

		if (dest instanceof File) {
			out.close();
		} else {
			out.flush();
			((ZipOutputStream) dest).closeEntry();
		}

		// disposing the old unused coverages
		gc1.dispose();
	}

	/**
	 * Purpose of this method is rescaling the original coverage in order to
	 * create an overview for the shaded relief gif image to be put inside the
	 * gtopo30 set of files.
	 * 
	 * @param gc
	 *            the original {@link GridCoverage2D}.
	 * 
	 * @return the rescaled {@link GridCoverage2D}.
	 */
	private GridCoverage2D rescaleCoverage(GridCoverage2D gc) {
		final RenderedImage image = gc.getRenderedImage();
		final int width = image.getWidth();
		final int height = image.getHeight();

		if ((height < GIF_HEIGHT) && (width < GIF_WIDTH)) {
			return gc;
		}

		// new grid range
		final GeneralGridRange newGridrange = new GeneralGridRange(new int[] {
				0, 0 }, new int[] { GIF_WIDTH, GIF_HEIGHT });

		final GridGeometry2D newGridGeometry = new GridGeometry2D(newGridrange,
				gc.getEnvelope());

		// resample this coverage
		final ParameterValueGroup pvg= resampleFactory.getParameters();
		pvg.parameter("Source").setValue(gc);
		pvg.parameter("GridGeometry").setValue(newGridGeometry);
		pvg.parameter("InterpolationType").setValue(new InterpolationBilinear());
		return (GridCoverage2D) resampleFactory.doOperation(pvg, hints);

	}

	/**
	 * Write a projection file (.PRJ) using wkt
	 * 
	 * @param gc
	 *            The GridCoverage to write
	 * @param file
	 *            The destination object (can be a File or ZipOutputStream)
	 * @param dest
	 * 
	 * @throws IOException
	 *             If the file could not be written
	 */
	private void writePRJ(final GridCoverage2D gc, String name, Object dest)
			throws IOException {
		if (dest instanceof File) {
			// create the file
			final BufferedWriter fileWriter = new BufferedWriter(
					new FileWriter(new File((File) dest, new StringBuffer(name)
							.append(".PRJ").toString())));

			// write information on crs
			fileWriter.write(gc.getCoordinateReferenceSystem().toWKT());
			fileWriter.close();
		} else {
			final ZipOutputStream out = (ZipOutputStream) dest;
			final ZipEntry e = new ZipEntry(new StringBuffer(gc.getName()
					.toString()).append(".PRJ").toString());
			out.putNextEntry(e);
			out.write(gc.getCoordinateReferenceSystem().toWKT().getBytes());
			out.closeEntry();
		}
	}

	/**
	 * Writes the stats file (.STX).
	 * 
	 * @param image
	 *            The GridCoverage to write
	 * @param file
	 *            The destination object (can be a File or ZipOutputStream)
	 * @param gc
	 *            DOCUMENT ME!
	 * 
	 * @throws IOException
	 *             If the file could not be written
	 */
	private void writeStats(final PlanarImage image, String name, Object dest,
			final GridCoverage2D gc) throws IOException {
		ParameterBlock pb = new ParameterBlock();
		// /////////////////////////////////////////////////////////////////////
		//
		// we need to evaluate stats first using jai
		//
		// /////////////////////////////////////////////////////////////////////
		final int visibleBand = CoverageUtilities.getVisibleBand(image);
		final GridSampleDimension sd = (GridSampleDimension) gc
				.getSampleDimension(visibleBand);
		final double[] Max = new double[] { sd.getMaximumValue() };
		final double[] Min = new double[] { -9999.0 };

		// histogram
		pb.addSource(image);
		pb.add(null); // no roi
		pb.add(1);
		pb.add(1);
		pb.add(new int[] { (int) (Max[0] - Min[0] + 1) });
		pb.add(Min);
		pb.add(Max);
		pb.add(1);

		// /////////////////////////////////////////////////////////////////////
		//
		// Create the histogram
		//
		// /////////////////////////////////////////////////////////////////////
		final PlanarImage histogramImage = JAI.create("histogram", pb,
				new RenderingHints(JAI.KEY_TILE_CACHE, null));
		final Histogram hist = (Histogram) histogramImage
				.getProperty("histogram");
		pb.removeParameters();
		pb.removeSources();

		// /////////////////////////////////////////////////////////////////////
		//
		// Write things
		//
		// /////////////////////////////////////////////////////////////////////
		if (dest instanceof File) {
			// files destinations
			// write statistics
			if (dest instanceof File) {
				dest = new File((File) dest, new StringBuffer(name).append(
						".STX").toString());
			}
			// writing world file
			final PrintWriter p = new PrintWriter(new FileOutputStream(
					((File) dest)));
			p.print(1);
			p.print(" ");
			p.print((int) Min[0]);
			p.print(" ");
			p.print((int) Max[0]);
			p.print(" ");
			p.print(hist.getMean()[0]);
			p.print(" ");
			p.print(hist.getStandardDeviation()[0]);
			p.close();
		} else {
			final ZipOutputStream outZ = (ZipOutputStream) dest;
			final ZipEntry e = new ZipEntry(name + ".STX");
			outZ.putNextEntry(e);

			// writing world file
			outZ.write("1".getBytes());
			outZ.write(" ".getBytes());
			outZ.write(new Integer((int) Min[0]).toString().getBytes());
			outZ.write(" ".getBytes());
			outZ.write(new Integer((int) Max[0]).toString().getBytes());
			outZ.write(" ".getBytes());
			outZ.write(new Double(hist.getMean()[0]).toString().getBytes());
			outZ.write(" ".getBytes());
			outZ.write(new Double(hist.getStandardDeviation()[0]).toString()
					.getBytes());
			((ZipOutputStream) dest).closeEntry();
		}

		histogramImage.dispose();
	}

	/**
	 * Writes the world file (.DMW)
	 * 
	 * @param gc
	 *            The GridCoverage to write
	 * @param worldFile
	 *            The destination world file (can be a file or a
	 *            ZipOutputStream)
	 * 
	 * @throws IOException
	 *             if the file could not be written
	 */
	private void writeWorldFile(final GridCoverage2D gc, String name,
			Object dest) throws IOException {
		// final RenderedImage image = (PlanarImage) gc.getRenderedImage();

		/**
		 * It is worth to point out that here I build the values using the axes
		 * order specified in the CRs which can be either LAT,LON or LON,LAT.
		 * This is important since we ned to know how to assocaite the
		 * underlying raster dimensions with the envelope which is in CRS
		 * values.
		 */

		try {
			// /////////////////////////////////////////////////////////////////////
			//
			// trying to understand the direction of the first axis in order to
			// understand how to associate the value to the crs.
			//
			// /////////////////////////////////////////////////////////////////////
			final CoordinateReferenceSystem crs = CRSUtilities.getCRS2D(gc
					.getCoordinateReferenceSystem());
			final AffineTransform gridToWorld = (AffineTransform) gc
					.getGridGeometry().getGridToCoordinateSystem();
			boolean lonFirst = (XAffineTransform.getSwapXY(gridToWorld) != -1);

			// /////////////////////////////////////////////////////////////////////
			//
			// associate value to crs
			//
			// /////////////////////////////////////////////////////////////////////
			final double xPixelSize = (lonFirst) ? gridToWorld.getScaleX()
					: gridToWorld.getShearY();
			final double rotation1 = (lonFirst) ? gridToWorld.getShearX()
					: gridToWorld.getScaleY();
			final double rotation2 = (lonFirst) ? gridToWorld.getShearY()
					: gridToWorld.getScaleX();
			final double yPixelSize = (lonFirst) ? gridToWorld.getScaleY()
					: gridToWorld.getShearX();
			final double xLoc = lonFirst ? gridToWorld.getTranslateX()
					: gridToWorld.getTranslateY();
			final double yLoc = lonFirst ? gridToWorld.getTranslateY()
					: gridToWorld.getTranslateX();
			if (dest instanceof File) {

				dest = new File((File) dest, new StringBuffer(name).append(
						".DMW").toString());
				// writing world file
				final PrintWriter out = new PrintWriter(new FileOutputStream(
						(File) dest));
				out.println(xPixelSize);
				out.println(rotation1);
				out.println(rotation2);
				out.println(yPixelSize);
				out.println(xLoc);
				out.println(yLoc);
				out.close();
			} else {
				final ZipOutputStream outZ = (ZipOutputStream) dest;
				final ZipEntry e = new ZipEntry(gc.getName().toString()
						+ ".DMW");
				outZ.putNextEntry(e);

				// writing world file
				outZ.write(Double.toString(xPixelSize).getBytes());
				outZ.write("\n".getBytes());
				outZ.write(Double.toString(rotation1).toString().getBytes());
				outZ.write("\n".getBytes());
				outZ.write(Double.toString(rotation2).toString().getBytes());
				outZ.write("\n".getBytes());
				outZ.write(Double.toString(xPixelSize).toString().getBytes());
				outZ.write("\n".getBytes());
				outZ.write(Double.toString(yPixelSize).toString().getBytes());
				outZ.write("\n".getBytes());
				outZ.write(Double.toString(xLoc).toString().getBytes());
				outZ.write("\n".getBytes());
				outZ.write(Double.toString(yLoc).toString().getBytes());
				outZ.write("\n".getBytes());
				((ZipOutputStream) dest).closeEntry();
			}
		} catch (TransformException e) {
			final IOException ioe = new IOException(
					"Unable to write world file");
			ioe.initCause(e);
			throw ioe;
		}
	}

	/**
	 * Writes the digital elevation model file (.DEM). The default byte order is
	 * BIG_ENDIAN.
	 * 
	 * @param image
	 *            The GridCoverage object to write
	 * @param name
	 *            DOCUMENT ME!
	 * @param dest
	 *            The destination object (can be a File or a ZipOutputStream)
	 * 
	 * @throws FileNotFoundException
	 *             If the destination file could not be found
	 * @throws IOException
	 *             If the file could not be written
	 */
	private void writeDEM(PlanarImage image, final String name, Object dest)
			throws FileNotFoundException, IOException {
		ImageOutputStream out;

		if (dest instanceof File) {
			// write statistics
			if (dest instanceof File) {
				dest = new File((File) dest, new StringBuffer(name).append(
						".DEM").toString());
			}
			out = ImageIO.createImageOutputStream((File) dest);
		} else {
			final ZipOutputStream outZ = (ZipOutputStream) dest;
			final ZipEntry e = new ZipEntry(name + ".DEM");
			outZ.putNextEntry(e);
			out = ImageIO.createImageOutputStream(outZ);
		}

		out.setByteOrder(java.nio.ByteOrder.BIG_ENDIAN);

		// untile the image in case it is tiled
		// otherwise we could add tiles which are unexistant in the original
		// data
		// generating failures when reading back the data again.
		image = untileImage(image);

		// requesting an imageio writer
		// setting tile parameters in order to tile the image on the disk
		final ParameterBlockJAI pbjW = new ParameterBlockJAI("ImageWrite");
		pbjW.addSource(image);
		pbjW.setParameter("Format", "raw");
		pbjW.setParameter("Output", out);
		JAI.create("ImageWrite", pbjW);

		if (dest instanceof File) {
			out.flush();
			out.close();
		} else {
			((ZipOutputStream) dest).closeEntry();
		}
	}

	/**
	 * This method has the objective of untiling the final image to write on
	 * disk since we do not want to have tiles added to the file on disk causing
	 * failures when reading it back into memory.
	 * 
	 * @param image
	 *            Image to untile.
	 * 
	 * @return Untiled image.
	 */
	private PlanarImage untileImage(RenderedImage image) {
		final ParameterBlockJAI pbj = new ParameterBlockJAI("format");
		pbj.addSource(image);
		pbj.setParameter("dataType", image.getSampleModel().getTransferType());

		final ImageLayout layout = new ImageLayout(image);
		layout.unsetTileLayout();
		layout.setTileGridXOffset(0);
		layout.setTileGridYOffset(0);
		layout.setTileHeight(image.getHeight());
		layout.setTileWidth(image.getWidth());
		layout.setValid(ImageLayout.TILE_GRID_X_OFFSET_MASK
				| ImageLayout.TILE_GRID_Y_OFFSET_MASK
				| ImageLayout.TILE_HEIGHT_MASK | ImageLayout.TILE_WIDTH_MASK);

		final RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT,
				layout);
		// avoid caching this image

		return JAI.create("format", pbj, hints);
	}

}
