package org.geotools.data.ogr;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.Arrays;
import java.util.Vector;

import org.gdal.gdal.gdal;
import org.gdal.ogr.DataSource;
import org.gdal.ogr.Driver;
import org.gdal.ogr.FeatureDefn;
import org.gdal.ogr.FieldDefn;
import org.gdal.ogr.Layer;
import org.gdal.ogr.ogr;
import org.gdal.osr.SpatialReference;
import org.geotools.data.AbstractDataStore;
import org.geotools.data.DataSourceException;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Query;
import org.geotools.data.Transaction;
import org.geotools.factory.FactoryConfigurationError;
import org.geotools.feature.AttributeType;
import org.geotools.feature.AttributeTypeFactory;
import org.geotools.feature.FeatureType;
import org.geotools.feature.FeatureTypes;
import org.geotools.feature.GeometryAttributeType;
import org.geotools.feature.SchemaException;
import org.geotools.feature.type.BasicFeatureTypes;
import org.geotools.referencing.CRS;
import org.opengis.filter.Filter;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
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.Point;
import com.vividsolutions.jts.geom.Polygon;

/**
 * A datastore based on the the <a href="http://www.gdal.org/ogr">OGR</a>
 * spatial data abstraction library
 * 
 * @author aaime
 */
public class OGRDataStore extends AbstractDataStore {
	/** C compatible FALSE */
	static final int FALSE = 0;

	/** C compatible TRUE */
	static final int TRUE = 1;

	/**
	 * The OGRwkbGeometryType enum from ogr_core.h, reduced to represent only 2D
	 * classes (I hope the 2.5D will be handled transparently by JTS)
	 */
	static final Class[] OGR_GEOM_TYPES = new Class[] { //
	Geometry.class, // wkbUnknown = 0
			Point.class, // wkbPoint = 1
			MultiLineString.class, // wkbLineString = 2,
			MultiPolygon.class, // wkbPolygon = 3,
			MultiPoint.class, // wkbMultiPoint = 4,
			MultiLineString.class, // wkbMultiLineString = 5,
			MultiPolygon.class, // wkbMultiPolygon = 6,
			GeometryCollection.class, // wkbGeometryCollection = 7
	};

	/**
	 * The source name that OGR should open and handle
	 */
	private String ogrSourceName;

	/**
	 * Datastore namespace
	 */
	private URI namespace;

	/**
	 * OGR driver to be used for the creation of new data sources
	 */
	private String ogrDriverName;

	static {
		// perform OGR format registration once
		if (ogr.GetDriverCount() == 0)
			ogr.RegisterAll();
	}

	/**
	 * Creates a new OGRDataStore
	 * 
	 * @param ogrSourceName
	 *            a references to the source that needs to be opened. May be a
	 *            file system path, or a database reference. See the OGR driver
	 *            documentation for valid formats of this string.
	 */

	public OGRDataStore(String ogrSourceName, String ogrDriverName, URI namespace)
			throws IOException {
		this.ogrSourceName = ogrSourceName;
		this.ogrDriverName = ogrDriverName;
		this.namespace = (namespace != null) ? namespace : FeatureTypes.DEFAULT_NAMESPACE;
		int update = FALSE;
		if (ogrDriverName == null) {
			DataSource ds = getOGRDataSource(update);
			ds.delete();
		}
	}

	protected FeatureReader getFeatureReader(String typeName) throws IOException {
		return getFeatureReader(typeName, false);
	}

	protected FeatureReader getFeatureReader(String typeName, boolean openForUpdate)
			throws IOException {
		DataSource ds = getOGRDataSource(openForUpdate ? TRUE : FALSE);
		Layer layer = getOGRLayer(ds, typeName);
		FeatureType schema = getSchema(typeName);
		return new OGRFeatureReader(ds, layer, schema);
	}

	public FeatureType getSchema(String typeName) throws IOException {
		DataSource ds = getOGRDataSource(FALSE);
		Layer layer = getOGRLayer(ds, typeName);
		if (layer == null) {
			ds.delete();
			throw new IOException("No such type : " + typeName);
		}
		FeatureDefn featureDef = null;
		try {
			featureDef = layer.GetLayerDefn();
			AttributeType[] types = new AttributeType[featureDef.GetFieldCount() + 1];

			// handle the geometry
			CoordinateReferenceSystem crs = getCRS(layer);
			Class geomClass = getGeometryClass(featureDef.GetGeomType());
			types[0] = AttributeTypeFactory.newAttributeType("the_geom", geomClass, true, 0, null,
					crs);

			// compute a default parent feature type
			Class geomType = types[0].getType();
			FeatureType parent = null;
			if ((geomType == Point.class) || (geomType == MultiPoint.class)) {
				parent = BasicFeatureTypes.POINT;
			} else if ((geomType == Polygon.class) || (geomType == MultiPolygon.class)) {
				parent = BasicFeatureTypes.POLYGON;
			} else if ((geomType == LineString.class) || (geomType == MultiLineString.class)) {
				parent = BasicFeatureTypes.LINE;
			}

			// handle the other fields
			for (int i = 1; i < types.length; i++) {
				FieldDefn fd = featureDef.GetFieldDefn(i - 1);
				types[i] = AttributeTypeFactory.newAttributeType(fd.GetNameRef(),
						getFieldClass(fd), true, fd.GetWidth());
				fd.delete();
			}

			// finally build the geometry type
			return FeatureTypes.newFeatureType(types, layer.GetName(), namespace, false,
					parent != null ? new FeatureType[] { parent } : null);
		} catch (FactoryException e) {
			throw new DataSourceException("Could not determine geometry SRS", e);
		} catch (FactoryConfigurationError e) {
			throw new DataSourceException("Could not create feature type", e);
		} catch (SchemaException e) {
			throw new DataSourceException("Could not create feature type", e);
		} finally {
			if (featureDef != null)
				featureDef.delete();
			if (layer != null)
				layer.delete();
			if (ds != null)
				ds.delete();
		}

	}

	public String[] getTypeNames() throws IOException {
		DataSource ds;
		try {
			ds = getOGRDataSource(FALSE);
		} catch (IOException e) {
			return new String[0];
		}
		String[] typeNames = new String[ds.GetLayerCount()];
		for (int i = 0; i < typeNames.length; i++) {
			Layer l = ds.GetLayerByIndex(i);
			typeNames[i] = l.GetName();
			l.delete();
		}
		ds.delete();
		return typeNames;
	}

	protected int getCount(Query query) throws IOException {
		if (!Filter.INCLUDE.equals(query.getFilter()))
			return -1;

		DataSource ds = getOGRDataSource(FALSE);
		Layer l = getOGRLayer(ds, query.getTypeName());
		if (l == null)
			throw new IOException("Unknown feature type: " + query.getTypeName());

		// go for the quick computation, return -1 otherwise
		int count = l.GetFeatureCount(FALSE);
		l.delete();
		ds.delete();
		return count;
	}

	protected FeatureWriter createFeatureWriter(String typeName, Transaction transaction)
			throws IOException {
		if (supportsInPlaceWrite(typeName)) {
			OGRFeatureReader reader = (OGRFeatureReader) getFeatureReader(typeName, true);
			return new OGRDirectFeatureWriter(reader);
		} else {
			throw new UnsupportedOperationException(
					"This file format does not support in place write, "
							+ "can't perform updates/deletes");
		}
	}

	public boolean supportsInPlaceWrite(String typeName) throws IOException {
		// if it's not able to open the datasource in update mode, there's no
		// change it'll be able to support in place write (no need to throw an
		// exception in
		// this case, thus the direct call instead of getOGRDataSource())
		DataSource ds = ogr.OpenShared(ogrSourceName, TRUE);
		if (ds == null)
			return false;
		Layer l = getOGRLayer(ds, typeName);
		boolean retval = l.TestCapability(ogr.OLCDeleteFeature)
				&& l.TestCapability(ogr.OLCRandomWrite) && l.TestCapability(ogr.OLCSequentialWrite);
		l.delete();
		ds.delete();
		return retval;
	}

	// given up on this one. Some drivers tell you that you can write on a layer
	// only if you opened in update mode. We could try to create a new data
	// source,
	// but unfortunately there is no way to have a generic ogr name that works
	// for every data store. Finally, not all drivers that do write do support
	// datastore deletion. So, in the end, either we can update directly an OGR
	// datasource content, or we have to create a new one and roll our own
	// mechanism to have it replace the old one (this includes moving and
	// deleting
	// the elements that make up a data store).
	public boolean supportsWriteNewLayer(String typeName) throws IOException {
		DataSource ds = getOGRDataSource(FALSE);
		Layer l = getOGRLayer(ds, typeName);
		boolean retval = ds.TestCapability(ogr.ODsCCreateLayer)
				&& l.TestCapability(ogr.OLCSequentialWrite);
		l.delete();
		ds.delete();
		return retval;
	}

	public Envelope getBounds(Query q) throws IOException {
		if (!q.getFilter().equals(Filter.INCLUDE))
			return null;

		DataSource ds = getOGRDataSource(OGRDataStore.FALSE);
		Layer l = getOGRLayer(ds, q.getTypeName());
		if (l.TestCapability(ogr.OLCFastGetExtent)) {
			double[] bbox = new double[4];
			// TODO: hum... going thru this forcing the computation is anyways
			// faster than loading all the features just for the sake of
			// getting out the bbox.... but we don't have this
			// explicit middle ground in Geotools
			l.GetExtent(bbox, OGRDataStore.FALSE);
			// TODO: return a ReferencedEnvelope?
			return new Envelope(bbox[0], bbox[1], bbox[2], bbox[3]);
		} else {
			return null;
		}
	}

	public void createSchema(FeatureType schema) throws IOException {
		// TODO: add a field to allow approximate definitions
		createSchema(schema, false, null);
	}

	/**
	 * Creates a new OGR layer with provided schema and options
	 * 
	 * @param schema
	 *            the geotools schema
	 * @param approximateFields
	 *            if true, OGR will try to create fields that are approximations
	 *            of the required ones when an exact match cannt be provided
	 * @param options
	 *            OGR data source/layer creation options
	 * @throws IOException
	 */
	public void createSchema(FeatureType schema, boolean approximateFields, String[] options)
			throws IOException {
		DataSource ds = null;
		Layer l = null;

		try {
			// either open datasource, or try creating one
			Vector optVector = options != null ? new Vector(Arrays.asList(options)) : null;
			try {
				ds = getOGRDataSource(TRUE);
			} catch (IOException e) {
				if (ogrDriverName != null) {
					Driver d = ogr.GetDriverByName(ogrDriverName);
					ds = d.CreateDataSource(ogrSourceName, optVector);
					d.delete();

					if (ds == null)
						throw new IOException("Could not create OGR data source with driver "
								+ ogrDriverName + " and options " + optVector);
				} else {
					throw new DataSourceException("Driver not provided, and could not "
							+ "open data source neither");
				}
			}

			// get the spatial reference corresponding to the default geometry
			GeometryAttributeType geomType = schema.getDefaultGeometry();
			int ogrGeomType = getOGRGeometryType(geomType);
			SpatialReference sr = null;
			if (geomType.getCoordinateSystem() != null) {
				String wkt = geomType.getCoordinateSystem().toString();
				sr = new SpatialReference(null);
				if (sr.ImportFromWkt(wkt) != 0) {
					sr = null;
					LOGGER.warning("OGR could not parse the geometry WKT," + " detailed error is: "
							+ gdal.GetLastErrorMsg() + "\n" + "WKT was: " + wkt);
				}
			}

			// create the layer
			l = ds.CreateLayer(schema.getTypeName(), sr, ogrGeomType, optVector);
			if (l == null) {
				throw new DataSourceException("Could not create the OGR layer: "
						+ gdal.GetLastErrorMsg());
			}

			// create fields
			for (int i = 0; i < schema.getAttributeCount(); i++) {
				AttributeType at = schema.getAttributeType(i);
				if (at == schema.getDefaultGeometry())
					continue;

				FieldDefn definition = getOGRFieldDefinition(at);
				l.CreateField(definition, approximateFields ? TRUE : FALSE);
			}
		} finally {
			if (l != null) {
				l.delete();
			}
			if (ds != null)
				ds.delete();
		}
	}

	// ---------------------------------------------------------------------------------------
	// PRIVATE SUPPORT METHODS
	// ---------------------------------------------------------------------------------------

	private FieldDefn getOGRFieldDefinition(AttributeType at) throws IOException {
		final Class type = at.getType();
		final FieldDefn def;
		// set type, width, precision and justification where:
		// * width is the number of chars needed to format the strings
		// equivalent of
		// the number
		// * precision is the number of chars after decimal pont
		// * justification: right or left (in outputs)
		// TODO: steal code from Shapefile data store to guess eventual size
		// limitations
		if (Boolean.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTString);
			def.SetWidth(5);
		} else if (Byte.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTInteger);
			def.SetWidth(3);
			def.SetJustify(ogr.OJRight);
		} else if (Short.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTInteger);
			def.SetWidth(5);
			def.SetJustify(ogr.OJRight);
		} else if (Integer.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTInteger);
			def.SetWidth(9);
			def.SetJustify(ogr.OJRight);
		} else if (Long.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTInteger);
			def.SetWidth(19);
			def.SetJustify(ogr.OJRight);
		} else if (BigInteger.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTInteger);
			def.SetWidth(32);
			def.SetJustify(ogr.OJRight);
		} else if (BigDecimal.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTReal);
			def.SetWidth(32);
			def.SetPrecision(15);
			def.SetJustify(ogr.OJRight);
		} else if (Float.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTReal);
			def.SetWidth(12);
			def.SetPrecision(7);
			def.SetJustify(ogr.OJRight);
		} else if (Double.class.equals(type) || Number.class.isAssignableFrom(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTReal);
			def.SetWidth(22);
			def.SetPrecision(16);
			def.SetJustify(ogr.OJRight);
		} else if (String.class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTString);
			def.SetWidth(255);
			// TODO: do a serious attempt to cover blob and clob too
		} else if (byte[].class.equals(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTBinary);
//		} else if (java.sql.Date.class.isAssignableFrom(type)) {
//			def = new FieldDefn(at.getName(), ogr.OFTDate);
//		} else if (java.sql.Time.class.isAssignableFrom(type)) {
//			def = new FieldDefn(at.getName(), ogr.OFTTime);
		} else if (java.util.Date.class.isAssignableFrom(type)) {
			def = new FieldDefn(at.getName(), ogr.OFTDateTime);
		} else {
			throw new IOException("Cannot map " + type + " to an OGR type");
		}

		return def;
	}

	/**
	 * Tries to open the specified source in either read only or read/write mode
	 * 
	 * @param update
	 *            open read/write if TRUE, otherwise open read only
	 * 
	 * @return
	 * @throws IOException
	 */
	DataSource getOGRDataSource(int update) throws IOException {
		DataSource ds = ogr.OpenShared(ogrSourceName, update);
		if (ds == null)
			throw new IOException("OGR could not open '" + ogrSourceName + "'");
		return ds;
	}

	/**
	 * Internal utility method, returns a layer if available, otherwise closes
	 * the provided datasource and throws and exception stating the feature type
	 * is not available
	 * 
	 * @param ds
	 * @param typeName
	 * @return
	 * @throws IOException
	 */
	Layer getOGRLayer(DataSource ds, String typeName) throws IOException {
		Layer l = ds.GetLayerByName(typeName);
		if (l == null) {
			ds.delete();
			throw new IOException("No feature type " + typeName);
		}
		return l;
	}

	/**
	 * Turns an ogrFieldType into the best corresponding class we can figure out
	 * 
	 * @param ogrFieldType
	 * @return
	 */
	private Class getFieldClass(FieldDefn field) {
		final int ogrFieldType = field.GetFieldType();
		final int width = field.GetWidth();
		if (ogrFieldType < 0)
			throw new IllegalArgumentException("Can't have a negative type");
		if (ogrFieldType == ogr.OFTInteger) {
			if (width <= 9)
				return Integer.class;
			else if (width <= 19)
				return Long.class;
			else
				return BigDecimal.class;
		} else if (ogrFieldType == ogr.OFTReal) {
			if (width < 13)
				return Float.class;
			else
				return Double.class;
		} else if (ogrFieldType == ogr.OFTDate || ogrFieldType == ogr.OFTTime || ogrFieldType == ogr.OFTDateTime) {
//			return java.sql.Date.class;
//		} else if (ogrFieldType == ogr.OFTTime) {
//			return java.sql.Time.class;
//		} else if (ogrFieldType == ogr.OFTDateTime) {
			return java.util.Date.class;
		} else {
			return String.class;
		}
	}

	/**
	 * Turns an OGR {@link SpatialReference} object into a
	 * {@link CoordinateReferenceSystem} one (using WKT parsing)
	 * 
	 * @param reference
	 * @return
	 * @throws FactoryException
	 */
	private CoordinateReferenceSystem getCRS(Layer layer) throws FactoryException {
		SpatialReference reference = layer.GetSpatialRef();
		if (reference == null)
			return null;

		String[] wkt = new String[1];
		reference.ExportToWkt(wkt);
		reference.delete();
		return CRS.parseWKT(wkt[0]);
	}

	/**
	 * Turns a wkbGeometryType into the best corresponding geometry class we can
	 * figure out
	 * 
	 * @param wkbGeometryType
	 * @return
	 */
	private Class getGeometryClass(int wkbGeometryType) {
		if (wkbGeometryType < 0)
			throw new IllegalArgumentException("Can't have a negative type");
		int mask25d = 0x80000000;
		int unmaskedType = wkbGeometryType & ~mask25d;
		if (wkbGeometryType >= OGR_GEOM_TYPES.length) {
			LOGGER.warning("Could not recognize geometry type " + wkbGeometryType
					+ ". Assuming it's a new type and handling as a string");
			return Geometry.class;
		}
		return OGR_GEOM_TYPES[unmaskedType];
	}

	/**
	 * Returns the OGR geometry type constant ginve a geometry attribute type
	 * 
	 * @param type
	 * @return
	 * @throws IOException
	 */
	private int getOGRGeometryType(GeometryAttributeType type) throws IOException {
		for (int i = 0; i < OGR_GEOM_TYPES.length; i++) {
			if (type.getType().equals(OGR_GEOM_TYPES[i]))
				return i;
		}
		throw new IOException("Could not map " + type.getType() + " to an OGR geometry type");
	}

}
