/*
 *    Geotools2 - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2002, 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.image;

import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.renderable.ParameterBlock;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.channels.FileChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;

import org.geotools.coverage.grid.GeneralGridRange;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.data.DataSourceException;
import org.geotools.data.PrjFileReader;
import org.geotools.data.WorldFileReader;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.parameter.Parameter;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.matrix.AffineTransform2D;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridCoverageReader;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.MismatchedDimensionException;

/**
 * Reads a GridCoverage from a given source. WorldImage sources only support one
 * GridCoverage so hasMoreGridCoverages() will return true until the only
 * GridCoverage is read. No metadata is currently supported, so all related
 * methods return null. In the early future we will start (hopefully supporting
 * them).
 * 
 * @author simone giannecchini
 * @author alessio fabiani
 * @author rgould
 */
public final class WorldImageReader extends AbstractGridCoverage2DReader
		implements GridCoverageReader {

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

	private boolean wmsRequest;

	private boolean metaFile;

	private String parentPath;

	private String extension;

	private ImageReaderSpi readerSPI;

	/**
	 * Class constructor. Construct a new ImageWorldReader to read a
	 * GridCoverage from the source object. The source must point to the raster
	 * file itself, not the world file. If the source is a Java URL it checks if
	 * it is ponting to a file and if so it converts the url into a file.
	 * 
	 * @param input
	 *            The source of a GridCoverage, can be a File, a URL or an input
	 *            stream.
	 * @throws DataSourceException
	 */
	public WorldImageReader(Object input) throws DataSourceException {
		this(input, null);
	}
	/**
	 * Class constructor. Construct a new ImageWorldReader to read a
	 * GridCoverage from the source object. The source must point to the raster
	 * file itself, not the world file. If the source is a Java URL it checks if
	 * it is ponting to a file and if so it converts the url into a file.
	 * 
	 * @param input
	 *            The source of a GridCoverage, can be a File, a URL or an input
	 *            stream.
	 * @throws DataSourceException
	 */
	public WorldImageReader(Object input,final Hints hints) throws DataSourceException {
		// /////////////////////////////////////////////////////////////////////
		//
		// Checking input
		//
		// /////////////////////////////////////////////////////////////////////
		if (input == null) {

			final IOException ex = new IOException(
					"WorldImage:No source set to read this coverage.");
			LOGGER.logp(Level.SEVERE, WorldImageReader.class.toString(),
					"WorldImageReader", ex.getLocalizedMessage(), ex);
			throw new DataSourceException(ex);
		}
		this.source = input;
		if (hints != null)
			this.hints.add(hints);
		coverageName = "image_coverage";
		try {
			boolean closeMe = true;

			// /////////////////////////////////////////////////////////////////////
			//
			// Source management
			//
			// /////////////////////////////////////////////////////////////////////
			if (input instanceof URL) {
				// URL that point to a file
				final URL sourceURL = ((URL) input);
				if (sourceURL.getProtocol().compareToIgnoreCase("file") == 0) {
					String auth = sourceURL.getAuthority();
					String path = sourceURL.getPath();
					if (auth != null && !auth.equals("")) {
						path = "//"+auth+path;
					}
					this.source = input = new File(URLDecoder.decode(path, "UTF-8"));
				} else if (sourceURL.getProtocol().equalsIgnoreCase("http")) {
					// // getting a stream to the reader
					// this.source = sourceURL.openStream();

					// /////////////////////////////////////////////////////////////////////
					//
					// WMS Request? I want to be able to handle that case too
					//
					// /////////////////////////////////////////////////////////////////////
					wmsRequest = WMSRequest(input);
				}
			}

			// //
			//
			// Name, path, etc...
			//
			// //
			if (input instanceof File) {
				final File sourceFile = (File) input;
				final String filename = sourceFile.getName();
				final int i = filename.lastIndexOf('.');
				final int length = filename.length();
				if (i > 0 && i < length - 1) {
					extension = filename.substring(i + 1).toLowerCase();
				}
				this.parentPath = sourceFile.getParent();
				this.coverageName = filename;
				final int dotIndex = coverageName.lastIndexOf(".");
				coverageName = (dotIndex == -1) ? coverageName : coverageName
						.substring(0, dotIndex);
			} else if (input instanceof URL)
				input = ((URL) input).openStream();
			// //
			//
			// Get a stream in order to read from it for getting the basic
			// information for this coverfage
			//
			// //
			if (input instanceof ImageInputStream)
				closeMe = false;

			inStream = (ImageInputStream) (this.source instanceof ImageInputStream ? this.source
					: ImageIO
							.createImageInputStream((this.source instanceof URL) ? ((URL) this.source)
									.openStream()
									: this.source));
			if (inStream == null)
				throw new IllegalArgumentException(
						"No input stream for the provided source");

			// /////////////////////////////////////////////////////////////////////
			//
			// CRS
			//
			// /////////////////////////////////////////////////////////////////////
			if (!wmsRequest) {
				final Object tempCRS = this.hints
						.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
				if (tempCRS != null) {
					this.crs = (CoordinateReferenceSystem) tempCRS;
					LOGGER.log(Level.WARNING, new StringBuffer(
							"Using forced coordinate reference system ")
							.append(crs.toWKT()).toString());
				} else
					readCRS();
			}

			// /////////////////////////////////////////////////////////////////////
			//
			// Informations about multiple levels and such
			//
			// /////////////////////////////////////////////////////////////////////
			getHRInfo();

			// release the stream
			if (closeMe)
				inStream.close();
		} catch (IOException e) {
			LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
			throw new DataSourceException(e);
		} catch (TransformException e) {
			LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
			throw new DataSourceException(e);
		}
	}

	/**
	 * Gets the relevant information for the underlying raster.
	 * 
	 * @throws IOException
	 * @throws TransformException
	 */
	private void getHRInfo() throws IOException, TransformException {

		// //
		//
		// Get a reader for this format
		// TODO optimize this using image file extension when possible
		//
		// //
		final Iterator it = ImageIO.getImageReaders(inStream);
		if (!it.hasNext())
			throw new DataSourceException("No reader avalaible for this source");
		final ImageReader reader = (ImageReader) it.next();
		readerSPI = reader.getOriginatingProvider();
		reader.setInput(inStream);

		// //
		//
		// get the dimension of the hr image and build the model as well as
		// computing the resolution
		// //
		numOverviews = wmsRequest ? 0 : reader.getNumImages(true) - 1;
		int hrWidth = reader.getWidth(0);
		int hrHeight = reader.getHeight(0);
		final Rectangle actualDim = new Rectangle(0, 0, hrWidth, hrHeight);
		originalGridRange = new GeneralGridRange(actualDim);

		// /////////////////////////////////////////////////////////////////////
		//
		// Envelope, coverage name and other resolution information
		//
		// /////////////////////////////////////////////////////////////////////
		if (source instanceof File) {
			prepareWorldImageGridToWorldTransform();

			// //
			//
			// In case we read from a real world file we have toget the envelope
			//
			// //
			if (!metaFile) {
				final AffineTransform tempTransform = new AffineTransform(
						(AffineTransform) raster2Model);
				tempTransform.translate(-0.5, -0.5);

				originalEnvelope = CRS.transform(ProjectiveTransform
						.create(tempTransform), new GeneralEnvelope(actualDim));
				originalEnvelope.setCoordinateReferenceSystem(crs);
			}

			// ///
			//
			// setting the higher resolution avalaible for this coverage
			//
			// ///
			highestRes = getResolution(originalEnvelope, actualDim, crs);

		}
		// //
		//
		// get information for the overviews in case ony exists
		//
		// //
		if (numOverviews > 1) {
			overViewResolutions = new double[numOverviews][2];
			double res[];
			for (int i = 0; i < numOverviews; i++) {
				res = getResolution(originalEnvelope, new Rectangle(0, 0,
						reader.getWidth(i), reader.getHeight(i)), crs);
				overViewResolutions[i][0] = res[0];
				overViewResolutions[i][1] = res[1];
			}
		} else
			overViewResolutions = null;
	}

	/**
	 * Returns the format that this Reader accepts.
	 * 
	 * @return a new WorldImageFormat class
	 */
	public Format getFormat() {
		return new WorldImageFormat();
	}

	/**
	 * Reads an image from a source stream. Loads an image from a source stream,
	 * then loads the values from the world file and constructs a new
	 * GridCoverage from this information. When reading from a remote stream we
	 * do not look for a world fiel but we suppose those information comes from
	 * a different way (xml, gml, pigeon?)
	 * 
	 * @param params
	 *            WorldImageReader supports no parameters, it just ignores them.
	 * 
	 * @return a new GridCoverage read from the source.
	 * 
	 * @throws IllegalArgumentException
	 *             DOCUMENT ME!
	 * @throws IOException
	 *             DOCUMENT ME!
	 */
	public GridCoverage read(GeneralParameterValue[] params)
			throws IllegalArgumentException, IOException {

		// /////////////////////////////////////////////////////////////////////
		//
		// do we have paramters to use for reading from the specified source
		//
		// /////////////////////////////////////////////////////////////////////
		GeneralEnvelope requestedEnvelope = null;
		Rectangle dim = null;
		if (params != null) {
			// /////////////////////////////////////////////////////////////////////
			//
			// Checking params
			//
			// /////////////////////////////////////////////////////////////////////
			if (params != null) {
				for (int i = 0; i < params.length; i++) {
					final Parameter param = (Parameter) params[i];
					if (param.getDescriptor().getName().getCode().equals(
							AbstractGridFormat.READ_GRIDGEOMETRY2D.getName()
									.toString())) {
						final GridGeometry2D gg = (GridGeometry2D) param
								.getValue();
						requestedEnvelope = new GeneralEnvelope((Envelope) gg
								.getEnvelope2D());
						dim = gg.getGridRange2D().getBounds();
					}
				}
			}
		}

		// /////////////////////////////////////////////////////////////////////
		//
		// set params
		//
		// /////////////////////////////////////////////////////////////////////
		Integer imageChoice = new Integer(0);
		final ImageReadParam readP = new ImageReadParam();
		if (!wmsRequest) {
			try {
				imageChoice = setReadParams(readP, requestedEnvelope, dim);
			} catch (TransformException e) {
				new DataSourceException(e);
			}
		}
		// /////////////////////////////////////////////////////////////////////
		//
		// Reading the source layer
		//
		// /////////////////////////////////////////////////////////////////////
//		final ImageReader reader = readerSPI.createReaderInstance();
//		final ImageInputStream inStream = wmsRequest ? ImageIO
//				.createImageInputStream(((URL) source).openStream()) : ImageIO
//				.createImageInputStream(source);
//		
		final Hints newHints = (Hints) hints.clone();
//		if (!wmsRequest) {
//			reader.setInput(inStream);
//			if (!reader.isImageTiled(imageChoice.intValue())) {
//				final Dimension tileSize = ImageUtilities
//						.toTileSize(new Dimension(reader.getWidth(imageChoice
//								.intValue()), reader.getHeight(imageChoice
//								.intValue())));
//				final ImageLayout layout = new ImageLayout();
//				layout.setTileGridXOffset(0);
//				layout.setTileGridYOffset(0);
//				layout.setTileHeight(tileSize.height);
//				layout.setTileWidth(tileSize.width);
//				newHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout));
//			}
//		}
//		inStream.close();
		final ParameterBlock pbjRead = new ParameterBlock();
		pbjRead.add(wmsRequest ? ImageIO
				.createImageInputStream(((URL) source).openStream()) : ImageIO
				.createImageInputStream(source));
		pbjRead.add(imageChoice);
		pbjRead.add(Boolean.FALSE);
		pbjRead.add(Boolean.FALSE);
		pbjRead.add(Boolean.FALSE);
		pbjRead.add(null);
		pbjRead.add(null);
		pbjRead.add(readP);
		pbjRead.add(readerSPI.createReaderInstance());

		// /////////////////////////////////////////////////////////////////////
		//
		// BUILDING COVERAGE
		//
		// /////////////////////////////////////////////////////////////////////
		// get the raster -> model transformation and
		// create the coverage
		if (imageChoice.intValue() == 0 && !metaFile) {
			final AffineTransform tempRaster2Model = new AffineTransform(
					(AffineTransform) raster2Model);
			tempRaster2Model.concatenate(new AffineTransform(readP
					.getSourceXSubsampling(), 0, 0, readP
					.getSourceYSubsampling(), 0, 0));
			return createImageCoverage(JAI.create("ImageRead", pbjRead,
					(RenderingHints) newHints), ProjectiveTransform
					.create((AffineTransform) tempRaster2Model));

		}
		return createImageCoverage(JAI.create("ImageRead", pbjRead,
				 (RenderingHints) newHints));

	}

	/**
	 * This method is used to check if we are connecting directly to a WMS with
	 * a getmap request. In such a case we skip reading all the parameters we
	 * can read from this http string.
	 * 
	 * @param input
	 * 
	 * @return true if we are dealing with a WMS request, false otherwise.
	 */
	private boolean WMSRequest(Object input) {
		// TODO do we need the requested envelope?
		if (input instanceof URL
				&& (((URL) input).getProtocol().equalsIgnoreCase("http"))) {
			try {
				// getting the query
				final String query = java.net.URLDecoder.decode(((URL) input)
						.getQuery().intern(), "UTF-8");

				// should we proceed? Let's look for a getmap WMS request
				if (query.intern().indexOf("GetMap") == -1) {
					return false;
				}

				// tokenizer on $
				final String[] pairs = query.split("&");

				// parse each pair
				final int numPairs = pairs.length;
				String[] kvp = null;

				for (int i = 0; i < numPairs; i++) {
					// splitting the pairs
					kvp = pairs[i].split("=");

					// checking the fields
					// BBOX
					if (kvp[0].equalsIgnoreCase("BBOX")) {
						// splitting fields
						kvp = kvp[1].split(",");
						originalEnvelope = new GeneralEnvelope(new double[] {
								Double.parseDouble(kvp[0]),
								Double.parseDouble(kvp[1]) }, new double[] {
								Double.parseDouble(kvp[2]),
								Double.parseDouble(kvp[3]) });
					}

					// SRS
					if (kvp[0].equalsIgnoreCase("SRS")) {
						crs = CRS.decode(kvp[1], true);
					}

					// layers
					if (kvp[0].equalsIgnoreCase("layers")) {
						this.coverageName = kvp[1].replaceAll(",", "_");
					}
				}

			} catch (IOException e) {
				// TODO how to handle this?
				return false;

			} catch (NoSuchAuthorityCodeException e) {
				// TODO how to handle this?
				return false;
			} catch (MismatchedDimensionException e) {
				// TODO how to handle this?
				return false;
			} catch (IndexOutOfBoundsException e) {
				// TODO how to handle this?
				return false;
			} catch (FactoryException e) {
				// TODO how to handle this?
				return false;
			}

			return true;
		}

		return false;
	}

	/**
	 * This method is responsible for reading the CRS whhther a projection file
	 * is provided. If no projection file is provided the second choice is the
	 * CRS supplied via the crs paramter. If even this one is not avalaible we
	 * default to EPSG:4326.
	 * 
	 * @throws IOException
	 */
	private void readCRS() throws IOException {

		// check to see if there is a projection file
		if (source instanceof File
				|| (source instanceof URL && (((URL) source).getProtocol() == "file"))) {
			// getting name for the prj file
			final String sourceAsString;

            if (source instanceof File) {
                sourceAsString = ((File) source).getAbsolutePath();
            } else {
            	String auth = ((URL) source).getAuthority();
            	String path = ((URL) source).getPath();
            	if (auth != null && !auth.equals("")) {
            		sourceAsString = "//"+auth+path;
            	} else {
            		sourceAsString = path;
            	}
            }

			final int index = sourceAsString.lastIndexOf(".");
			final StringBuffer base = new StringBuffer(sourceAsString
					.substring(0, index)).append(".prj");

			// does it exist?
			final File prjFile = new File(base.toString());
			if (prjFile.exists()) {
				// it exists then we have top read it
				PrjFileReader projReader=null;
				try {
					final FileChannel channel = new FileInputStream(prjFile)
							.getChannel();
					projReader = new PrjFileReader(channel);
					crs = projReader.getCoordinateReferenceSystem();
				} catch (FileNotFoundException e) {
					// warn about the error but proceed, it is not fatal
					// we have at least the default crs to use
					LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
				} catch (IOException e) {
					// warn about the error but proceed, it is not fatal
					// we have at least the default crs to use
					LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
				} catch (FactoryException e) {
					// warn about the error but proceed, it is not fatal
					// we have at least the default crs to use
					LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
				}
				finally{
					if(projReader!=null)
						try{
							projReader.close();
						}
					catch (IOException e) {
						// warn about the error but proceed, it is not fatal
						// we have at least the default crs to use
						LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
					}
				}

			}
		}
		if (crs == null) {
			crs = AbstractGridFormat.getDefaultCRS();
			LOGGER.info(new StringBuffer(
					"Unable to find crs, continuing with default WGS4 CRS")
					.append("\n").append(crs.toWKT()).toString());
		}

	}

	/**
	 * This method is in charge for reading the metadata file and for creating a
	 * valid envelope (whether possible);
	 * 
	 * TODO it would be great to having a centralized management for the world
	 * file
	 * 
	 * @throws IOException
	 */
	private void prepareWorldImageGridToWorldTransform() throws IOException {

		// getting name and extension
		final String base = (parentPath != null) ? new StringBuffer(
				this.parentPath).append(File.separator).append(coverageName)
				.toString() : coverageName;

		// We can now construct the baseURL from this string.
		File file2Parse = new File(new StringBuffer(base).append(".wld")
				.toString());

		if (file2Parse.exists()) {
			final WorldFileReader reader = new WorldFileReader(file2Parse);
			raster2Model = reader.getTransform();
		} else {
			// looking for another extension
			final Set ext=WorldImageFormat.getWorldExtension(extension);
			final Iterator it=ext.iterator();
			if(!it.hasNext())
				throw new DataSourceException("Unable to parse extension "+extension);
			do{
			file2Parse = new File(new StringBuffer(base).append((String)it.next()
					).toString());
			}while(!file2Parse.exists()&&it.hasNext());
			
			if (file2Parse.exists()) {
				// parse world file
				final WorldFileReader reader = new WorldFileReader(file2Parse);
				raster2Model = reader.getTransform();
				metaFile = false;
			} else {
				// looking for a meta file
				file2Parse = new File(new StringBuffer(base).append(".meta")
						.toString());

				if (file2Parse.exists()) {
					parseMetaFile(file2Parse);
					metaFile = true;
				} else {
					final IOException ex = new IOException(
							"No file with meta information found: " + file2Parse.getAbsolutePath());
					LOGGER
							.logp(
									Level.SEVERE,
									WorldImageReader.class.toString(),
									"private void prepareWorldImage2Model() throws IOException",
									ex.getLocalizedMessage(), ex);
					throw ex;
				}
			}
		}
	}

	/**
	 * This method is responsible for parsing a META file which is nothing more
	 * than another format of a WorldFile used by the GIDB database.
	 * 
	 * @param file2Parse
	 *            
	 * 
	 * @throws NumberFormatException
	 * @throws IOException
	 * 
	 * @task move me to a separate implementation
	 */
	private void parseMetaFile(File file2Parse) throws NumberFormatException,
			IOException {
		double xMin = 0.0;
		double yMax = 0.0;
		double xMax = 0.0;
		double yMin = 0.0;

		// getting a buffered reader
		final BufferedReader in = new BufferedReader(new FileReader(file2Parse));

		// parsing the lines
		String str = null;
		int index = 0;
		double value = 0;

		while ((str = in.readLine()) != null) {
			switch (index) {
			case 1:
				value = Double.parseDouble(str.substring("Origin Longitude = "
						.intern().length()));
				xMin = value;

				break;

			case 2:
				value = Double.parseDouble(str.substring("Origin Latitude = "
						.intern().length()));
				yMin = value;

				break;

			case 3:
				value = Double.parseDouble(str.substring("Corner Longitude = "
						.intern().length()));
				xMax = value;

				break;

			case 4:
				value = Double.parseDouble(str.substring("Corner Latitude = "
						.intern().length()));
				yMax = value;

				break;

			default:
				break;
			}

			index++;
		}

		in.close();

		// building up envelope of this coverage
		originalEnvelope = new GeneralEnvelope(new double[] { xMin, yMin },
				new double[] { xMax, yMax });
		originalEnvelope.setCoordinateReferenceSystem(crs);
	}

}
