package org.geotools.data.ogr;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.gdal.ogr.FeatureDefn;
import org.gdal.ogr.FieldDefn;
import org.gdal.ogr.ogr;
import org.geotools.data.DataSourceException;
import org.geotools.feature.AttributeType;
import org.geotools.feature.Feature;
import org.geotools.feature.FeatureType;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.feature.type.GeometricAttributeType;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKBReader;
import com.vividsolutions.jts.io.WKBWriter;
import com.vividsolutions.jts.io.WKTReader;
import com.vividsolutions.jts.io.WKTWriter;

/**
 * Maps OGR features into Geotools ones, and vice versa. Chances are that if you
 * need to update a decode method a simmetric modification will be needed in the
 * encode method. This class is not thread safe, so each thread should create
 * its own instance.
 * 
 * @author aaime
 * 
 */
class FeatureMapper {

	/**
	 * From ogr_core.h, the byte order constants
	 */
	static final int WKB_XDR = 1;

	/**
	 * Enables usage of WKB encoding for OGR/Java Geometry conversion. At the
	 * time of writing, it cannot be used because it'll bring the virtual
	 * machine down (yes, a real crash...)
	 */
	static final boolean USE_WKB = true;

	GeometryFactory geomFactory;

	WKBReader wkbReader;

	WKTReader wktReader;

	WKBWriter wkbWriter;

	WKTWriter wktWriter;

	/**
	 * The date time format used by OGR when getting/setting times using strings
	 */
	DateFormat dateTimeFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");

	DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");

	DateFormat timeFormat = new SimpleDateFormat("hh:mm:ss");

	public FeatureMapper(GeometryFactory geomFactory) {
		this.geomFactory = geomFactory;
		if (USE_WKB) {
			this.wkbReader = new WKBReader(geomFactory);
			this.wkbWriter = new WKBWriter();
		} else {
			this.wktReader = new WKTReader(geomFactory);
			this.wktWriter = new WKTWriter();
		}
	}

	/**
	 * Converts an OGR feature into a GeoTools one
	 * 
	 * @param schema
	 * @param ogrFeature
	 * @return
	 * @throws IOException
	 * @throws IllegalAttributeException
	 */
	Feature convertOgrFeature(FeatureType schema, org.gdal.ogr.Feature ogrFeature)
			throws IOException, IllegalAttributeException {
		// Extract all attributes (do not assume any specific order, the feature
		// type may have been re-ordered by the Query)
		Object[] attributes = new Object[schema.getAttributeCount()];

		// .. then extract each attribute using the attribute type to determine
		// which extraction method to call
		for (int i = 0; i < attributes.length; i++) {
			AttributeType at = schema.getAttributeType(i);
			if (at instanceof GeometricAttributeType) {
				org.gdal.ogr.Geometry ogrGeometry = ogrFeature.GetGeometryRef();
				try {
					attributes[i] = fixGeometryType(parseOgrGeometry(ogrGeometry), at);
				} finally {
					ogrGeometry.delete();
				}
			} else {
				attributes[i] = getOgrField(at, ogrFeature);
			}
		}

		// .. gather the FID
		String fid = convertOGRFID(schema, ogrFeature);

		// .. finally create the feature
		return schema.create(attributes, fid);
	}

	/**
	 * Turns a GeoTools feature into an OGR one
	 * 
	 * @param feature
	 * @return
	 * @throws DataSourceException
	 */
	org.gdal.ogr.Feature convertGTFeature(FeatureDefn ogrSchema, Feature feature)
			throws IOException {
		// create a new empty OGR feature
		org.gdal.ogr.Feature result = new org.gdal.ogr.Feature(ogrSchema);

		// go thru GeoTools feature attributes, and convert
		FeatureType schema = feature.getFeatureType();
		Object[] attributes = feature.getAttributes(new Object[schema.getAttributeCount()]);
		for (int i = 0, j = 0; i < attributes.length; i++) {
			AttributeType at = schema.getAttributeType(i);
			if (at instanceof GeometricAttributeType) {
                // using setGeoemtryDirectly the feature becomes the owner of the generated
                // OGR geometry and we don't have to .delete() it (it's faster, too) 
				result.SetGeometryDirectly(parseGTGeometry((Geometry) attributes[i]));
				continue;
			}

			if (attributes[i] == null) {
				result.UnsetField(j);
			} else {
                final FieldDefn ogrField = ogrSchema.GetFieldDefn(j);
                final int ogrType = ogrField.GetFieldType();
                ogrField.delete();
                if(ogrType == ogr.OFTInteger)
                    result.SetField(j, ((Number) attributes[i]).intValue());
                else if(ogrType == ogr.OFTReal)
                    result.SetField(j, ((Number) attributes[i]).doubleValue());
                else if (ogrType == ogr.OFTDateTime)
					result.SetField(j, dateTimeFormat.format((java.util.Date) attributes[i]));
				else if (ogrType == ogr.OFTDate)
					result.SetField(j, dateFormat.format((java.util.Date) attributes[i]));
				else if (ogrType == ogr.OFTTime)
					result.SetField(j, timeFormat.format((java.util.Date) attributes[i]));
				else
					result.SetField(j, attributes[i].toString());
			} 
			j++;
		}

		return result;
	}

	/**
	 * Turns line and polygon into multiline and multipolygon. This is a
	 * stop-gap measure to make things works against shapefiles, I've asked the
	 * GDAL mailing list on how to properly handle this in the meantime
	 * 
	 * @param ogrGeometry
	 * @param at
	 * @return
	 */
	Geometry fixGeometryType(Geometry ogrGeometry, AttributeType at) {
		if (MultiPolygon.class.equals(at.getType())) {
			if (ogrGeometry instanceof MultiPolygon)
				return ogrGeometry;
			else
				return geomFactory.createMultiPolygon(new Polygon[] { (Polygon) ogrGeometry });
		} else if (MultiLineString.class.equals(at.getType())) {
			if (ogrGeometry instanceof MultiLineString)
				return ogrGeometry;
			else
				return geomFactory
						.createMultiLineString(new LineString[] { (LineString) ogrGeometry });
		}
		return ogrGeometry;

	}

	/**
	 * Reads the current feature's geometry using wkb encoding. A wkbReader
	 * should be provided since it's not thread safe by design.
	 * 
	 * @throws IOException
	 */
	Geometry parseOgrGeometry(org.gdal.ogr.Geometry geom) throws IOException {
		// Extract the geometry using either WKT or WKB. Rationale: the SWIG
		// bindings do not provide subclasses. Even if they did, going thru the
		// JNI barrier often is expensive, so it's better to gather the geometry
		// is a single call
		if (USE_WKB) {
			int wkbSize = geom.WkbSize();
			// the gdal interface uses a char* type, maybe because in C it's
			// unsigned and has
			// the same size as a byte, unfortunately this means we have to
			// unpack it
			// to byte format by doing bit masking and shifting
			byte[] byteBuffer = new byte[wkbSize];
			geom.ExportToWkb(byteBuffer, WKB_XDR);
			try {
				Geometry g = wkbReader.read(byteBuffer);
				return g;
			} catch (ParseException pe) {
				throw new DataSourceException(
						"Could not parse the current Geometry in WKB format.", pe);
			}
		} else {
			String[] stringArray = new String[1];
			geom.ExportToWkt(stringArray);
			try {
				return wktReader.read(stringArray[0]);
			} catch (ParseException pe) {
				throw new DataSourceException(
						"Could not parse the current Geometry in WKB format.", pe);
			}
		}
	}

	org.gdal.ogr.Geometry parseGTGeometry(Geometry geometry) throws DataSourceException {
		final org.gdal.ogr.Geometry ogrGeom;
		if (USE_WKB) {
			byte[] wkb = wkbWriter.write(geometry);
			ogrGeom = ogr.CreateGeometryFromWkb(wkb, null);
			if (ogrGeom == null)
				throw new DataSourceException(
						"Could not turn JTS geometry into an OGR one thought WKB");
		} else {
			String wkt = wktWriter.write(geometry);
			ogrGeom = ogr.CreateGeometryFromWkt(wkt, null);
			if (ogrGeom == null)
				throw new DataSourceException(
						"Could not turn JTS geometry into an OGR one thought WKT");
		}
		return ogrGeom;
	}

	/**
	 * Reads the current feature's specified field using the most appropriate
	 * OGR field extraction method
	 * 
	 * @param at
	 * @return
	 */
	Object getOgrField(AttributeType at, org.gdal.ogr.Feature ogrFeature) throws IOException {
		String name = at.getName();
		Class clazz = at.getType();

		// check for null fields
		if (!ogrFeature.IsFieldSet(name))
			return null;

		// hum, ok try and parse it
		if (clazz.equals(String.class)) {
			return ogrFeature.GetFieldAsString(name);
		} else if (clazz.equals(Integer.class)) {
			return new Integer(ogrFeature.GetFieldAsInteger(name));
		} else if (clazz.equals(Double.class)) {
			return new Double(ogrFeature.GetFieldAsDouble(name));
		} else if (clazz.equals(Float.class)) {
			return new Float(ogrFeature.GetFieldAsDouble(name));
		} else if (clazz.equals(Integer.class)) {
			return new Integer(ogrFeature.GetFieldAsInteger(name));
		} else if (clazz.equals(java.util.Date.class)) {
			String date = ogrFeature.GetFieldAsString(name);
			if (date == null || date.trim().equals(""))
				return null;
			int ogrType = ogrFeature.GetFieldType(name);
			try {
				if (ogrType == ogr.OFTDateTime)
					return dateTimeFormat.parse(date);
				else if (ogrType == ogr.OFTDate)
					return dateFormat.parse(date);
				else if (ogrType == ogr.OFTTime)
					return timeFormat.parse(date);
			} catch (java.text.ParseException e) {
				throw new DataSourceException("Could not parse date value", e);
			}
			throw new IOException("Date attribute, but field type is not compatible: " + ogrType);
		} else {
			throw new IllegalArgumentException("Don't know how to read " + clazz.getName()
					+ " fields");
		}
	}

	/**
	 * Generates a GT2 feature id given its feature type and an OGR feature
	 * 
	 * @param schema
	 * @param ogrFeature
	 * @return
	 */
	String convertOGRFID(FeatureType schema, org.gdal.ogr.Feature ogrFeature) {
		return schema.getTypeName() + "." + ogrFeature.GetFID();
	}

	/**
	 * Decodes a GT2 feature id into an OGR one
	 * 
	 * @param feature
	 * @return
	 */
	int convertGTFID(Feature feature) {
		String id = feature.getID();
		return Integer.parseInt(id.substring(id.indexOf(".") + 1));
	}

}
