import org.geotools.feature.simple.SimpleFeatureImpl;
import org.geotools.filter.identity.FeatureIdImpl;
import org.geotools.geometry.jts.JTS;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;

import fr.ocelet.runtime.List;

/**
 * Represents one record from a shapefile. Such a record contains a geometry
 * attribute and a series a other attributes that can be of type String,
 * boolean, int or double.
 * 
 * @author pdegenne@teledetection.fr
 */
public class ShpRecord {

	private final String ERR_HEADER = "Datafacer ShapeFile: ";
	private SimpleFeature feature;

	public ShpRecord() {
		// TODO Feature à initialiser à partir d'un descripteur fourni par le
		// ShapeFile qui contient le schema des attributs
	}

	public ShpRecord(SimpleFeature sf) {
		feature = sf;
	}

	public boolean isMPoints() {
		return (feature.getAttribute("the_geom") instanceof MultiPoint);
	}

	public boolean isPolyline() {
		return (feature.getAttribute("the_geom") instanceof MultiLineString);
	}

	public boolean isSPolygon() {
		return (feature.getAttribute("the_geom") instanceof MultiPolygon);
	}

	/**
	 * Updates the value of the specified attribute with the given value
	 * 
	 * @param attrName
	 *            Name of the attribute to update
	 * @param value
	 *            The new value for that attribute
	 */
	public void setIntAttribute(String attrName, int value) {
		feature.setAttribute(attrName, value);
	}

	/**
	 * Updates the value of the specified attribute with the given value
	 * 
	 * @param attrName
	 *            Name of the attribute to update
	 * @param value
	 *            The new value for that attribute
	 */
	public void setRealAttribute(String attrName, double value) {
		feature.setAttribute(attrName, value);
	}

	/**
	 * Updates the value of the specified attribute with the given value
	 * 
	 * @param attrName
	 *            Name of the attribute to update
	 * @param value
	 *            The new value for that attribute
	 */
	public void setBooleanAttribute(String attrName, boolean value) {
		feature.setAttribute(attrName, value);
	}

	/**
	 * Updates the value of the specified attribute with the given value
	 * 
	 * @param attrName
	 *            Name of the attribute to update
	 * @param value
	 *            The new value for that attribute
	 */
	public void setTextAttribute(String attrName, String value) {
		feature.setAttribute(attrName, value);
	}

	/**
	 * Returns an attribute of this ShpRecord as a String The attribute MUST
	 * already be a String, no conversion is performed.
	 * 
	 * @param atName
	 *            Name of the attribute to obtain
	 * @return The String value of the specified attribute
	 */
	public String getTextAttribute(String atName) {
		return (String) feature.getAttribute(atName);
	}

	/**
	 * Returns an attribute of this ShpRecord in double format The attribute
	 * MUST already be a double, no conversion is performed.
	 * 
	 * @param atName
	 *            Name of the attribute to obtain
	 * @return The double value of the specified attribute
	 */
	public double getRealAttribute(String atName) {
		return ((Double) feature.getAttribute(atName)).doubleValue();
	}

	/**
	 * Returns an attribute of this ShpRecord in int format The attribute MUST
	 * already be an int, no conversion is performed.
	 * 
	 * @param atName
	 *            Name of the attribute to obtain
	 * @return The int value of the specified attribute
	 */
	public int getIntAttribute(String atName) {
		return ((Integer) feature.getAttribute(atName)).intValue();
		// return ((Long) feature.getAttribute(atName)).intValue();
	}

	/**
	 * Returns an attribute of this ShpRecord in boolean format The attribute
	 * MUST already be a boolean, no conversion is performed.
	 * 
	 * @param atName
	 *            Name of the attribute to obtain
	 * @return The boolean value of the specified attribute
	 */
	public boolean getBooleanAttribute(String atName) {
		return ((Boolean) feature.getAttribute(atName)).booleanValue();
	}

	private Position geom2Point(Geometry g) {
		Coordinate coord = g.getCoordinate();
		return new Position(coord.x, coord.y);
	}

	private List<Position> geom2Line(Geometry g) {
		Coordinate[] coordinates = g.getCoordinates();
		List<Position> line = new List<Position>();
		for (Coordinate coord : coordinates) {
			line.add(new Position(coord.x, coord.y));
		}
		return line;
	}

	/**
	 * @deprecated Use geom2SPoly() instead
	 * @param g
	 * @return
	 */
	private List<Position> geom2Poly(Geometry g) {
		return geom2SPoly(g).getShell();
	}

	private SPolygon geom2SPoly(Geometry g) {
		SPolygon sp = null;
		if (g instanceof Polygon) {
			Polygon p = (Polygon) g;
			LineString[] holeslr = new LineString[p.getNumInteriorRing()];
			for (int lsi = 0; lsi < p.getNumInteriorRing(); lsi++)
				holeslr[lsi] = p.getInteriorRingN(lsi);
			sp = new SPolygon(p.getExteriorRing(), holeslr);
		}
		return sp;
	}

	/**
	 * Returns a MPoints with its coordinates transformed by a coordinate system
	 * transformation operation
	 * 
	 * @param mt
	 *            The MathTtransform to be applied to all coordinates
	 * @return A MPoints
	 */
	public MPoints getTransformedMPoints(MathTransform mt) {
		MPoints pl = new MPoints();
		GeometryCollection gc = (GeometryCollection) feature
				.getDefaultGeometry();
		try {
			for (int gi = 0; gi < gc.getNumGeometries(); gi++) {
				Geometry geom = gc.getGeometryN(gi);
				Geometry tgeom = JTS.transform(geom, mt);
				pl.addPoint(geom2Point(tgeom));
			}
		} catch (MismatchedDimensionException e) {
			System.out
					.println(ERR_HEADER
							+ "Mismatched dimensions when trying to transform coordinates to a new coordinates system.");
			// e.printStackTrace();
			pl = null;
		} catch (TransformException e) {
			System.out
					.println(ERR_HEADER
							+ "Transformation error when trying to transform coordinates to a new coordinates system.");
			// e.printStackTrace();
			pl = null;
		}
		return pl;
	}

	/**
	 * Returns a PolyLine with its coordinates transformed by a coordinate
	 * system transformation operation
	 * 
	 * @param mt
	 *            The MathTtransform to be applied to all coordinates
	 * @return A Polyline
	 */
	public Polyline getTransformedPolyline(MathTransform mt) {
		Polyline pl = new Polyline();
		GeometryCollection gc = (GeometryCollection) feature
				.getDefaultGeometry();
		try {
			for (int gi = 0; gi < gc.getNumGeometries(); gi++) {
				Geometry geom = gc.getGeometryN(gi);
				Geometry tgeom = JTS.transform(geom, mt);
				pl.addLine(geom2Line(tgeom));
			}
		} catch (MismatchedDimensionException e) {
			System.out
					.println(ERR_HEADER
							+ "Mismatched dimensions when trying to transform coordinates to a new coordinates system.");
			// e.printStackTrace();
			pl = null;
		} catch (TransformException e) {
			System.out
					.println(ERR_HEADER
							+ "Transformation error when trying to transform coordinates to a new coordinates system.");
			// e.printStackTrace();
			pl = null;
		}
		return pl;
	}

	/**
	 * Returns a SPolygon with its coordinates transformed by a coordinate
	 * system transformation operation
	 * 
	 * @param mt
	 *            The MathTtransform to be applied to all coordinates
	 * @return A SPolygon
	 */
	public MPolygon getTransformedMPolygon(MathTransform mt) {
		GeometryCollection gc = (GeometryCollection) feature
				.getDefaultGeometry();
		MPolygon mpl = new MPolygon();
		try {
			for (int gi = 0; gi < gc.getNumGeometries(); gi++) {
				Geometry geom = gc.getGeometryN(gi);
				Geometry tgeom = JTS.transform(geom, mt);
				mpl.addSPolygon(geom2SPoly(tgeom));
			}
		} catch (MismatchedDimensionException e) {
			System.out
					.println(ERR_HEADER
							+ "Mismatched dimensions when trying to transform coordinates to a new coordinates system.");
			// e.printStackTrace();
			mpl = null;
		} catch (TransformException e) {
			System.out
					.println(ERR_HEADER
							+ "Transformation error when trying to transform coordinates to a new coordinates system.");
			// e.printStackTrace();
			mpl = null;
		}
		return mpl;
	}

	/**
	 * Gives a new MPoints with its own set of coordinates.
	 * 
	 * @param pt
	 *            The new Point to assign to this ShpRecord
	 */
	public void setMPoint(MPoints pt) {
		feature.setDefaultGeometry(pt.asGeometry());
	}

	/**
	 * Returns the coordinates of this ShpRecord in the form of a MPoints
	 * 
	 * @return This ShpRecord's MPoints.
	 */
	public MPoints getMPoints() {
		MPoints pt = new MPoints();
		GeometryCollection gc = (GeometryCollection) feature
				.getDefaultGeometry();
		for (int gi = 0; gi < gc.getNumGeometries(); gi++) {
			Geometry geom = gc.getGeometryN(gi);
			pt.addPoint(geom2Point(geom));
		}
		return pt;
	}

	/**
	 * Returns the coordinates of this ShpRecord in the form of a Polyline
	 * 
	 * @return This ShpRecord's Polyline.
	 */
	public Polyline getPolyline() {
		Polyline pl = new Polyline();
		GeometryCollection gc = (GeometryCollection) feature
				.getDefaultGeometry();
		for (int gi = 0; gi < gc.getNumGeometries(); gi++) {
			Geometry geom = gc.getGeometryN(gi);
			pl.addLine(geom2Line(geom));
		}
		return pl;
	}

	/**
	 * Gives a new Polyline with its own set of coordinates.
	 * 
	 * @param pol
	 *            The new Polyline to assign to this ShpRecord
	 */
	public void setPolyline(Polyline pol) {
		feature.setDefaultGeometry(pol.asGeometry());
	}

	/**
	 * Returns the coordinates of this ShpRecord in the form of a MPolygon
	 * 
	 * @return This ShpRecord's MPolygon.
	 */
	public MPolygon getMPolygon() {
		MPolygon mpl = new MPolygon();
		GeometryCollection gc = (GeometryCollection) feature
				.getDefaultGeometry();
		for (int gi = 0; gi < gc.getNumGeometries(); gi++) {
			Geometry geom = gc.getGeometryN(gi);
			mpl.addSPolygon(geom2SPoly(geom));
		}
		return mpl;
	}

	/**
	 * Creates a buffer around the ShpRecord given in argument and updates this
	 * ShpRecord's MPolygon with the result.
	 * 
	 * @param sr
	 *            The ShpRecord used to calculate the buffer
	 * @param distance
	 *            Buffer distance
	 */
	public void setMPolygonBuffer(ShpRecord sr, double distance) {
		GeometryCollection gc = (GeometryCollection) sr.feature
				.getDefaultGeometry();
		Geometry[] geometries = new Geometry[gc.getNumGeometries()];
		for (int gi = 0; gi < gc.getNumGeometries(); gi++) {
			geometries[gi] = gc.getGeometryN(gi).buffer(distance);
		}
		GeometryCollection ngc = new GeometryCollection(geometries,
				new GeometryFactory());

		// Ces deux façons de mettre une geometry dans feature semblent
		// "applatir"
		// le multipolygon (qui contient 3 polygons) en un mutltipolygon qui
		// n'en
		// contient plus qu'un seul réunissant les coordonnées des trois !!
		// PB à creuser ...
		// feature.setDefaultGeometry(ngc);
		feature.setAttribute("the_geom", ngc);
	}

	/**
	 * Gives a new MPolygon with its own set of coordinates.
	 * 
	 * @param mpol
	 *            The new MPolygon to assign to this ShpRecord
	 */
	public void setMPolygon(MPolygon mpol) {
		feature.setDefaultGeometry(mpol.asGeometry());
	}

	/**
	 * Creates a copy of this ShpRecord in the case of a Polyline record.
	 * 
	 * @see getCloneSP
	 */
	public ShpRecord getClone() {
		SimpleFeatureImpl nsf = new SimpleFeatureImpl(feature.getAttributes(),
				feature.getFeatureType(), new FeatureIdImpl("clone_"
						+ feature.getIdentifier()));
		ShpRecord nsc = new ShpRecord(nsf);
		if (nsc.isPolyline())
			nsc.setPolyline(this.getPolyline());
		else if (nsc.isSPolygon())
			nsc.setMPolygon(this.getMPolygon());
		return nsc;
	}

	/**
	 * @return The SimpleFeature associated to this ShpRecord
	 */
	public SimpleFeature getFeature() {
		return this.feature;
	}

	/**
	 * @return A SimpleFeatureType describing the SimpleFeature associated to
	 *         this ShpRecord
	 */
	public SimpleFeatureType getType() {
		return feature.getType();
	}
}
