import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ListIterator;

import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureStore;
import org.geotools.data.FileDataStoreFinder;
import org.geotools.data.Transaction;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.map.DefaultMapContext;
import org.geotools.map.MapContext;
import org.geotools.referencing.CRS;
import org.geotools.swing.JMapFrame;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;

import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;

import fr.ocelet.runtime.List;


/**
 * Datafacer specialized in ESRI shapefile geographic data manipulation
 * 
 * @author pdegenne@teledetection.fr
 */
public class ShapeFile {

	private final String ERR_HEADER = "Datafacer ShapeFile: ";
	public final int ATTYPE_TEXT = 1;
	public final int ATTYPE_REAL = 2;
	public final int ATTYPE_INT = 3;
	public final int ATTYPE_BOOLEAN = 4;
	public final int ATTYPE_POINT = 5;
	public final int ATTYPE_LINE = 6;
	public final int ATTYPE_POLYGON = 7;
	private SimpleFeatureSource featureSource = null;
	private CoordinateReferenceSystem crs;
	private SimpleFeatureCollection featureCollection;
	private SimpleFeatureIterator iterator;
	private List<AttrDesc> featuresSchema;
	private SimpleFeatureType sft;
	private File sourceFile;
	private ShapefileDataStore store;

	/**
	 * Empty constructor. Nothing special.
	 */
	public ShapeFile() {
	}

	/**
	 * Initialize and prepares the ShapeFile using the file name given in
	 * argument. The file is not read at this point, but it's availability is
	 * being checked. An error message is printed in case of initialization
	 * problem.
	 * 
	 * @since mai 2012 This same method is used to create a new Shapefile which
	 *        is created if the name given in argument doesn't already exist on
	 *        the disk.
	 * 
	 * @param shpFileName
	 *            Name of the .shp file
	 */
	public void setFileName(String shpFileName) {
		sourceFile = new File(shpFileName);
		if (sourceFile.exists()) { // It does, we expect to be able to read it
			try {
				store = (ShapefileDataStore) FileDataStoreFinder.getDataStore(sourceFile);
				featureSource = store.getFeatureSource();
			} catch (IOException e) {
				System.out.println(ERR_HEADER + "Failed to open the shapefile "
						+ sourceFile);
				// e.printStackTrace();
			}
		}
	}

	/**
	 * Adds a ShpRecord to the list of records contained in this Shapefile
	 * 
	 * @param sr
	 *            The ShpRecord to be added to the list
	 */
	public void addShpRecord(ShpRecord sr) {
		if (featureCollection == null) {
			sft = sr.getType();
			featureCollection = new ListFeatureCollection(sft);
			try {
				store.createSchema(sft);
			} catch (IOException e) {
				System.out.println(ERR_HEADER
						+ "Failed to write a record into the shapefile "
						+ sourceFile);
			}
		}
		featureCollection.add(sr.getFeature());
	}

	/**
	 * Save this Shapefile on disk, in Shapefile format (!). The name used is
	 * the one given in argument. Warning : if the file does already exist, it
	 * will be overwritten !
	 * 
	 * @param filename
	 * @return True if the file was successfully saved
	 */
	public boolean saveAsShp(String filename) {
		boolean result = true;
		try {
			File shpf = new File(filename);
			ShapefileDataStore newShapefileDataStore = new ShapefileDataStore(
					shpf.toURI().toURL());

			// create the schema using from the original shapefile
			if (sft == null) sft = store.getSchema();
			newShapefileDataStore.createSchema(sft);

			// grab the data source from the new shapefile data store

            FeatureSource newFeatureSource = newShapefileDataStore.getFeatureSource(sft.getName());
			
			// downcast FeatureSource to specific implementation of FeatureStore
            
			FeatureStore newFeatureStore = (FeatureStore) newFeatureSource;

			// accquire a transaction to create the shapefile from FeatureStore
			Transaction t = newFeatureStore.getTransaction();

			newFeatureStore.addFeatures(featureCollection);

			// filteredReader is now exhausted and closed, commit the changes
			t.commit();
			t.close();
		} catch (MalformedURLException e) {
			System.out.println(ERR_HEADER + "Failed to write the shapefile "
					+ filename + " something is wrong with the file's name");
			result = false;
		} catch (IOException e) {
			System.out.println(ERR_HEADER + "Failed to write the shapefile "
					+ filename);
			System.out.println("cause :"+e.getMessage());
			e.printStackTrace();
			result = false;
		}

		return result;
	}

	/**
	 * Allow the definition of a attribute which is added to the attribute list.
	 * Be careful to always respect the order in which you create the
	 * attributes. That order will then be kept in the shapefile. This method
	 * has a state that will evolve at every call. It is not Thread safe.
	 * 
	 * @param attrType
	 *            Type is one of the predefined types : ATTR_TEXT, ...
	 *            ,ATTR_POLYGON
	 * @param attrName
	 *            Name of the attribute. We suggest "the_geom" for geometry
	 *            attributes (but you are free to choose anything else).
	 */
	public void defineAttribute(int attrType, String attrName) {
		if (featuresSchema == null)
			featuresSchema = new List<AttrDesc>();
		AttrDesc ad = new AttrDesc();
		ad.name = attrName;
		switch (attrType) {
		case ATTYPE_TEXT:
			ad.cl = String.class;
			break;
		case ATTYPE_REAL:
			ad.cl = Double.class;
			break;
		case ATTYPE_INT:
			ad.cl = Integer.class;
			break;
		case ATTYPE_BOOLEAN:
			ad.cl = Boolean.class;
			break;
		case ATTYPE_POINT:
			ad.cl = MultiPoint.class;
			break;
		case ATTYPE_LINE:
			ad.cl = MultiLineString.class;
			break;
		case ATTYPE_POLYGON:
			ad.cl = MultiPolygon.class;
			break;
		}
		featuresSchema.add(ad);
	}

	/**
	 * This is a way to close the defineAttribute process by giving a name to
	 * the feature Schema that has been defined.
	 * 
	 * @param name
	 *            For the feature schema
	 */
	public void buildSchema(String name) {
		sft = createFeatureType(name, featuresSchema);
		featureCollection = new ListFeatureCollection(sft);
	}

	/**
	 * 
	 * @return A newly created empty feature that complies with the Schema that
	 *         must have been defined earlier by a series of calls to
	 *         defineAttribute() and one optionnal call to buildSchema() (it is called
	 *          here if it has not been before)
	 */
	private SimpleFeature getNewFeature() {
		if (sft == null) buildSchema("anonymousSchema");
		return createFeature(sft);
	}

	public void defineTextAttribute(String attrName) {
		defineAttribute(ATTYPE_TEXT, attrName);
	}

	public void defineRealAttribute(String attrName) {
		defineAttribute(ATTYPE_REAL, attrName);
	}

	public void defineIntAttribute(String attrName) {
		defineAttribute(ATTYPE_INT, attrName);
	}

	public void definePointAttribute(String attrName) {
		defineAttribute(ATTYPE_POINT, attrName);
	}

	public void defineLineAttribute(String attrName) {
		defineAttribute(ATTYPE_LINE, attrName);
	}

	public void definePolygonAttribute(String attrName) {
		defineAttribute(ATTYPE_POLYGON, attrName);
	}

	public void defineBooleanAttribute(String attrName) {
		defineAttribute(ATTYPE_BOOLEAN, attrName);
	}

	public ShpRecord createEmptyShpRecord() {
		SimpleFeature nsf = getNewFeature();
		featureCollection.add(nsf);
		return new ShpRecord(nsf);
	}

	private SimpleFeatureType createFeatureType(String ftName,
			List<AttrDesc> schema) {

		SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
		builder.setName(ftName);
		if (crs != null)
			builder.setCRS(crs);

		// add attributes in order
		for (ListIterator<AttrDesc> it = schema.listIterator(); it.hasNext();) {
			AttrDesc ad = it.next();
			builder.add(ad.name, ad.cl);
		}
		SimpleFeatureType result = builder.buildFeatureType();
		return result;
	}

	private SimpleFeature createFeature(SimpleFeatureType ftype) {
		SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(ftype);
		SimpleFeature feature = featureBuilder.buildFeature(null);
		return feature;
	}

	/**
	 * Initializes the coordinate system of this ShapeFile.
	 * 
	 * @param epsgCode
	 *            The coordinate system in text format. Ex: "EPSG:4326"
	 */
	public void setCrsEPSG(String epsgCode) {
		try {
			crs = CRS.decode(epsgCode);
		} catch (NoSuchAuthorityCodeException e) {
			System.out.println(ERR_HEADER + "Unknown EPSG code : " + epsgCode);
		} catch (FactoryException e) {
			System.out.println(ERR_HEADER
					+ "Failed to build the coordinate system :" + epsgCode);
			e.printStackTrace();
		}
	}

	/**
	 * Reads and displays this ShapeFile in a window
	 */
	public void view() {
		MapContext map = new DefaultMapContext();
		map.setTitle("Shape file viewer");
		map.addLayer(featureSource, null);
		JMapFrame.showMap(map);
	}

	/**
	 * Checks if there is any more record that has not yet been read
	 * 
	 * @return true is there are more records to be read
	 */
	public boolean hasNextRecord() {
		if (iterator == null) {
			try {
				if (featureCollection == null)
					featureCollection = featureSource.getFeatures();
				iterator = featureCollection.features();
			} catch (IOException e) {
				System.out
						.println(ERR_HEADER
								+ "Problem while attempting to read the shapefile's content.");
			}
		}
		return iterator.hasNext();
	}

	/**
	 * Returns the next record read from the source shapefile.
	 * 
	 * @return A ShpRecord, or null if all records have been read.
	 */
	public ShpRecord getNextRecord() {
		ShpRecord nextRecord = null;
		if (hasNextRecord()) {
			SimpleFeature feature = iterator.next();
			nextRecord = new ShpRecord(feature);
		} else {
			iterator.close();
			iterator = null;
		}
		return nextRecord;
	}

	/**
	 * Creates a transformation operation from the coordinates system of this
	 * ShapeFile to the coordinate system provided in argument
	 * 
	 * @param dstCrs
	 *            Target Crs in textual form "EPSG:num" ex : "EPSG:4326"
	 * @return MathTransform A ready to use transformation. Or null if anything
	 *         went wrong
	 */
	public MathTransform getTransformCrs(String dstCrs) {
		MathTransform mt = null;
		try {
			CoordinateReferenceSystem destCRS = CRS.decode(dstCrs);
			mt = CRS.findMathTransform(crs, destCRS, true);
		} catch (NoSuchAuthorityCodeException e) {
			// e.printStackTrace();
			System.out.println(ERR_HEADER + "Unrecognized coordinate system :"
					+ dstCrs);
		} catch (FactoryException e) {
			// e.printStackTrace();
			System.out.println(ERR_HEADER
					+ "Failed to build the coordinate system " + dstCrs);
		}
		return mt;
	}

	class AttrDesc {
		public String name;
		public Class cl;
	}

}
