package camera;

import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.sanselan.ImageWriteException;
import org.apache.sanselan.common.BinaryConstants;
import org.apache.sanselan.formats.tiff.constants.ExifTagConstants;
import org.apache.sanselan.formats.tiff.constants.TagInfo;
import org.apache.sanselan.formats.tiff.constants.TiffDirectoryConstants.ExifDirectoryType;
import org.apache.sanselan.formats.tiff.write.TiffOutputDirectory;
import org.apache.sanselan.formats.tiff.write.TiffOutputField;
import org.apache.sanselan.formats.tiff.write.TiffOutputSet;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;

import com.buglabs.application.AbstractServiceTracker;
import com.buglabs.bug.module.gps.pub.IPositionProvider;
import com.buglabs.bug.module.gps.pub.LatLon;

public class ExifDataHandler {
	private final static String MANUFACTURER = "Bug Labs";
	private final static String MODEL = "BUGcam2MP";
	// TODO: Image.getWidth() would be nice if it didn't require an observer:
	private final static int IMAGE_WIDTH = 1600; 
	private final static int IMAGE_HEIGHT = 1200;
	private final static int BYTE_ORDER = BinaryConstants.BYTE_ORDER_INTEL;
	private final static SimpleDateFormat timestampFormat= new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
	private final AbstractServiceTracker serviceTracker;
	// The GPS ones aren't all predefined by Sanselan, so create them from the EXIF spec.
	// version id is 2.2.0.0 but we're doing Intel (aka little-endian) so do it backwards:
	private final byte [] GPS_VERSION_ID = new byte[] {(byte)0, (byte)0, (byte)2, (byte)2};
	private final TagInfo gpsVerTagInfo;
	private final TagInfo gpsLatRefTagInfo;
	private final TagInfo gpsLatTagInfo;
	private final TagInfo gpsLonRefTagInfo;
	private final TagInfo gpsLonTagInfo;
	private final LogService log;
	
	public ExifDataHandler(LogService log, AbstractServiceTracker serviceTracker) {
		this.serviceTracker = serviceTracker;
		this.log = log;
		
		final ExifDirectoryType gpsDirectory = new ExifDirectoryType.Special(-3, "GPS IFD");
		gpsVerTagInfo = new TagInfo("GPSVersionID", 0, TiffOutputField.FIELD_TYPE_BYTE, 4,
				gpsDirectory);
		gpsLatRefTagInfo = new TagInfo("GPSLatitudeRef", 1, TiffOutputField.FIELD_TYPE_ASCII, 1,
				gpsDirectory);
		gpsLatTagInfo = new TagInfo("GPSLatitude", 2, TiffOutputField.FIELD_TYPE_RATIONAL, 3,
				gpsDirectory);
		gpsLonRefTagInfo = new TagInfo("GPSLongitudeRef", 3, TiffOutputField.FIELD_TYPE_ASCII, 1,
				gpsDirectory);
		gpsLonTagInfo = new TagInfo("GPSLongitude", 4, TiffOutputField.FIELD_TYPE_RATIONAL, 3,
				gpsDirectory);
	}
	
	public TiffOutputSet createExifData(final Date timestamp) {
		final TiffOutputSet exifData = new TiffOutputSet();
		
		try {
			TiffOutputDirectory rootDirectory = exifData.getOrCreateRootDirectory();
			TiffOutputDirectory exifDirectory = exifData.getOrCreateExifDirectory();
			
			// TiffOuputField.create((TagInfo tagInfo, int byteOrder,
			//   String value) expects it to be FIELD_TYPE_ASCII_DESCRIPTION
			// so it throws an exception; so, do it the hard way...
			final TiffOutputField manufacturer = new TiffOutputField(
					ExifTagConstants.EXIF_TAG_MAKE,
					TiffOutputField.FIELD_TYPE_ASCII,
					MANUFACTURER.length(),
					MANUFACTURER.getBytes());
			rootDirectory.add(manufacturer);
			
			final TiffOutputField model = new TiffOutputField(
					ExifTagConstants.EXIF_TAG_MODEL,
					TiffOutputField.FIELD_TYPE_ASCII,
					MODEL.length(),
					MODEL.getBytes());
			rootDirectory.add(model);
			
			TiffOutputField imageWidth = TiffOutputField.create(
					ExifTagConstants.EXIF_TAG_IMAGE_WIDTH_IFD0,
					BYTE_ORDER,
					new Integer(IMAGE_WIDTH));
			rootDirectory.add(imageWidth);
			
			TiffOutputField imageHeight = TiffOutputField.create(
					ExifTagConstants.EXIF_TAG_IMAGE_HEIGHT_IFD0,
					BYTE_ORDER,
					new Integer(IMAGE_HEIGHT));
			rootDirectory.add(imageHeight);
			
			final StringBuffer sb = new StringBuffer();
			timestampFormat.format(timestamp, sb, new FieldPosition(0));
			final String dateTime = sb.toString();
			
			TiffOutputField dateTimeModified = new TiffOutputField(
					ExifTagConstants.EXIF_TAG_MODIFY_DATE,
					TiffOutputField.FIELD_TYPE_ASCII,
					dateTime.length(),
					dateTime.getBytes());
			rootDirectory.add(dateTimeModified);

			TiffOutputField dateTimeOriginal = new TiffOutputField(
					ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL,
					TiffOutputField.FIELD_TYPE_ASCII,
					dateTime.length(),
					dateTime.getBytes());
			exifDirectory.add(dateTimeOriginal);
			
			TiffOutputField dateTimeDigitized = new TiffOutputField(
					// This is DateTimeDigitized in the Exif 2.2 spec
					ExifTagConstants.EXIF_TAG_CREATE_DATE,
					TiffOutputField.FIELD_TYPE_ASCII,
					dateTime.length(),
					dateTime.getBytes());
			exifDirectory.add(dateTimeDigitized);
			
			addGpsInfo(exifData);
		} catch (ImageWriteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return exifData;
	}
	
	/**
	 * Get IPositionProvider if available
	 * 
	 * @return
	 */
	private IPositionProvider getPositionProvider() {
		ServiceReference sr = serviceTracker.getBundleContext().getServiceReference(
				com.buglabs.bug.module.gps.pub.IPositionProvider.class.getName());
		if (sr == null) return null; 
		return (IPositionProvider) serviceTracker.getBundleContext().getService(sr);
	}
	
	private void addGpsInfo(TiffOutputSet exifData) {
		// GPS Optional
		IPositionProvider iPositionProvider = getPositionProvider();
		if (iPositionProvider == null) {
			log.log(LogService.LOG_WARNING, "No position provider so can't geo tag");
		}
		else {			
			try {
				final TiffOutputDirectory gpsDirectory = exifData.getOrCreateGPSDirectory();
				
				final LatLon latLon = iPositionProvider.getLatitudeLongitude();
				if (latLon == null) {
					log.log(LogService.LOG_WARNING, "No LatLon returned, so can't geo tag");
					return;
				}
				
				// convert lat lon for exif
				double lat = latLon.latitude;
				String latRef = "N";
				if (lat < 0) {
					latRef = "S";
					lat *= -1;
				}
				
				double lon = latLon.longitude;
				String lonRef = "E";
				if (lon < 0) {
					lonRef = "W";
					lon *= -1;
				}
				
				log.log(LogService.LOG_INFO, "geo tagging: " + lat + latRef + "," + lon + lonRef);

				final TiffOutputField verField = new TiffOutputField(
						gpsVerTagInfo,
						TiffOutputField.FIELD_TYPE_BYTE,
						GPS_VERSION_ID.length,
						GPS_VERSION_ID);
				gpsDirectory.add(verField);
				
				final TiffOutputField latRefField = new TiffOutputField(
						gpsLatRefTagInfo,
						TiffOutputField.FIELD_TYPE_ASCII,
						latRef.length(),
						latRef.getBytes());
				gpsDirectory.add(latRefField);
				
				final TiffOutputField latField = TiffOutputField.create(
						gpsLatTagInfo,
						BYTE_ORDER,
						decimalToDegMins(lat));
				gpsDirectory.add(latField);

				final TiffOutputField lonRefField = new TiffOutputField(
						gpsLonRefTagInfo,
						TiffOutputField.FIELD_TYPE_ASCII,
						lonRef.length(),
						lonRef.getBytes());
				gpsDirectory.add(lonRefField);

				final TiffOutputField lonField = TiffOutputField.create(
						gpsLonTagInfo,
						BYTE_ORDER,
						decimalToDegMins(lon));
				gpsDirectory.add(lonField);
			} catch (NumberFormatException e) { // get this from IPositionProvider sometimes
				e.printStackTrace();
			} catch (ImageWriteException e) {
				e.printStackTrace();
			} catch (RuntimeException e) {
				e.printStackTrace();				
			}
		}		
	}
	
	private static Double [] decimalToDegMins(double latOrLon) {
		final double floor = Math.floor(latOrLon);
		
		return new Double [] {
				new Double(floor), 						// degrees
				new Double((latOrLon - floor) * 60),	// minutes
				new Double(0.0) }; 						// seconds
	}
}