package projectEQ2440.QRcode;

import projectEQ2440.QRcode.decode.*;
import projectEQ2440.camera.PreviewPicture;

/**
 * <b>public class Decoder extends Thread</b><br/>
 * Class which manages the decoding of a picture. It also implements the <i>Decoder Thread<i/> for parallel calculation.
 */
public class Decoder extends Thread {

	// States of the decoding
	public static final int KILL = -1;
	public static final int DECODE = 0;
	public static final int NEXT = 1;
	public static final int RGB = 2;
	public static final int RGB_NEXT = 3;
	private int decodeState;
	
	// The binarizer
	private Binarizer binarizer;
	
	// the Reader
	private Reader reader;
	
	// the Interleaver
	private Interleaver interleaver;
	
	// The preview picture to treat
	private PreviewPicture picture;
	private PreviewPicture nextPicture;
	
	// the callback when the message is ready
	private MessageCallback msgCallback;
	
	// the callback when the decoding is finished
	private DecodeCallback decodeCallback;
	
	// The ReaderFrameCallback to check the current frame
	private Reader.ReaderFrameCallback readerFrameCallback;
	
	// The flags for the decoder to run and to wait
	private boolean alive;
	
	/**
	 * <b>public Decoder()</b><br/>
	 * Constructor initializing the decoding
	 */
	public Decoder() {
		binarizer = new Binarizer();
		decodeState = -1;
		reader = null;
		interleaver = null;
		picture = null;
		nextPicture = null;
		msgCallback = null;
		decodeCallback = null;
		readerFrameCallback = null;
		alive = true;
		
		this.setName("Decoder");
		this.setPriority(Thread.MAX_PRIORITY);
		this.start();
	}
	
	/**
	 * <b>public Reader getReader()</b><br/>
	 * gives the <i>Reader</i> used to read the actual QR code
	 * 
	 * @return the actual <i>Reader</i>
	 */
	public Reader getReader() {
		return reader;
	}
	
	/**
	 * <b>public Interleaver getInterleaver()</b><br/>
	 * gives the <i>Interleaver</i> used to decode the actual QR code
	 * 
	 * @return the actual <i>Interleaver</i>
	 */
	public Interleaver getInterleaver() {
		return interleaver;
	}
	
	/**
	 * <b>public void setMessageCallback(MessageCallback msgCallback)</b><br/>
	 * Set the callback for treating the resulting messages<br/>
	 * <i>Needed to begin the decoding</i>
	 * 
	 * @param msgCallback : the <i>MessageCallback</i> for callback
	 */
	public void setMessageCallback(MessageCallback msgCallback) {
		this.msgCallback = msgCallback;
	}
	
	/**
	 * <b>public void setDecodeCallback(DecodeCallback decodeCallback)</b><br/>
	 * Set the callback which tell when the decoding is finish
	 * 
	 * @param decodeCallback : the <i>DecodeCallback</i> for callback
	 */
	public void setDecodeCallback(DecodeCallback decodeCallback) {
		this.decodeCallback = decodeCallback;
	}
	
	/**
	 * <b>public void setReaderFrameCallback(Reader.ReaderFrameCallback readerFrameCallback)</b><br/>
	 * Set the callback for the reader. It checks the frame number before decoding
	 * 
	 * @param readerFrameCallback : The callback for the checking frame
	 */
	public void setReaderFrameCallback(Reader.ReaderFrameCallback readerFrameCallback) {
		this.readerFrameCallback = readerFrameCallback;
	}
	
	/**
	 * <b>public static interface MessageCallback</b><br/>
	 * the interface to implement a callback on the messages
	 */
	public static interface MessageCallback {
		public void messageReady(byte[] data, int version, PreviewPicture picture);
	}
	
	/**
	 * <b>public static interface DecodeCallback</b><br/>
	 * the interface to implement a callback on the end of the decoding
	 */
	public static interface DecodeCallback {
		public void decodeEnd();
	}
	
	/**
	 * <b>public void decodePicture(PreviewPicture picture)</b><br/>
	 * Call the <i>Decoder Thread</i> to decode a new Picture.<br/>
	 * If the function is called several time before the end of the previous decoding, only the last picture will be keep.
	 * 
	 * @param picture : the picture to decode
	 */
	public void decodePicture(PreviewPicture picture) {
		if (decodeState != DECODE) decodeState = DECODE;
		
		this.nextPicture = picture;
		
		wakeUp();
	}
	
	/**
	 * <b>public void decodeNextPicture(PreviewPicture picture)</b><br/>
	 * Call the <i>Decoder Thread</i> to decode a new Picture in a local area define by the previous picture.<br/>
	 * If the previous picture wasn't enough good to detect the QR code, the decoding will be operated in all the picture.<br/>
	 * If the function is called several time before the end of the previous decoding, only the last picture will be keep.
	 * 
	 * @param picture : the picture to decode
	 */
	public void decodeNextPicture(PreviewPicture picture) {
		if (decodeState != NEXT) decodeState = NEXT;
		
		this.nextPicture = picture;

		wakeUp();
	}
	
	/**
	 * <b>public void decodeRGBPicture(PreviewPicture picture)</b><br/>
	 * Call the <i>Decoder Thread</i> to decode a new Picture using the patterns position of the previous picture<br/>
	 * If the previous picture wasn't enough good to detect the patterns of a QR code, the decoding will be operated in all the picture.<br/>
	 * If the function is called several time before the end of the previous decoding, only the last picture will be keep.
	 * 
	 * @param picture : the picture to decode
	 */
	public void decodeRGBPicture(PreviewPicture picture) {
		if (decodeState != RGB) decodeState = RGB;
		
		this.nextPicture = picture;
		
		wakeUp();
	}
	
	// TODO comments
	public void decodeRGBNextPicture(PreviewPicture picture) {
		if (decodeState != RGB_NEXT) decodeState = RGB_NEXT;
		
		this.nextPicture = picture;
		
		wakeUp();
	}
	
	/**
	 * <b>public boolean waiting()</b><br/>
	 * Tell if the <i>Decoder Thread</i> is asleep
	 * 
	 * @return if it is waiting
	 */
	public boolean waiting() {
		return (this.getState() == Thread.State.WAITING);
	}
	
	/**
	 * <b>public void wakeUp()</b><br/>
	 * Awake the <i>Decoder Thread</i> if it is sleeping
	 */
	public void wakeUp() {
		synchronized (this) {
			if (waiting()) notify();
		}
	}
	
	/**
	 * <b>public void kill()</b><br/>
	 * Kill the <i>Decoder Thread</i>.<br/>
	 * <b>BE CAREFUL</b> : After using this function, the <i>Decoder</i> becomes useless
	 */
	public void kill() {
		alive = false;
		decodeState = KILL;
		wakeUp();
		try {
			this.finalize();
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * <b>public void run()</b><br/>
	 * Manage the beginning and the end of the decoding
	 */
	@Override
	public void run() {
		while (alive) {
			try {
				synchronized (this) {
					if (nextPicture == null) wait();
				}
				
				picture = nextPicture;
				nextPicture = null;
				if (msgCallback != null && picture != null) {
					switch (decodeState) {
						case DECODE   : decode();  break;
						case NEXT     : next();    break;
						case RGB      : rgb();     break;
						case RGB_NEXT : rgbNext(); break;
					}
				}
				
				if (decodeCallback != null) decodeCallback.decodeEnd();

			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
		
		msgCallback = null;
		decodeCallback = null;
		
		reader = null;
		interleaver = null;
		picture = null;
		nextPicture = null;
	}
	
	/**
	 * <b>private void decode()</b><br/>
	 * Apply the decoding on all the picture
	 */
	private void decode() {
		// Local area
		int[] around = { 0, 0, picture.getHeight(), picture.getWidth() };
		
		// Binarized picture
		binarizer.thresholdWhite(picture, around);
		boolean[][] bin = binarizer.binarizationWhite(picture, around);
		
		// The reader
		reader = setReader(bin);
		
		// Apply interleaving
		interleave(reader.readMessage());
	}
	
	/**
	 * <b>private void next()</b><br/>
	 * Apply the decoding on a restricted area of the picture based on the previous picture<br/>
	 * If there is no available information on the previous picture <b>decode()</b> is called;
	 */
	private void next() {
		// Find the local area around
		int[] around = new int[4];
		if (!findAround(around)) {
			decode();
			return;
		}
		
		// Binarize the picture
		binarizer.thresholdWhite(picture, around);
		boolean[][] bin = binarizer.binarizationWhite(picture, around);
		
		// The reader
		reader = setReader(bin, around);
		
		// Apply interleaving
		interleave(reader.readMessage());
	}

	/**
	 * <b>private void rgb()</b><br/>
	 * Apply the decoding using the patterns information of the previous picture<br/>
	 * If there is no available information on the previous picture <b>decode()</b> is called;
	 */
	// TODO comments to change
	private void rgb() {
		int[] around = { 0, 0, picture.getHeight(), picture.getWidth() };
		
		// Binarization 
		binarizer.thresholdRGB(picture, around);
		boolean[][][] bin = binarizer.binarizationRGB(picture, around);
		
		// Blue first
		reader = setReader(bin[2]);
		interleave(reader.readMessage());
		
		// Red next
		reader = setReaderRGB(bin[0]);
		interleave(reader.readMessage());
		
		// Green finally
		reader = setReaderRGB(bin[1]);
		interleave(reader.readMessage());
	}
	
	// TODO comments
	private void rgbNext() {
		// Find the local area around
		int[] around = new int[4];
		if (!findAround(around)) {
			rgb();
			return;
		}
		
		// Binarization 
		binarizer.thresholdRGB(picture, around);
		boolean[][][] bin = binarizer.binarizationRGB(picture, around);
		
		// Blue first
		reader = setReader(bin[2], around);
		interleave(reader.readMessage());
		
		// Red next
		reader = setReaderRGB(bin[0], around);
		interleave(reader.readMessage());
		
		// Green finally
		reader = setReaderRGB(bin[1], around);
		interleave(reader.readMessage());
	}

	/**
	 * <b>private boolean findAround(int[] around)</b><br/>
	 * Initialized and launch the research of the local area.<br/>
	 * It returns if the result has been saved in around
	 * 
	 * @param around : array for the result (must be at least length 4)
	 * @return if it succeeded and if the result is saved
	 */
	private boolean findAround(int[] around) {
		if (reader == null) return false;
		if (!reader.isReadable()) return false;
		
		int N = reader.getPatterns().nbrPatterns() - 1;
		double row = reader.getPatterns().pattern(N, N, Patterns.ROW);
		double col = reader.getPatterns().pattern(N, N, Patterns.COL);
		return findAround(reader.getTransform(), row, col, around, 4);
	}
	
	/**
	 * <b>private boolean findAround(AffineTransform transform, double rowLast, double colLast, int[] around, int distance)</b><br/>
	 * Research the local area around the QR code given the affine transform and the position of bottom right pattern
	 * 
	 * @param transform : the affine transform
	 * @param rowLast : row of bottom right pattern
	 * @param colLast : column of bottom right pattern
	 * @param around : the array for result (must be at least length 4)
	 * @param distance : the distance around the QR code
	 * @return if it succeeded
	 */
	private boolean findAround(AffineTransform transform, double rowLast, double colLast, int[] around, int distance) {
		if (around == null) return false;
		if (around.length < 4) return false;
		
		int tmp;
		
		// Top Left of the QR code
		around[0] = transform.rowTransform(-distance, -distance);
		around[2] = around[0];
		around[1] = transform.colTransform(-distance, -distance);
		around[3] = around[1];
		
		// Top Right of the QR code
		tmp = transform.rowTransform(-distance, transform.QRlength()-1+distance);
		if (tmp<around[0]) around[0] = tmp;
		else if (tmp>around[2]) around[2] = tmp;	
		
		tmp = transform.colTransform(-distance, transform.QRlength()-1+distance);
		if (tmp<around[1]) around[1] = tmp;
		else if (tmp>around[3]) around[3] = tmp;

		// Bottom Left of the QR code
		tmp = transform.rowTransform(transform.QRlength()-1+distance, -distance);
		if (tmp<around[0]) around[0] = tmp;
		else if (tmp>around[2]) around[2] = tmp;		
		
		tmp = transform.colTransform(transform.QRlength()-1+distance, -distance);
		if (tmp<around[1]) around[1] = tmp;
		else if (tmp>around[3]) around[3] = tmp;

		// Bottom Right of the QR code
		tmp = Math.min(transform.finders().getHeight(), Math.max(0, (int) (rowLast +
				transform.rowLinear(7+distance, 7+distance)) ));
		if (tmp<around[0]) around[0] = tmp;
		else if (tmp>around[2]) around[2] = tmp;		
		
		tmp = Math.min(transform.finders().getWidth(), Math.max(0, (int) (colLast +
				transform.colLinear(7+distance, 7+distance)) ));
		if (tmp<around[1]) around[1] = tmp;
		else if (tmp>around[3]) around[3] = tmp;
		
		return true;
	}
	
	/**
	 * <b>private Reader setReader(boolean[][] bin)</b><br/>
	 * Set a new Reader for the next reading using only the binarized picture
	 * 
	 * @param bin : the binarized picture
	 * @return the new Reader
	 */
	private Reader setReader(boolean[][] bin) {
		Reader newReader = new Reader(bin, readerFrameCallback);
		
		newReader.setReader();
		
		return newReader;
	}
	
	/**
	 * <b>private Reader setReader(boolean[][] bin, int[] around)</b><br/>
	 * Set a new Reader for the next reading using the binarized picture in a given local area.
	 * 
	 * @param bin : the binarized picture
	 * @param around : the local area around
	 * @return the new Reader
	 */
	private Reader setReader(boolean[][] bin, int[] around) {
		Reader newReader = new Reader(bin, readerFrameCallback);
		
		newReader.setReader(around[0], around[1], around[2], around[3]);
		
		return newReader;
	}
	
	/**
	 * <b>private Reader setReaderRGB(boolean[][] bin)</b><br/>
	 * Set a new Reader for the next reading using the binarized picture and the informations of the old reader
	 * 
	 * @param bin : the binarized picture
	 * @return the new Reader
	 */
	// TODO comments to change
	private Reader setReaderRGB(boolean[][] bin) {
		if (reader == null) return setReader(bin);
		if (!reader.isReadable()) return setReader(bin);
		
		Reader newReader = new Reader(bin, readerFrameCallback);
		newReader.setReader(reader.getFinders(), reader.getTransform(), reader.getVersion(), reader.getPatterns(), reader.getDataArea());
		
		return newReader;
	}
	
	// TODO comments
	private Reader setReaderRGB(boolean[][] bin, int[] around) {
		if (reader == null) return setReader(bin, around);
		if (!reader.isReadable()) return setReader(bin, around);
		
		Reader newReader = new Reader(bin, readerFrameCallback);
		newReader.setReader(reader.getFinders(), reader.getTransform(), reader.getVersion(), reader.getPatterns(), reader.getDataArea());
		
		return newReader;
	}
	
	/**
	 * <b>private void interleave(byte[] readMsg)</b><br/>
	 * Apply the interleaving on the read message
	 * 
	 * @param readMsg : the message read in the QR code
	 */
	private void interleave(byte[] readMsg) {
		if (!reader.isReadable() || readMsg == null) 
			msgCallback.messageReady(null, 0, picture);
		else {
			interleaver = new Interleaver(reader.getVersion(), reader.getFormat().correctionLevel());
			msgCallback.messageReady(interleaver.decode(readMsg), reader.getVersion().version(), picture);
		}
	}
		
	public class Binarizer {
		
		// the different threshold
		private int threshold;
		private int thresholdR;
		private int thresholdG;
		private int thresholdB;
		
		public Binarizer() {
			threshold = 0;
			thresholdR = 0;
			thresholdG = 0;
			thresholdB = 0;
		}

		// TODO comments
		public void thresholdWhite(PreviewPicture picture, int[] around) {
			int[] histogram = new int[256];
			threshold = threshold(histogram, histogramWhite(picture, around, histogram));
		}

		// TODO comments
		public void thresholdRed(PreviewPicture picture, int[] around) {
			int[] histogram = new int[256];
			thresholdR = threshold(histogram, histogramRed(picture, around, histogram));
		}

		// TODO comments
		public void thresholdGreen(PreviewPicture picture, int[] around) {
			int[] histogram = new int[256];
			thresholdG = threshold(histogram, histogramGreen(picture, around, histogram));
		}

		// TODO comments
		public void thresholdBlue(PreviewPicture picture, int[] around) {
			int[] histogram = new int[256];
			thresholdB = threshold(histogram, histogramBlue(picture, around, histogram));
		}
		
		//TODO comments
		public void thresholdRGB(PreviewPicture picture, int[] around) {
			thresholdRed(picture, around);
			thresholdGreen(picture, around);
			thresholdBlue(picture, around);
		}
		
		private int histogramWhite(PreviewPicture picture, int[] around, int[] histogram) {
			int i, j, nbrPixel = 0;
			for(i=around[0]; i<around[2]; i+=2) {
				for(j=around[1]; j<around[3]; j+=2) {
					  histogram[picture.pixelWhite(i,j)&0xFF]++;
					  nbrPixel++;
				}
			}
			return nbrPixel;
		}
		
		private int histogramRed(PreviewPicture picture, int[] around, int[] histogram) {
			int i, j, nbrPixel = 0;
			for(i=around[0]; i<around[2]; i+=2) {
				for(j=around[1]; j<around[3]; j+=2) {
					  histogram[picture.pixelRed(i,j)&0xFF]++;
					  nbrPixel++;
				}
			}
			return nbrPixel;
		}
		
		private int histogramGreen(PreviewPicture picture, int[] around, int[] histogram) {
			int i, j, nbrPixel = 0;
			for(i=around[0]; i<around[2]; i+=2) {
				for(j=around[1]; j<around[3]; j+=2) {
					  histogram[picture.pixelGreen(i,j)&0xFF]++;
					  nbrPixel++;
				}
			}
			return nbrPixel;
		}
		
		private int histogramBlue(PreviewPicture picture, int[] around, int[] histogram) {
			int i, j, nbrPixel = 0;
			for(i=around[0]; i<around[2]; i+=2) {
				for(j=around[1]; j<around[3]; j+=2) {
					  histogram[picture.pixelBlue(i,j)&0xFF]++;
					  nbrPixel++;
				}
			}
			return nbrPixel;
		}
		
		private int threshold(int[] histogram, int nbrPixel) {
			int k;
			
			long pixelBackground = 0;
			long pixelForeground = 0;
			
			double sumMeanBackground = 0;
			double sumMeanForeground = 0;
			double sumMeanTotal = 0;
			for (k=0; k<256; k++) sumMeanTotal += k*histogram[k];
			
			double meanBackground, meanForeground, var;
			
			double varMax = 0;
			int threshold = 0;

			for (k=0 ; k<256; k++) {
			   pixelBackground += histogram[k]; 
			   if (pixelBackground == 0) continue;

			   pixelForeground = nbrPixel - pixelBackground;
			   if (pixelForeground == 0) break;
			   
			   sumMeanBackground += k*histogram[k];
			   sumMeanForeground = sumMeanTotal - sumMeanBackground;
			   
			   meanBackground = sumMeanBackground/pixelBackground; 
			   meanForeground = sumMeanForeground/pixelForeground; 

			   // Calculate Variance
			   var = pixelBackground*pixelForeground*Math.pow(meanBackground - meanForeground,2);

			   // Check if new maximum found
			   if (var > varMax) {
			      varMax = var;
			      threshold = k;
			   }
			}
			
			return threshold;
		}
		
		/**
		 * <b>private boolean[][] binarization(PreviewPicture picture, int[] around, int threshold)</b><br/>
		 * Calculate the binarized picture of a given picture given a threshold in the restricted area given by around.
		 * 
		 * @param picture : the given picture
		 * @param around : the local area (must be length 4)
		 * @param threshold : the threshold for binarization 
		 * @return the binarized picture
		 */
		// TODO comments to change
		public boolean[][] binarizationWhite(PreviewPicture picture, int[] around) {
			boolean[][] bin = new boolean[picture.getHeight()][picture.getWidth()];
			
			int i, j;
			for (i=around[0]; i<around[2]; i++)
				for (j=around[1]; j<around[3]; j++)  
					bin[i][j] = (picture.pixelWhite(i,j)&0xFF) <= threshold;
			
			return bin;
		}
		
		// TODO comments
		public boolean[][][] binarizationRGB(PreviewPicture picture, int[] around) {
			boolean[][][] bin = new boolean[3][picture.getHeight()][picture.getWidth()];
			
			int i, j;
			for (i=around[0]; i<around[2]; i++)
				for (j=around[1]; j<around[3]; j++) {
					bin[0][i][j] = (picture.pixelRed(i,j)&0xFF) <= thresholdR;
					bin[1][i][j] = (picture.pixelGreen(i,j)&0xFF) <= thresholdG;
					bin[2][i][j] = (picture.pixelBlue(i,j)&0xFF) <= thresholdB;
				}
			
			return bin;
		}
		
	}
}
