package org.wateraspects.gui.viewer;

/*
 *    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.
 */

// J2SE dependencies
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.util.logging.Level;

import javax.media.jai.ImageMIPMap;

import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.RenderedCoverage;
import org.geotools.coverage.processing.Operations;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.referencing.FactoryFinder;
import org.geotools.referencing.operation.matrix.MatrixFactory;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.CRSUtilities;
import org.geotools.resources.geometry.XAffineTransform;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridGeometry;
import org.opengis.coverage.grid.GridRange;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.TransformException;
import org.wateraspects.util.woexception.WOException;
import org.wateraspects.util.woexception.WOExceptionDialog;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.PrecisionModel;

/**
 * A helper class for rendering {@link GridCoverage} objects. Still doesn't
 * support grid coverage SLD stylers
 *
 * @author Martin Desruisseaux
 * @author Andrea Aime
 * @source $URL: http://svn.geotools.org/geotools/tags/2.2.0/module/render/src/org/geotools/renderer/lite/GridCoverageRenderer.java $
 * @version $Id: GridCoverageRenderer.java 18671 2006-03-15 00:09:59Z jeichar $
 *
 * @task Add support for SLD stylers
 */
public final class WAGridCoverageRenderer {
    /** Tells if we should try an optimisation using pyramidal images. */
    private static final boolean USE_PYRAMID = false;

    /**
     * Decimation factor for image. A value of 0.5 means that each level in the
     * image pyramid will contains an image with half the resolution of
     * previous level. This value is used only if {@link #USE_PYRAMID} is
     * <code>true</code>.
     */
    private static final double DOWN_SAMPLER = 0.5;

    /**
     * Natural logarithm of {@link #DOWN_SAMPLER}. Used only if {@link
     * #USE_PYRAMID} is <code>true</code>.
     */
    private static final double LOG_DOWN_SAMPLER = Math.log(DOWN_SAMPLER);

    /**
     * Minimum size (in pixel) for use of pyramidal image. Images smaller than
     * this size will not use pyramidal images, since it would not give many
     * visible benefict. Used only if {@link #USE_PYRAMID} is
     * <code>true</code>.
     */
    private static final int MIN_SIZE = 256;

    /** The grid coverage to be rendered. */
    private final GridCoverage gridCoverage;

    /** The Display (User defined) CRS **/
	private final CoordinateReferenceSystem destinationCRS;

    /**
     * A list of multi-resolution images. Image at level 0 is identical to
     * {@link GridCoverage#getRenderedImage()}.  Other levels contains the
     * image at lower resolution for faster rendering.
     */
    private final ImageMIPMap images;

    /** Maximum amount of level to use for multi-resolution images. */
    private final int maxLevel;

    /**
     * The renderered image that represents the grid coverage according to  the
     * current style setting
     */
    private RenderedImage image;
    
    private GridGeometry gridGeometry;
    
    protected  final static CoordinateOperationFactory opFactory = FactoryFinder.getCoordinateOperationFactory(new Hints(Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE));
    
    /**
     * Creates a new GridCoverageRenderer object.
     *
     * @param gridCoverage DOCUMENT ME!
     */
    public WAGridCoverageRenderer(final GridCoverage gridCoverage, final CoordinateReferenceSystem destinationCRS) {
        this.gridCoverage = gridCoverage;
        if(destinationCRS!=null){
        	this.destinationCRS = destinationCRS;
        }else {
        	this.destinationCRS = gridCoverage.getCoordinateReferenceSystem();
        }
        
        if(gridCoverage.getGridGeometry()!= null){
        	this.gridGeometry = gridCoverage.getGridGeometry();
        }

        if (gridCoverage instanceof GridCoverage2D) {
        	image = ((GridCoverage2D) gridCoverage).geophysics(false).getRenderedImage();
        }else if (gridCoverage instanceof RenderedCoverage) {
            image = ((RenderedCoverage) gridCoverage).getRenderedImage();
        } 

        if (USE_PYRAMID) {
            AffineTransform at = AffineTransform.getScaleInstance(
                    DOWN_SAMPLER, DOWN_SAMPLER);
            images = new ImageMIPMap(image, at, null);
        } else {
            images = null;
        }

        double maxSize = Math.max(image.getWidth(), image.getHeight());
        int logLevel = (int) (Math.log(MIN_SIZE / maxSize) / LOG_DOWN_SAMPLER);
        maxLevel = Math.max(logLevel, 0);
    }

    /**
     * Paint this grid coverage. The caller must ensure that
     * <code>graphics</code> has an affine transform mapping "real world"
     * coordinates in the coordinate system given by {@link
     * #getCoordinateSystem}.
     *
     * @param graphics the <code>Graphics</code> context in which to paint
     *
     * @throws UnsupportedOperationException if the transformation from grid to
     *         coordinate system in the GridCoverage is not an AffineTransform
     */
    public void paint(final Graphics2D graphics) {
    	
    	/**
    	 * STEP 1
    	 * setting the destination crs for the display device
    	 * 
    	 */
    	final CoordinateReferenceSystem displayCRS = this.destinationCRS;
    	
    	/**
    	 * STEP 2
    	 * converting the envelope of the provided coverage to the destiantion crs\
    	 * WHEN NEEDED!
    	 */
        org.opengis.spatialschema.geometry.Envelope env=this.gridCoverage.getEnvelope();
        double minimumX = env.getMinimum(0);
		double minimumY = env.getMinimum(1);
        double maximumY = env.getMaximum(1);
        double maximumX = env.getMaximum(0);
//        System.out.println("RasterRenderer envelope xMin:"+minimumX);
//        System.out.println("RasterRenderer envelope yMin:"+minimumY);
//        System.out.println("RasterRenderer envelope xMax:"+maximumX);
//        System.out.println("RasterRenderer envelope yMax:"+maximumY);

        
        double[] cornersOfEnvelope = new double[]{minimumX,minimumY,
        								maximumX,minimumY,
        								maximumX,maximumY,
        								minimumX,maximumY};
        
		// If there is a math transform then be avare that rotation changes the size of the
        // envelope.. therefor some scaling is applied here:
        if(gridGeometry!=null){
        	MathTransform transform = gridGeometry.getGridToCoordinateSystem();
        	if(transform!=null && transform instanceof AffineTransform){
        		int lowerX = gridGeometry.getGridRange().getLower(0);
        		int lowerY = gridGeometry.getGridRange().getLower(1);
        		int upperX = gridGeometry.getGridRange().getUpper(0);
        		int upperY = gridGeometry.getGridRange().getUpper(1);
	            scaleEnvelopeDueToRotation(cornersOfEnvelope, (AffineTransform) transform, upperX-lowerX,upperY-lowerY);
        	}
        }
        
        double minX = Double.POSITIVE_INFINITY;
        double maxX = Double.NEGATIVE_INFINITY;
        double minY = Double.POSITIVE_INFINITY;
        double maxY = Double.NEGATIVE_INFINITY;
        
        for(int i=0;i<cornersOfEnvelope.length;i = i+2){
        	double x = cornersOfEnvelope[i];
        	double y = cornersOfEnvelope[i+1];
        	if(x>maxX)
        		maxX = x;
        	if(y>maxY)
        		maxY = y;
        	if(x<minX)
        		minX = x;
        	if(y<minY)
        		minY = y;
        }
//        System.out.println("Min x: "+minX);
//        System.out.println("Min y: "+minY);
//        System.out.println("Max x: "+maxX);
//        System.out.println("Max y: "+maxY);

    	GeneralEnvelope newEnvelope = new GeneralEnvelope(new double[]{minX,minY},new double[]{maxX,maxY});
//    			(GeneralDirectPosition) this.gridCoverage.getEnvelope().getLowerCorner(),
//    			(GeneralDirectPosition) this.gridCoverage.getEnvelope().getUpperCorner()
//    	);
    	newEnvelope.setCoordinateReferenceSystem(displayCRS);
    	try{
	    	//checking if we need tp transform coordinate reference system from source to display
	          //getting an operation between source and destination crs
	        final CoordinateOperation operation=opFactory.createOperation(
	        		this.gridCoverage.getCoordinateReferenceSystem(),
					displayCRS);
	        MathTransform crsTransform=operation.getMathTransform();
	
	        if( !crsTransform.isIdentity() ) {
//	        	System.out.println("Transform is not identity");
	        	newEnvelope = CRSUtilities.transform(crsTransform, newEnvelope);
	        	newEnvelope.setCoordinateReferenceSystem(displayCRS);
	        }
    	}
    	catch(Exception e){
    		e.printStackTrace();
    	}
    	
    	/**
    	 * STEP 3
    	 * Creating the parameters for the reprojection of the coverage.
    	 */
    	MathTransform crsToDeviceGeometry = null;
    	if(gridGeometry.getGridToCoordinateSystem()!=null && gridGeometry.getGridToCoordinateSystem() instanceof AffineTransform){
    		crsToDeviceGeometry = gridGeometry.getGridToCoordinateSystem();
    	}else{
    		crsToDeviceGeometry = (MathTransform) crsToDeviceGeometry(this.gridCoverage.getGridGeometry().getGridRange(), newEnvelope);
    	}
    	GridGeometry2D newGridGeometry = new GridGeometry2D(
    			this.gridCoverage.getGridGeometry().getGridRange(),
				crsToDeviceGeometry,
				newEnvelope.getCoordinateReferenceSystem()
    	);
    	Operations coverageOperations = new Operations(new RenderingHints(Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE));
    	GridCoverage2D prjGridCoverage = (GridCoverage2D) coverageOperations.resample(
    			this.gridCoverage,
    			displayCRS,
				newGridGeometry,
				null
    	);

        final MathTransform2D mathTransform =(MathTransform2D) prjGridCoverage.getGridGeometry().getGridToCoordinateSystem();
        
//        mathTransform = 

        if (!(mathTransform instanceof AffineTransform)) {
            throw new UnsupportedOperationException(
                "Non-affine transformations not yet implemented"); // TODO
        }

        final AffineTransform gridToDevice = new AffineTransform((AffineTransform) mathTransform);

        //gridToDevice.concatenate(crsToDeviceGeometry(this.gridCoverage.getGridGeometry().getGridRange(), newEnvelope));
        
        if (images == null) {
        	AffineTransform transform = new AffineTransform(gridToDevice);
            transform.translate(-0.5, -0.5); // Map to upper-left corner.

            try {
                graphics.drawRenderedImage(image, transform);
            } catch (Exception e) {
                image = getGoodImage(image);
                graphics.drawRenderedImage(image, transform);
            }

        } else {
            /*
             * Compute the most appropriate level as a function of the required
             * resolution
             */
            AffineTransform transform = graphics.getTransform();
            transform.concatenate(gridToDevice);

            double maxScale = Math.max(
                    XAffineTransform.getScaleX0(transform),
                    XAffineTransform.getScaleY0(transform));
            int level = Math.min(
                    maxLevel, (int) (Math.log(maxScale) / LOG_DOWN_SAMPLER));

            if (level < 0) {
                level = 0;
            }

            /*
             * If using an inferior resolution to speed up painting adjust
             * georeferencing
             */
            transform.setTransform(gridToDevice);

            if (level != 0) {
                final double scale = Math.pow(DOWN_SAMPLER, -level);
                transform.scale(scale, scale);
            }

            transform.translate(-0.5, -0.5); // Map to upper-left corner.
            graphics.drawRenderedImage(images.getImage(level), transform);
        }
    }

    /**
     * This method scales an envelope defined by its corners. The scaling that is applied is found from
     * the rotation in the MathTransform
     * @param cornersOfEnvelope
     * @param transform
     * @param width
     * @param height
     */
	public static void scaleEnvelopeDueToRotation(double[] cornersOfEnvelope, AffineTransform affineTransform, double width, double height) {
		double angle = getRotationFromTransform(affineTransform);
		if(angle!=0){
//			System.out.println("Rotation: "+angle/Math.PI*180);
			double scaleX = width/(Math.abs(Math.cos(angle)*width)+Math.abs(Math.sin(angle)*height));
			double scaleY = height/(Math.abs(Math.cos(angle)*height)+Math.abs(Math.sin(angle)*width));
//			System.out.println("Scale x: "+scaleX + " scale Y: "+scaleY);
			AffineTransform scaleTransform = new AffineTransform();
			scaleTransform.translate((width-1)/2,(height-1)/2);
			scaleTransform.scale(scaleX,scaleY);
			scaleTransform.translate(-(width-1)/2,-(height-1)/2);
			scaleTransform.transform(cornersOfEnvelope,0,cornersOfEnvelope,0,cornersOfEnvelope.length/2);
		}
	}

	/**
	 * This method return the rotation of an <code>AffineTransform<code>.
	 * Note that the method only works for transforms without shear and 
	 * only for rotations smaller than PI
	 * @param affineTransform The <code>AffineTransform<code>
	 * @return rotation angle in radians
	 */
	public static double getRotationFromTransform(AffineTransform affineTransform) {
		double scaleX0 = Math.sqrt(affineTransform.getScaleX()*affineTransform.getScaleX() +
                affineTransform.getShearX()*affineTransform.getShearX());
		double rotateX = Math.acos(affineTransform.getScaleX()/scaleX0);
		return rotateX;
	}
	
	/**
	 * @param gridRange
	 * @return
	 */
	private AffineTransform crsToDeviceGeometry(final GridRange gridRange, final GeneralEnvelope userRange) {
        final int dimension = gridRange.getDimension();
    	final CoordinateSystem cs = userRange.getCoordinateReferenceSystem().getCoordinateSystem();
    	boolean lonFirst = true;
    	
    	if (cs.getAxis(0).getDirection().absolute().equals(AxisDirection.NORTH)) {
    		lonFirst = false;
    	}
    	final boolean swapXY = false;
        // latitude index
        final int latIndex = lonFirst ? 1 : 0;

        final AxisDirection latitude = cs.getAxis(latIndex).getDirection();
        final AxisDirection longitude = cs.getAxis((latIndex + 1) % 2).getDirection();
        final boolean[] reverse = new boolean[] {false, true};
        
        /* 
         * Setup the multi-dimensional affine transform for use with OpenGIS.
         * According OpenGIS specification, transforms must map pixel center.
         * This is done by adding 0.5 to grid coordinates.
         */
        final Matrix matrix = MatrixFactory.create(dimension+1);
        for (int i=0; i<dimension; i++) {
            // NOTE: i is a dimension in the 'gridRange' space (source coordinates).
            //       j is a dimension in the 'userRange' space (target coordinates).
            int j = i;
            if (swapXY && j<=1) {
                j = 1-j;
            }
            double scale = userRange.getLength(j) / gridRange.getLength(i);
            double offset;
            if (reverse==null || !reverse[j]) {
                offset = userRange.getMinimum(j);
            } else {
                scale  = -scale;
                offset = userRange.getMaximum(j);
            }
            offset -= scale * (gridRange.getLower(i)-0.5);
            matrix.setElement(j, j,         0.0   );
            matrix.setElement(j, i,         scale );
            matrix.setElement(j, dimension, offset);
        }
        return (AffineTransform) ProjectiveTransform.create(matrix);
	}


//	/**
//	 * This class is designed to use for Odense Radar pictures with a background 
//	 * picture... 
//	 * @param geometry
//	 * @return
//	 */
//	private AffineTransform crsToDeviceGeometry(GridGeometry geometry, final GeneralEnvelope userRange) {
//        
//        // Use the MathTransform to create the crsToDeviceGeometry.
//		// The main problem here is that the scaling and rotation has already been
//		// applied to the envelope. Rotating an envelope does not rotate the gridCoverage
//		// So the rotation must be found and aplied here: 
//        AffineTransform affineTransform = new AffineTransform();
//        MathTransform gridToCRS = geometry.getGridToCoordinateSystem();
//        if(true){
//        return (AffineTransform) gridToCRS;
//        }else
//        if(gridToCRS !=null && gridToCRS instanceof AffineTransform){
//        	// Get the rotation:
//        	double rotation = getRotationFromTransform((AffineTransform) gridToCRS);
//    		System.out.println("Rotation: "+rotation/Math.PI*180);
//    		// Find height and width of gridGeometry:
//    		int lowerX = gridGeometry.getGridRange().getLower(0);
//    		int lowerY = gridGeometry.getGridRange().getLower(1);
//    		int upperX = gridGeometry.getGridRange().getUpper(0);
//    		int upperY = gridGeometry.getGridRange().getUpper(1);
//    		double height = upperY-lowerY;
//    		double width = upperX-lowerX;
//    		
//    		// Find the center
//			double centerX = this.gridCoverage.getEnvelope().getCenter(0);
//			double centerY = this.gridCoverage.getEnvelope().getCenter(1);
//			System.out.println("Center, X: "+centerX + " y: "+centerY);
//
//    		
//    		// Create the original rotation transform:
//    		AffineTransform originalRotation = AffineTransform.getRotateInstance(rotation,(width-1)/2.0,(height-1)/2.0);
//    		
//    		// Get the translation from the original rotation:
//    		double rotationTranslateX = originalRotation.getTranslateX();
//    		double rotationTranslateY = originalRotation.getTranslateY();
//    		System.out.println("Translate due to rotation, x: "+rotationTranslateX + " y: "+rotationTranslateY);
//    		
//    		// Get the translation due to the original scaling:
//			double scaleX = ((AffineTransform)gridToCRS).getScaleX();
//			double scaleY = ((AffineTransform)gridToCRS).getScaleY();
//			if(scaleX == 0)
//				scaleX = 1;
//			if(scaleY == 0)
//				scaleY=1;
//			scaleX = scaleX/Math.cos(rotation);
//			scaleY = scaleY/Math.cos(rotation);
//			System.out.println("Scale, x: "+scaleX + " y: "+scaleY);
//    		double translateDueToScaleX = centerX*scaleX-centerX;
//    		double translateDueToScaleY = centerY*scaleY-centerY;
//    		
//    		// Get the original translate:
//    		double translateX = ((AffineTransform)gridToCRS).getTranslateX()/scaleX
//    									-rotationTranslateX
//    									+translateDueToScaleX/scaleX;
//			double translateY = ((AffineTransform)gridToCRS).getTranslateY()/scaleX
//										-rotationTranslateY
//										+translateDueToScaleY/scaleY;
//			System.out.println("Translate,  X: "+translateX +" Y: "+translateY);
//			
//			translateDueToScaleX = (centerX-translateX)*(scaleX-1);
//			translateDueToScaleY = (centerY-translateY)*(scaleY-1);
//			
//    		System.out.println("Translate due to scale  , x: "+translateDueToScaleX + " y: "+translateDueToScaleY);
//
//	        // Get the translation in the rotated coordinate system:
//	        double[] ds = new double[]{translateX*scaleX,translateY*scaleY};
//	        AffineTransform pureRotate = AffineTransform.getRotateInstance(rotation);
//	        pureRotate.scale(scaleX,scaleY);
//	        try {
//				pureRotate.inverseTransform(ds,0,ds,0,1);
////					System.out.println("ds[0]: "+ds[0] + " ds[1]" + ds[1]);
//
//			} catch (NoninvertibleTransformException e) {
//				// Nothing a pure rotation transform is always invertible
//				e.printStackTrace();
//			}
//			
//			// Apply the translation in the rotated coordinate system:
//			originalRotation.translate(ds[0],ds[1]);
//			
//			// Apply the scale:
//			originalRotation.scale(scaleX,scaleY);
//	        // Fix the position change due to the scaling:
//	        originalRotation.translate(-translateDueToScaleX/scaleX,
//	        		-translateDueToScaleY/scaleX);
//
//    		affineTransform.concatenate(originalRotation);
//
//		}
//        return (AffineTransform) ProjectiveTransform.create(affineTransform);
//	}

	/**
     * Work around a bug in older JDKs
     *
     * @param img The JAI rendered image
     *
     * @return a buffered image that can be handled by Java2D without problems
     */
    private RenderedImage getGoodImage(RenderedImage img) {
        BufferedImage good = new BufferedImage(
                img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
        Graphics2D g2d = (Graphics2D) good.getGraphics();
        g2d.drawRenderedImage(img, new AffineTransform());

        return good;
    }
}
