package projectEQ2440.message;

import projectEQ2440.QRcode.Decoder;
import projectEQ2440.QRcode.decode.Reader;
import projectEQ2440.camera.PictureManager;
import projectEQ2440.camera.PreviewPicture;
import projectEQ2440.utils.Queue;
import projectEQ2440.utils.ThreadManager;

/**
 * <b>public class MessageManager extends ThreadManager</b><br/>
 * Class treating the decoded messages to reunify them, using the frame protocol developed for the project 
 */
public class MessageManager extends ThreadManager {
	
	// The decoder
	private Decoder decoder;
	
	// The message queue
	private Queue<MessageInQueue> messages;
	
	// The message manager callback
	private MessageManagerCallback callback;
	
	// List of saved good message
	private byte[][] goodMessages;
	private int lengthGoodMessages;
	
	// Number of verified message
	private int nbrVerified;
	
	// Time of last flash
	private long lastFlash;
	
	// Set a frame callback
	private boolean withFrameCheck;
	
	/**
	 * <b>public MessageManager(Decoder decoder)</b><br/>
	 * Constructor initializing the queue of message and set the <i>MessageCallback</i> of the <i>Decoder</i>
	 * 
	 * @param decoder : the <i>Decoder</i>
	 */
	public MessageManager(Decoder decoder) {
		super("MessageManager");
		
		this.decoder = decoder;
		
		this.callback = null;
		
		messages = new Queue<MessageInQueue>();
		
		goodMessages = null;
		lengthGoodMessages = 0;
		nbrVerified = 0;
		
		this.decoder.setMessageCallback(new Decoder.MessageCallback() {
			@Override
			public void messageReady(byte[] data, int version, PreviewPicture picture) {
				if (picture == null) return;
				
				if (callback == null) {
					picture.destroyPicture();
					return;
				}
				
				if (!isStart()) {
					callback.messageOutOfLoop(data, version);
					picture.destroyPicture();
					return;
				}
				
				messages.add(new MessageInQueue(data, version, picture.getInfo()));
				MessageManager.this.wakeManager();

				picture.destroyPicture();
				
			}
		});
		
		withoutFrameCheck();
	}
	
	/**
	 * <b>public boolean isThereFrameCheck()</b><br/>
	 * tell if the checking frame Number is activated or not
	 * 
	 * @return it checks frame number
	 */
	public boolean isThereFrameCheck() {
		return withFrameCheck;
	}
	
	/**
	 * <b>public void withFrameCheck()</b><br/>
	 * Set the checking of frame number on
	 */
	public void withFrameCheck() {
		this.decoder.setReaderFrameCallback(new Reader.ReaderFrameCallback() {
			@Override
			public boolean check(int currentFrame) {
				if (lengthGoodMessages == 0) return true;
				if (currentFrame >= lengthGoodMessages || currentFrame < 0) return false;
				if (goodMessages == null) return false;
				return goodMessages[currentFrame] == null;
			}
		});
	}
	
	/**
	 * <b>public void withoutFrameCheck()</b><br/>
	 * Set the checking of frame number off
	 */
	public void withoutFrameCheck() {
		this.decoder.setReaderFrameCallback(new Reader.ReaderFrameCallback() { });
	}
	
	/**
	 * <b>private void good(byte[] data, int length, int totalFrame, int frameNumber, int version, PreviewPicture.Info info)</b><br/>
	 * Treating of a good message
	 * 
	 * @param data : the message
	 * @param length : the length read in QR code standard format
	 * @param totalFrame : the total number of frame read define in the project protocol
	 * @param frameNumber : the number of the actual frame read define in the project protocol
	 * @param version : the version of the QR code
	 * @param info : the Informations about the picture
	 */
	private void good(byte[] data, int length, int totalFrame, int frameNumber, int version, PreviewPicture.Info info) {
		if (goodMessages[frameNumber] == null) {
			goodMessages[frameNumber] = new byte[length-2];
			
			int k, start;
			if (version < 10) start = 4;
			else start = 5;
			for (k=0; k<length-2; k++) goodMessages[frameNumber][k] = data[k+start];
			
			nbrVerified++;
			lengthGoodMessages = totalFrame;
			
			if (info.color == PreviewPicture.Info.BW && callback != null) {
				callback.flash();
				lastFlash = System.currentTimeMillis();
			} else if (info.color == PreviewPicture.Info.RGB && nbrVerified%3 == 0 && callback != null) {
				callback.flash();
				lastFlash = System.currentTimeMillis();
			}
		} else if (info.time-lastFlash > 2*PictureManager.TIME_MAX_BETWEEN_PICTURES)
			if (info.color == PreviewPicture.Info.BW && callback != null) {
				callback.flash();
				lastFlash = System.currentTimeMillis();
			} else if (info.color == PreviewPicture.Info.RGB && nbrVerified%3 == 0 && callback != null) {
				callback.flash();
				lastFlash = System.currentTimeMillis();
			}
		
		if (callback != null) {
			callback.goodFrame(data, length, totalFrame, frameNumber, version);
			callback.state(lengthGoodMessages, nbrVerified, frameNumber);
		}
		
		if (nbrVerified == lengthGoodMessages && lengthGoodMessages > 0) stopManager();
	}
	
	// TODO comments
	private void wrong(byte[] data, int version) {
		if (callback != null) {
			callback.wrongFrame(data, version);
			callback.state(lengthGoodMessages, nbrVerified, -1);
		}
	}
	
	/**
	 * <b>protected void initAction() throws InterruptedException</b><br/>
	 * Initialization of the manager and cleaning the queue
	 */
	@Override
	protected void initAction() throws InterruptedException {
		messages.clean();
		nbrVerified = 0;
		goodMessages = new byte[255][];
		lengthGoodMessages = 0;
		lastFlash = System.currentTimeMillis();
	}
	
	/**
	 * <b>protected void loopAction() throws InterruptedException</b><br/>
	 * Treatment  of the next message
	 */
	@Override
	protected void loopAction() throws InterruptedException {
		
		MessageInQueue message = messages.pop();
		
		if (message == null) return;
		
		byte[] data = message.message;
		int version = message.version;
		
		int length = 0;
		int marker = 0;
		int totalFrame = 0;
		int frameNumber = 0;
		
		if (data == null) wrong(data, version);
		
		else if (message.version < 10) {
			if (data.length < 4) wrong(data, version);
			else {
				marker = (data[0]&0xF0)|(data[1]&0x0F);
				if (marker != 74) wrong(data, version);
				else {
					length = ((data[0]&0x0F)<<4)|((data[1]&0xF0)>>>4);
					if (data.length < 2+length) wrong(data, version);
					else {
						totalFrame = data[2]&0xFF;
						frameNumber = data[3]&0xFF;
						if (frameNumber >= totalFrame) wrong(data, version);
						else good(data, length, totalFrame, frameNumber, version, message.info);
					}
				}
			}
			
		} else {
			if (data.length < 5) wrong(data, version);
			else {
				marker = (data[0]&0xF0)|(data[2]&0x0F);
				if (marker != 74) wrong(data, version);
				else {
					length = ((data[0]&0x0F)<<12)|((data[1]&0xFF)<<4)|((data[2]&0xF0)>>>4);
					if (data.length < 3+length) wrong(data, version);
					else {
						totalFrame = data[3]&0xFF;
						frameNumber = data[4]&0xFF;
						if (frameNumber >= totalFrame) wrong(data, version);
						else good(data, length, totalFrame, frameNumber, version, message.info);
					}
				}
			}
		}
	}
	
	/**
	 * <b>protected void endAction() throws InterruptedException</b><br/>
	 * Treating of the end of decoding, call the callback with the final message if it exists and clean the queue
	 */
	@Override
	protected void endAction() throws InterruptedException {
		
		if (nbrVerified == lengthGoodMessages && lengthGoodMessages > 0) {
			int totalLength = 0;
			int k,l,m;
			for (k=0; k<lengthGoodMessages; k++) totalLength += goodMessages[k].length;
			
			byte[] finalMessage = new byte[totalLength];
			m = 0;
			for (k=0; k<lengthGoodMessages; k++)
				for (l=0; l<goodMessages[k].length; l++) {
					finalMessage[m] = goodMessages[k][l];
					m++;
				}	
			
			if (callback != null) callback.messageComplete(finalMessage);
		}
		goodMessages = null;
		messages.clean();
	}

	/**
	 * <b>public void setMessageManagerCallback(MessageManagerCallback callback)</b><br/>
	 * Set the callback to interact with the returning messages  
	 * 
	 * @param callback : the <i> MessageManagerCallback</i>
	 */
	public void setMessageManagerCallback(MessageManagerCallback callback) {
		this.callback = callback;
	}
	
	/**
	 * <b>public interface MessageManagerCallback</b><br/>
	 * the interface to implement the callback on receive messages
	 */
	public interface MessageManagerCallback {
		
		/**
		 * <b>public void goodFrame(byte[] data, int length, int totalFrame, int frameNumber, int version)</b><br/>
		 * Callback for a good message receive in the manager
		 * 
		 * @param data : the data read in the QR code
		 * @param length : the length read in the standard of QR code
		 * @param totalFrame : the total number of frame read
		 * @param frameNumber : the number of the actual frame read
		 * @param version : the version of the QR code read
		 */
		public void goodFrame(byte[] data, int length, int totalFrame, int frameNumber, int version);
		
		/**
		 * <b>public void wrongFrame(byte[] data, int version)</b><br/>
		 * Callback for a wrong message receive in the manager
		 * 
		 * @param data : the data read in the QR code (can be null if wrong QR code)
		 * @param version : the version of the QR code ( can be 0 if wrong QR code)
		 */
		public void wrongFrame(byte[] data, int version);
		
		/**
		 * <b>public void messageComplete(byte[] data)</b><br/>
		 * Callback for the final message transmitted
		 * 
		 * @param data : the complete message
		 */
		public void messageComplete(byte[] data);
		
		/**
		 * <b>public void messageOutOfLoop(byte[] data, int version)</b><br/>
		 * Callback for a message receive when the manager isn't running
		 * 
		 * @param data : the data read in the QR code (can be null if wrong QR code)
		 * @param version : the version of the QR code ( can be 0 if wrong QR code)
		 */
		public void messageOutOfLoop(byte[] data, int version);
		
		/**
		 * <b>public void state(int totalFrame, int decodedFrame, int currentFrame)</b><br/>
		 * general callback call to know the state of decoding
		 * 
		 * @param totalFrame : total number of frame
		 * @param decodedFrame : number of decoded frame
		 * @param currentFrame : the current frame
		 */
		public void state(int totalFrame, int decodedFrame, int currentFrame);
		
		/**
		 * <b>public void flash()</b>
		 * the callback for the flash between frames
		 */
		public void flash();
	}
	
	/**
	 * <b>private class MessageInQueue</b><br/>
	 * Class managing the informations in the element in the queue
	 */
	private class MessageInQueue {
		
		/**
		 * <b>public byte[] message</b><br/>
		 * the message
		 */
		public byte[] message;
		
		/**
		 * <b>public int version</b><br/>
		 * The version
		 */
		public int version;
		
		/**
		 * <b>public int info</b><br/>
		 * Info about the picture
		 */
		public PreviewPicture.Info info;
		
		/**
		 * <b>public MessageInQueue(byte[] message, int version, int color)</b><br/>
		 * Constructor for initialization
		 * 
		 * @param message : the message
		 * @param version : the version
		 * @param info : Info about the picture
		 */
		public MessageInQueue(byte[] message, int version, PreviewPicture.Info info) {
			this.message = message;
			this.version = version;
			this.info = info;
		}
		
	}
}
