package camera;

import com.buglabs.application.AbstractServiceTracker;
import com.buglabs.application.MainApplicationThread;

import java.awt.Color;
import java.awt.Frame;
import java.awt.Image;
import java.awt.Label;
import java.awt.Rectangle;
import java.io.File;
import java.awt.Toolkit;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.*;

import com.buglabs.bug.base.pub.ITimeProvider;
import com.buglabs.bug.base.pub.IBaseAudioPlayer;
import com.buglabs.bug.module.camera.pub.ICameraButtonEventProvider;
import com.buglabs.bug.module.camera.pub.ICameraDevice;
import com.buglabs.bug.module.camera.pub.ICameraModuleControl;
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
import com.buglabs.device.ButtonEvent;
import com.buglabs.device.IButtonEventListener;
import com.buglabs.device.IButtonEventProvider;

import org.apache.sanselan.formats.tiff.write.TiffOutputSet;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;

/**
 * CameraApplication Main application thread. The run method is invoked 
 * by the applications service tracker when all services are accounted for.
 *
 */
public class CameraApplication
	extends MainApplicationThread
	implements
		IButtonEventListener {
	// TODO: Shouldn't need this...
	private static final boolean VIRTUAL_BUG = false;
	
	private static final Rectangle PREVIEW_BOUNDS = new Rectangle(30, 15, 260, 195);
	private static final Rectangle STATUS_BOUNDS = new Rectangle(30, 210, 140, 30);
	private static final Rectangle MODE_BOUNDS = new Rectangle(170, 210, 120, 30);
	private static final int BG_COLOR = 0xB61C00; // the red of the Bug matchbox theme
	private static final String REVIEW_MODE_LABEL = "Image";
	private static final String SLIDESHOW_LABEL = "Slide";
	private static final long SLIDESHOW_INTERVAL = 5000;
	
	// TODO: Make these use the Command pattern instead of cheesey ints
	private static final int TOGGLE_MODE_COMMAND = 1;
	private static final int TAKE_A_PICTURE_COMMAND = 2;
	private static final int NEXT_IMAGE_COMMAND = 3;
	private static final int PREV_IMAGE_COMMAND = 4;
	private static final int TOGGLE_SLIDESHOW = 5;
	
	private final SimpleDateFormat timestampFormat= new SimpleDateFormat("yyyyMMdd-HHmmss");
	private final AbstractServiceTracker serviceTracker;
	private boolean ran;
	private Frame frame;
	private PreviewPanel previewPanel;
	private Label statusArea;
	private Label modeArea;
	private LogService log;
	private Store store;
	private ICameraDevice iCameraDevice;
	private ICameraButtonEventProvider iCameraButtonEventProvider;
	private IButtonEventProvider baseIButtonEventProvider;
	private ITimeProvider iTimeProvider;
	private IBaseAudioPlayer iBaseAudioPlayer;
	private File shutterSound;
	private Toolkit toolkit;
	private LivePreviewControl livePreviewControl;
	private boolean inReviewMode = false; // start in live mode
	private List commands = new LinkedList();
	private int displayedImageIndex = -1;
	private ICameraModuleControl iCameraModuleControl;
	private boolean isSlideshowActive = false;
	private Timer slideshowTimer;
	private ExifDataHandler exifDataHandler;
	
	public CameraApplication(AbstractServiceTracker serviceTracker) {
		this.serviceTracker = serviceTracker;
		ran = false;
	}
	
	/**
	 * Informs the caller whether this thread ran.
	 */
	public boolean getRan() {
		return ran;
	}
	
	private void ran() {
		ran = true;
	}
	
	/**
     * This method is invoked as a result of all services
     * becoming available for the application. The list of services is
     * obtained from the getServices() method.
     */
	public void run() {
		log = getLogService();
		this.store = new Store(serviceTracker.getBundleContext(), getLogService());
		
		// get all our service handles
		iCameraModuleControl = getICameraModuleControl();
		iCameraButtonEventProvider = getICameraButtonEventProvider();
		baseIButtonEventProvider = getIButtonEventProvider();
		iCameraDevice = getICameraDevice();
		iTimeProvider = getITimeProvider();
		iBaseAudioPlayer = getIBaseAudioPlayer();
		toolkit = Toolkit.getDefaultToolkit();
		exifDataHandler = new ExifDataHandler(log, serviceTracker);

		try
		{
			final String TEMP_FILENAME = "/tmp/camera_shutter.wav";
			final URL url = serviceTracker.getBundleContext().getBundle().getResource("resources/shutter.wav");
			final InputStream shutterInputStream = url.openStream();
			final OutputStream shutterOutputStream = new FileOutputStream(TEMP_FILENAME);
			int c;
			while ((c = shutterInputStream.read()) != -1) {
				shutterOutputStream.write(c);
			}
			shutterOutputStream.close();
			shutterSound = new File(TEMP_FILENAME);
		}
		catch (IOException e) {
			log.log(LogService.LOG_ERROR, "Unable to read shutter wave file: " + e.getMessage());
			shutterSound = null;
		}
		
		// open our kimono
		showUI();
		
		// assume here that the frame (that is, the ImagePanel's parent) is
		// on-screen from 0,0 (since LivePreviewControl is going to do its
		// thing outside the control of AWT).
		livePreviewControl = new LivePreviewControl(log, iCameraDevice);
		
		// start with live preview running
		livePreviewControl.enableLivePreview();
		
		
		// now we're all set up, let the events commence
		iCameraButtonEventProvider.addListener(this);
		baseIButtonEventProvider.addListener(this);
		
		log.log(LogService.LOG_INFO, "Running: CameraApplication");

		//Main application loop the run method will commence
		//once all service dependencies are satisfied.
		Integer command = null;
		while(!tearDownRequested) {
			while ((command = nextCommand()) != null) {
				log.log(LogService.LOG_DEBUG, "Processing command " + command);
				
				switch (command.intValue()) {
				case TOGGLE_MODE_COMMAND:
					toggleMode();
					break;
					
				case TAKE_A_PICTURE_COMMAND:
					takeAPicture();
					break;
					
				case NEXT_IMAGE_COMMAND:
					nextImage();
					break;
					
				case PREV_IMAGE_COMMAND:
					prevImage();
					break;
					
				case TOGGLE_SLIDESHOW:
					toggleSlideshow();
					break;
					
				default:
					log.log(LogService.LOG_ERROR, "Unrecognized command " + command);
					break;
				}
				
				// sleep for a short time in case the user is mashing
				// buttons, so that we can deal with them all at before
				// displaying an image
				try {
					Thread.sleep(500);
				}
				catch (InterruptedException e) {
				}
			}
		
			// try and avoid thrashing on user mashing the prev/next buttons,
			// but not actually displaying an image until they let go, and 
			// then only if it was different
			if (inReviewMode) {
				displayReviewImage();
			}
			
			try {
				Thread.sleep(60000);
			}
			catch (InterruptedException e) {
				log.log(LogService.LOG_INFO, "Interrupted");
			}
		}
		
		stopSlideshow();
		destroyUI();
		iCameraButtonEventProvider.removeListener(this);
		iCameraButtonEventProvider = null;
		iCameraModuleControl = null;
		iCameraDevice = null;
		toolkit = null;
		log.log(LogService.LOG_INFO, "CameraApplication stopped");	
		
		/**
		 * Let the service tracker know we ran.
		 */
		ran();
	}
	
	/**
     * Provides a list of service names that this application depends on.
     *
     */
	public List getServices() {
		List services = new ArrayList();
		services.add("com.buglabs.bug.module.camera.pub.ICameraButtonEventProvider");
		services.add("com.buglabs.bug.module.camera.pub.ICameraDevice");
		services.add("com.buglabs.bug.module.camera.pub.ICameraModuleControl");
		services.add("com.buglabs.bug.module.lcd.pub.IModuleDisplay");
		services.add("com.buglabs.bug.base.pub.ITimeProvider"); 
		services.add("com.buglabs.device.IButtonEventProvider");
		
		if (!VIRTUAL_BUG) {
			services.add("org.osgi.service.log.LogService");
			services.add("com.buglabs.bug.base.pub.IBaseAudioPlayer");
		}
		return services;
	}

	/**
	 * Queries the service provider for ICameraButtonEventProvider.
	 *
	 * @return a handle to the a(n) ICameraButtonEventProvider service.
	 */
	private ICameraButtonEventProvider getICameraButtonEventProvider() {
		return (ICameraButtonEventProvider) serviceTracker.getService(ICameraButtonEventProvider.class);
	}

	/**
	 * Queries the service provider for ICameraDevice.
	 *
	 * @return a handle to the a(n) ICameraDevice service.
	 */
	private ICameraDevice getICameraDevice() {
		return (ICameraDevice) serviceTracker.getService(ICameraDevice.class);
	}
	
	/**
	 * Queries the service provider for ICameraModuleControl.
	 *
	 * @return a handle to the a(n) ICameraModuleControl service.
	 */
	private ICameraModuleControl getICameraModuleControl() {
		return (ICameraModuleControl) serviceTracker.getService(ICameraModuleControl.class);
	}

	/**
	 * Queries the service provider for IModuleDisplay.
	 *
	 * @return a handle to the a(n) IModuleDisplay service.
	 */
	private IModuleDisplay getIModuleDisplay() {
		return (IModuleDisplay) serviceTracker.getService(IModuleDisplay.class);
	}


	/**
	 * Queries the service provider for ITimeProvider.
	 *
	 * @return a handle to the a(n) ITimeProvider service.
	 */
	private ITimeProvider getITimeProvider() {
		return (ITimeProvider) serviceTracker.getService(ITimeProvider.class);
	}
	
	/**
	 * Queries the service provider for IButtonEventProvider (ie the base buttons).
	 *
	 * @return a handle to the a(n) IButtonEventProvider service.
	 */
	private IButtonEventProvider getIButtonEventProvider() {
		return (IButtonEventProvider) serviceTracker.getService(IButtonEventProvider.class);
	}
	
	/**
	 * Queries the service provider for IBaseAudioPlayer.
	 *
	 * @return a handle to the a(n) IBaseAudioPlayer service.
	 */
	private IBaseAudioPlayer getIBaseAudioPlayer() {
		return (IBaseAudioPlayer) serviceTracker.getService(IBaseAudioPlayer.class);
	}
	
	/**
	 * Queries the service provider for LogService.
	 *
	 * @return a handle to the a(n) LogService service.
	 */
	private LogService getLogService() {
		if (VIRTUAL_BUG) {
			return new LogService() {
				public void log(int level, String message) {
					System.out.println(message);
				}

				public void log(int level, String message, Throwable exception) {
					System.out.println(message);
				}

				public void log(ServiceReference sr, int level, String message) {
					System.out.println(message);
				}

				public void log(ServiceReference sr, int level, String message,
						Throwable exception) {
					System.out.println(message);
				}
			};
		}
		else {
			return (LogService) serviceTracker.getService(LogService.class);
		}
	}
	
	/**
	 * Build the frame that is our UI and show it.
	 */
	private void showUI() {		
		frame = getIModuleDisplay().getFrame();
		
		// set the title so the Matchbox switcher will show something useful
		frame.setTitle("Camera");
		
		// we're going it along on layout, as we have external
		// forces (the JNI-triggered live preview) that we need
		// absolute positioning to work with properly.
		frame.setLayout(null);
		System.out.println("Frame bounds: " + frame.getBounds());
		frame.setBackground(new Color(BG_COLOR));
		
		statusArea = new Label();
		statusArea.setBounds(STATUS_BOUNDS);
		statusArea.setBackground(Color.LIGHT_GRAY);
		statusArea.setAlignment(Label.LEFT);
		frame.add(statusArea);
		
		statusArea.setVisible(inReviewMode);
		
		modeArea = new Label();
		modeArea.setBounds(MODE_BOUNDS);
		modeArea.setBackground(Color.LIGHT_GRAY);
		modeArea.setAlignment(Label.RIGHT);
		modeArea.setText(REVIEW_MODE_LABEL);
		frame.add(modeArea);
		
		modeArea.setVisible(inReviewMode);
		
		previewPanel = new PreviewPanel(log);
		previewPanel.setBounds(PREVIEW_BOUNDS);
		previewPanel.setBackground(Color.BLACK);
		frame.add(previewPanel);
		
		previewPanel.setVisible(inReviewMode);

		log.log(LogService.LOG_DEBUG, "Showing frame now");
		frame.show();
	}

	/**
	 * Clean up the stuff what we created
	 */
	private void destroyUI() {
		frame.dispose();
		frame = null;
		previewPanel = null;
		statusArea = null;
		modeArea = null;
	}
	
	/*
	 * Capture events that were sent from the camera
	 * 
	 * @see com.buglabs.device.IButtonEventListener#buttonEvent(com.buglabs.device.ButtonEvent)
	 */
	public void buttonEvent(ButtonEvent event) {
		// note that this method is called on the event provider's context,
		// so we can't do time-intensive work in here. For example, calling
		// takeAPicture here instead of waking the main app thread up to
		// do it prevents the screen from being updated until this call,
		// and hence our call to takeAPicture, finishes.
		final int value = event.getRawValue();
		log.log(LogService.LOG_DEBUG, "raw event: " + event.getRawValue());

		if (event.getAction() == ButtonEvent.KEY_UP) {
			if (value == ButtonEvent.BUTTON_CAMERA_ZOOM_OUT) {
				// TODO: implement zoom out
				log.log(LogService.LOG_DEBUG, "ZOOM OUT Pressed");
			}
			
			if (value == ButtonEvent.BUTTON_CAMERA_ZOOM_IN) {
				// TODO: implement zoom in
				log.log(LogService.LOG_DEBUG, "ZOOM IN Pressed");
			}
			
			if (value == ButtonEvent.BUTTON_CAMERA_SHUTTER) {
				log.log(LogService.LOG_DEBUG, "SHUTTER Pressed");

				if (!inReviewMode) {
					// wake up our main loop, which will make it take a picture
					// (we can't take one here as we're in the event loop thread
					// and it takes too long)
					queueCommand(TAKE_A_PICTURE_COMMAND);
				}
			}
			
			if (value == ButtonEvent.BUTTON_HOTKEY_1) {
				log.log(LogService.LOG_DEBUG, "HOTKEY 1 Pressed");
				
				if (inReviewMode) {
					queueCommand(TOGGLE_SLIDESHOW);
				}
				else if (VIRTUAL_BUG) {
					// overload HK1 on virtual bug in picture mode to act as shutter
					queueCommand(TAKE_A_PICTURE_COMMAND);					
				}
			}
			
			if (value == ButtonEvent.BUTTON_HOTKEY_2) {
				log.log(LogService.LOG_DEBUG, "HOTKEY 2 Pressed");
				
				if (inReviewMode) {
					queueCommand(PREV_IMAGE_COMMAND);
				}
			}
			
			if (value == ButtonEvent.BUTTON_HOTKEY_3) {
				log.log(LogService.LOG_DEBUG, "HOTKEY 3 Pressed");
				
				if (inReviewMode) {
					queueCommand(NEXT_IMAGE_COMMAND);
				}
			}
			
			if (value == ButtonEvent.BUTTON_HOTKEY_4) {
				log.log(LogService.LOG_DEBUG, "HOTKEY 4 Pressed");
				queueCommand(TOGGLE_MODE_COMMAND);
			}
		}
	}
	
	private void makeShutterSound() {
		if ((shutterSound != null) && (iBaseAudioPlayer != null)) {
			try {
				iBaseAudioPlayer.play(new FileInputStream(shutterSound));
			} catch (IOException e) {
				log.log(LogService.LOG_WARNING, "Ignoring error playing shutter sound: " + e.getMessage());
			}
		}
	}
	
	private void takeAPicture() {
		log.log(LogService.LOG_DEBUG, "takeAPicture");
		
		makeShutterSound();
		
		final byte [] imageData;
		
		try {
			iCameraModuleControl.setLEDFlash(true);
			imageData = iCameraDevice.getImage();
			iCameraModuleControl.setLEDFlash(false);
		} catch (IOException e) {
			e.printStackTrace();
			log.log(LogService.LOG_ERROR, "Error playing with flash: " + e.getMessage());
			return;
		}
		
		final Date timestamp = iTimeProvider.getTime();
		final TiffOutputSet exifData = exifDataHandler.createExifData(timestamp);	
		final StringBuffer sb = new StringBuffer();
		timestampFormat.format(timestamp, sb, new FieldPosition(0));
		
		store.saveImage(imageData, sb.toString(), exifData);
	}

	private void toggleMode() {
		log.log(LogService.LOG_DEBUG, "toggleMode");
		if (inReviewMode) {
			if (isSlideshowActive) {
				stopSlideshow();
			}
			
			log.log(LogService.LOG_INFO, "switching to live mode");
			statusArea.setVisible(false);
			modeArea.setVisible(false);
			previewPanel.setVisible(false);
			livePreviewControl.enableLivePreview();
			// make sure we'll redisplay on the way back
			displayedImageIndex = -1;
		}
		else {
			log.log(LogService.LOG_INFO, "switching to review mode");
			livePreviewControl.disableLivePreview();
			statusArea.setVisible(true);
			modeArea.setVisible(true);
			previewPanel.setVisible(true);
			store.refresh();
			displayReviewImage();
		}
		
		inReviewMode = !inReviewMode;
	}
	
	private void displayReviewImage() {
		log.log(LogService.LOG_DEBUG, "displayReviewImage");
		final File file = store.getImageFile();
		if (file == null) {
			statusArea.setText("no saved images");
			updateModeInfo();
		}
		else if (displayedImageIndex == store.getImageIndex()) {
			// no need to redisplay it
		}
		else {
			displayedImageIndex = store.getImageIndex();
			
			final byte [] imageData = new byte[(int) file.length()];
			try {
				final FileInputStream in = new FileInputStream(file);
				in.read(imageData);
				final Image image = toolkit.createImage(imageData);
				previewPanel.setImage(image);
				statusArea.setText(file.getName());
				updateModeInfo();
				displayedImageIndex = store.getImageIndex();
			}
			catch (IOException e) {
				e.printStackTrace();
				statusArea.setText("unable to read " + file.getName());
			}
		}
	}

	private void updateModeInfo() {
		if (store.getImageCount() == 0) {
			modeArea.setText("empty");
		}
		else {
			modeArea.setText((isSlideshowActive ? SLIDESHOW_LABEL : REVIEW_MODE_LABEL) + 
					" " + (store.getImageIndex()+1) +
					" of " + store.getImageCount());		
		}
	}
	
	private void prevImage() {
		store.prevImage();
		// update the info, but don't actually
		// display the new image yet; we do that
		// when we reach quiescence to avoid unnecessary
		// chewing of cycles and memory
		updateModeInfo();
	}

	private void nextImage() {
		store.nextImage();
		// update the info, but don't actually
		// display the new image yet; we do that
		// when we reach quiescence to avoid unnecessary
		// chewing of cycles and memory
		updateModeInfo();
	}
	
	private synchronized void queueCommand(int command) {
		commands.add(new Integer(command));
		
		// wake up our main loop (in run) to process the command
		interrupt();
	}
	
	private synchronized Integer nextCommand() {
		if (commands.isEmpty()) {
			return null;
		}
		
		final Integer command = (Integer) commands.get(0);
		commands.remove(0);
		return command;
	}
	
	private void toggleSlideshow() {		
		if (isSlideshowActive) {
			stopSlideshow();
		}
		else {
			startSlideshow();
		}
	}
	
	private void startSlideshow() {
		log.log(LogService.LOG_DEBUG, "Starting slideshow");
		isSlideshowActive = true;
		slideshowTimer = new Timer();
		slideshowTimer.scheduleAtFixedRate(new TimerTask() {
			public void run() {
				if (CameraApplication.this.inReviewMode) {
					CameraApplication.this.nextImage();
					CameraApplication.this.displayReviewImage();
				}
			}},
			0, SLIDESHOW_INTERVAL);
	}
	
	private void stopSlideshow() {
		log.log(LogService.LOG_DEBUG, "Stopping slideshow");
		
		isSlideshowActive = false;
		if (slideshowTimer != null) { 
			slideshowTimer.cancel();
			slideshowTimer = null;
			updateModeInfo();
		}
	}
}