package projectEQ2440.QRcode.decode;

import projectEQ2440.QRcode.info.*;
import projectEQ2440.QRcode.image.*;
import projectEQ2440.QRcode.mask.*;

/**
 * <b>public class Reader</b><br/>
 * Class to manage the reading of the QR code
 */
public class Reader {
	
	// The picture
	private boolean[][] picture;
	
	// the finders
	private Finders finders; 
	
	// The affine transformation
	private AffineTransform transform;
	
	// The version of the QR code
	private Version version;
	
	// The patterns
	private Patterns patterns;
	
	// The dataArea for reading
	private DataArea dataArea;
	
	// The frame, the format and the mask
	private Frame frame;
	private Format format;
	private Mask mask;
	
	// Read picture
	private boolean[][] readPicture;
	
	// The check of current frame callback
	private ReaderFrameCallback readerFrameCallback; 

	// Boolean to tell if the reading is ok
	private boolean readable;
	private boolean read;
	private boolean initialized;
	
	/**
	 * <b>public Reader(boolean[][] picture)</b><br/>
	 * Constructor initializing the reading of the binarized picture
	 * 
	 * @param picture : the binarized picture
	 */
	public Reader(boolean[][] picture) {
		this.picture = picture;
		this.readerFrameCallback = new ReaderFrameCallback() { };
		readable = false;
		read = false;
	}
	
	/**
	 * <b>public Reader(boolean[][] picture, ReaderFrameCallback readerFrameCallback)</b><br/>
	 * Constructor initializing the reading of the binarized picture with the ReaderFrameCallback applying a check between each frame
	 * 
	 * @param picture : the binarized picture
	 * @param readerFrameCallback : the ReaderFrameCallback applying a check between each frame
	 */
	public Reader(boolean[][] picture, ReaderFrameCallback readerFrameCallback) {
		this.picture = picture;
		
		if (readerFrameCallback == null) this.readerFrameCallback = new ReaderFrameCallback() { };
		else this.readerFrameCallback = readerFrameCallback;
		
		readable = false;
		read = false;
	}

	/**
	 * <b>public static abstract class ReaderFrameCallback</b><br/>
	 * abstract call for the setting of the flashing algorithm between frames
	 */
	public static abstract class ReaderFrameCallback {
		/**
		 * <b>public boolean check(int currentFrame)</b><br/>
		 * Function which MUST (callback to implement) tell if the current frame isn't decoded yet
		 * 
		 * @param currentFrame : the current frame to test
		 * @return if it isn't decoded
		 */
		public boolean check(int currentFrame) {
			return true;
		}
	}

	/**
	 * <b>public void setReader(boolean[][] picture)</b><br/>
	 * Reader using only the binarized picture
	 * 
	 */
	public void setReader() {
		if (initFirst(0,0,picture.length,picture[0].length)) initialized = initVersion();
		else initialized = false;
	}

	/**
	 * <b>public void setReader(int rowStart, int colStart, int rowEnd, int colEnd)</b><br/>
	 * Reader using the binarized picture and restraining the research area
	 * 
	 * @param rowStart : the row where to start research
	 * @param colStart : the column where to start research
	 * @param rowEnd : the row where to end research
	 * @param colEnd : the column where to end research
	 */
	public void setReader(int rowStart, int colStart, int rowEnd, int colEnd) {
		if (initFirst(rowStart,colStart,rowEnd,colEnd)) initialized = initVersion();
		else initialized = false;
	}
	
	/**
	 * <b>public void setReader(Finders finders, AffineTransform transform, Version version, Patterns patterns, AffineTransformBack transformBack, DataArea dataArea)</b><br/>
	 * Reader using binarized picture, given finders, given affine transformation, given version, given patterns, given affine transformation back and the data area
	 * 
	 * @param finders : The given finders
	 * @param transform : The given affine transformation
	 * @param version : The given version
	 * @param patterns : The given patterns
	 * @param transformBack : the given affine transformation back
	 * @param dataArea : the given data area
	 */
	public void setReader(Finders finders, AffineTransform transform, Version version, Patterns patterns, DataArea dataArea) {
		this.finders = finders;
		this.transform = transform;
		
		this.version = version;		
		this.patterns = patterns;
		this.dataArea = dataArea;
		
		if (finders.findersFound() && patterns.patternsFound()) initialized = initEnd();
		else initialized = false;
	}
	
	/**
	 * <b>private boolean initFirst(int iStart, int jStart, int iEnd, int jEnd)</b><br/>
	 * Begin the initialization calculating the finders and affine transformation and return the result of <i>initVersion()</i>
	 * 
	 * @param iStart : the row where we start
	 * @param jStart : the column where we start
	 * @param iEnd : the row where we end
	 * @param jEnd : the column where we end
	 * @return if it succeed and the result of <i>initVersion()</i>
	 */
	private boolean initFirst(int iStart, int jStart, int iEnd, int jEnd) {
		finders = new Finders(picture, iStart, jStart, iEnd, jEnd);
		if (!finders.findersFound()) return false;
		
		transform = new AffineTransform(finders);
		
		return true;
	}
	
	/**
	 * <b>private boolean initVersion()</b><br/>
	 * Begin the initialization reading the version and return the result of <i>initRead</i> 
	 * 
	 * @return if the reading is a success and the result of <i>initRead()</i>
	 */
	private boolean initVersion() {
		if (!readVersion()) return false;
		return initRead();
	}
	
	/**
	 * <b>private boolean initRead()</b><br/>
	 * Begin the initialization finding the patterns, creating the affine transformation back and the data area. Then return the result of <i>initEnd()</i>
	 * 
	 * @return if the research is a success and the result of <i>initEnd()</i>
	 */
	private boolean initRead() {
		patterns = new Patterns(picture, version, transform);
		if (!patterns.patternsFound()) return false;
		
		dataArea = new DataArea(version);
		
		return initEnd();
	}
	
	/**
	 * <b>private boolean initEnd()</b><br/>
	 * Finish the initialization reading the format and calculating the mask
	 * 
	 * @return if it succeed 
	 */
	private boolean initEnd() {
		if (!readFrame()) {
			read = true;
			readable = true;
			return true;
		}
		if (!readFormat()) return false;
		mask = format.mask();
		
		return true;
	}
	
	/**
	 * <b>public boolean isRead()</b><br/>
	 * Tell if the message is ready to be read
	 * 
	 * @return if it is ready to read
	 */
	public boolean isRead() {
		if (read) return true;
		if (!initialized) return false;
		readable = readData();
		read = true;
		return true;
	}
	
	/**
	 * <b>public boolean isReadable()</b><br/>
	 * Tell if the message is readable.
	 * 
	 * @return if it is readable
	 */
	public boolean isReadable() {
		return readable;
	}
	
	/**
	 * <b>public byte[] readMessage()</b><br/>
	 * Read the message and return it. If it's not readable, return null
	 * 
	 * @return the message, null if it's not readable
	 */
	public byte[] readMessage() {
		if (!initialized) return null;
		
		int lengthMsg = version.messageLength();
		byte[] msg = new byte[lengthMsg];
		
		if (!read) isRead();
		if (!readable) return null;
		if (!snakeRead(msg)) return null;
		
		return msg;
	}
	
	/**
	 * <b>public boolean[][]  getPicture()</b><br/>
	 * get the binarized picture
	 * 
	 * @return the binarized picture
	 */
	public boolean[][]  getPicture() {
		return picture;
	}
	
	/**
	 * <b>public boolean[][] getReadPicture()</b><br/>
	 * get the read QR code
	 * 
	 * @return the read QR code
	 */
	public boolean[][] getReadPicture() {
		if (readable) return readPicture;
		else return null;
	}
	
	/**
	 * <b>public Finders getFinders()</b><br/>
	 * get the finders
	 * 
	 * @return the finders
	 */
	public Finders getFinders() {
		return finders;
	}
	
	/**
	 * <b>public AffineTransform getTransform()</b><br/>
	 * get the affine transformation
	 * 
	 * @return the affine transformation
	 */
	public AffineTransform getTransform() {
		return transform;
	}
	
	/**
	 * <b>public Version getVersion()</b><br/>
	 * get the version
	 * 
	 * @return the version
	 */
	public Version getVersion() {
		return version;
	}
	
	/**
	 * <b>public Patterns getPatterns()</b><br/>
	 * get the patterns
	 * 
	 * @return the patterns
	 */
	public Patterns getPatterns() {
		return patterns;
	}
	
	/**
	 * <b>public DataArea getDataArea()</b><br/>
	 * get the data area
	 * 
	 * @return the data area
	 */
	public DataArea getDataArea() {
		return dataArea;
	}
	
	/**
	 * <b>public Format getFormat()</b><br/>
	 * get the format
	 * 
	 * @return the format
	 */
	public Format getFormat() {
		return format;
	}
	

	/**
	 * <b>private boolean readVersion()</b><br/>
	 * Read the version and save it in <i>version</i>
	 * 
	 * @return if it succeeds
	 */
	private boolean readVersion() {
		if (transform.QRlength()<45) {
			version = new Version((transform.QRlength()-17)/4);
		} else {
			boolean[] tmp1 = new boolean[18];
			boolean[] tmp2 = new boolean[18];
			int i, j, k=17, N=transform.QRlength(), row, col;
			
			for (i=0; i<6; i++)
				for (j=N-11; j<=N-9; j++) {
					row = transform.rowTransform(i,j);
					col = transform.colTransform(i,j);
					tmp1[k] = picture[row][col];
					
					row = transform.rowTransform(j,i);
					col = transform.colTransform(j,i);
					tmp2[k] = picture[row][col];
					
					k--;
				}
			
			version = new Version(tmp1);
			version.compare(tmp2);
			
		}
		
		if (version.version()==0) return false;
		
		transform.newLength(version.lengthQR());
		return true;
	}
	
	/**
	 * <b>private boolean readFrame()</b><br/>
	 * Read the frame number in the QR code and check it
	 * 
	 * @return the result of the callback
	 */
	private boolean readFrame() {
		int i, j, k = 0, row, col;
		boolean[] frame = new boolean[24];
		
		for (i=0; i<6; i++) {
			for (j=9; j<11; j++) {
				row = transform.rowTransform(i,j);
				col = transform.colTransform(i,j);
				frame[23-k] = picture[row][col];
				
				row = transform.rowTransform(j,i);
				col = transform.colTransform(j,i);
				frame[k] = picture[row][col];
				
				k++;
			}
		}
		
		this.frame = new Frame(frame);  
		return readerFrameCallback.check(this.frame.frame());
	}
	
	/**
	 * <b>private boolean readFormat()</b><br/>
	 * Read the format and save it in <i>format</i>
	 * 
	 * @return if it succeeds
	 */
	private boolean readFormat() {
		boolean[] tmp1 = new boolean[15];
		boolean[] tmp2 = new boolean[15];
		int i, N=transform.QRlength(), row, col;
		
		for (i=0; i<6; i++) {
			row = transform.rowTransform(i,8);
			col = transform.colTransform(i,8);
			tmp1[14-i] = picture[row][col];
			
			row = transform.rowTransform(8,i);
			col = transform.colTransform(8,i);
			tmp1[i] = picture[row][col];
		}
		
		row = transform.rowTransform(7,8);
		col = transform.colTransform(7,8);
		tmp1[8] = picture[row][col];
		
		row = transform.rowTransform(8,8);
		col = transform.colTransform(8,8);
		tmp1[7] = picture[row][col];
		
		row = transform.rowTransform(8,7);
		col = transform.colTransform(8,7);
		tmp1[6] = picture[row][col];
		
		for (i=0; i<7; i++) {
			row = transform.rowTransform(7,N-1-i);
			col = transform.colTransform(7,N-1-i);
			tmp2[14-i] = picture[row][col];

			row = transform.rowTransform(N-1-i,7);
			col = transform.colTransform(N-1-i,7);
			tmp2[i] = picture[row][col];
		}

		row = transform.rowTransform(8,N-8);
		col = transform.colTransform(8,N-8);
		tmp2[7] = picture[row][col];
		
		format = new Format(tmp1,true);
		format.compare(tmp2,true);
		
		return true;
	}
	
	/**
	 * <b>private boolean readData()</b><br/>
	 * apply the reading of the QR code implementing the mask<br/> 
	 * The result is saved in <i>readPicture</i> </br>
	 * Apply <i>readSnake(byte[] )</i> to read in the good order
	 * 
	 * @return if it succeeds 
	 */
	private boolean readData() {
		int i, j, N=version.lengthQR(), ii, jj, row, col;
		
		readPicture = new boolean[N][N];
		
		if (version.version() == 1) {
			for (i=0; i<N; i++) {
				for (j=0; j<N; j++) {
					if (dataArea.value(i,j)) {
						row = Math.max(0, Math.min(finders.getHeight()-1,
								transform.rowTransform(i,j) ));
						col = Math.max(0, Math.min(finders.getWidth()-1,
								transform.colTransform(i,j) ));
						if (mask.value(i,j)) readPicture[i][j] = !picture[row][col];
						else readPicture[i][j] = picture[row][col];
					}
				}
			}
			return true;
		}
		
		double factor, tmp1, tmp2;
		double[] aa = new double[2];
		double[] ab = new double[2];
		double[] ba = new double[2];
		double[] bb = new double[2];
		int imax, imin, jmax, jmin;
		int iimax, iimin, jjmax, jjmin;
		int nPatt = patterns.nbrPatterns();
		int length = patterns.QRlength();

		for (ii=0; ii<nPatt-1; ii++) {
			
			iimin = patterns.localization(ii);
			if (ii == 0) imin = 0;
			else imin = iimin;
			
			iimax = patterns.localization(ii+1);
			if (ii == nPatt-2) imax = length;
			else imax = iimax;
			
			for (jj=0; jj<nPatt-1; jj++) {

				jjmin = patterns.localization(jj);
				if (jj == 0) jmin = 0;
				else jmin = jjmin;
				
				jjmax = patterns.localization(jj+1);
				if (jj == nPatt-2) jmax = length;
				else jmax = jjmax;
				
				factor = (iimax-iimin)*(jjmax-jjmin);
				
				aa[0] = patterns.pattern(ii, jj, Patterns.ROW);
				aa[1] = patterns.pattern(ii, jj, Patterns.COL);
				
				ab[0] = patterns.pattern(ii, jj+1, Patterns.ROW);
				ab[1] = patterns.pattern(ii, jj+1, Patterns.COL);
				
				ba[0] = patterns.pattern(ii+1, jj, Patterns.ROW);
				ba[1] = patterns.pattern(ii+1, jj, Patterns.COL);
				
				bb[0] = patterns.pattern(ii+1, jj+1, Patterns.ROW);
				bb[1] = patterns.pattern(ii+1, jj+1, Patterns.COL);
				
				for (i=imin; i<imax; i++) {
					for (j=jmin; j<jmax; j++) {
						if (dataArea.value(i,j)) {
							tmp1 = (iimax-i)*(jjmax-j)*aa[0] + (iimax-i)*(j-jjmin)*ab[0] + (i-iimin)*(jjmax-j)*ba[0] + (i-iimin)*(j-jjmin)*bb[0];
							tmp2 = (iimax-i)*(jjmax-j)*aa[1] + (iimax-i)*(j-jjmin)*ab[1] + (i-iimin)*(jjmax-j)*ba[1] + (i-iimin)*(j-jjmin)*bb[1];
							row = Math.max(0, Math.min(finders.getHeight()-1,
									(int) Math.round(tmp1/factor) ));
							col = Math.max(0, Math.min(finders.getWidth()-1,
									(int) Math.round(tmp2/factor) ));
							if (mask.value(i,j)) readPicture[i][j] = !picture[row][col];
							else readPicture[i][j] = picture[row][col];
						}
					}
				}
				
			}
		}
		
		return true;
	}
	
	/**
	 * <b>private boolean snakeRead(byte[] msg)</b><br/>
	 * Apply the snake read to read information in the good way
	 * 
	 * @param msg : the byte array where the read message is saved
	 * @return if the reading finished well
	 */
	private boolean snakeRead(byte[] msg) {
		if (readPicture == null) return false;
		
		int N = version.lengthQR();
		int i=N-1, j=N-1;
		int k=0, l=0;
		byte b = 0x00;
		boolean up=true, right=true;
		
		while (k<msg.length) {
			if (dataArea.value(i,j)) {
				b = (byte) (b << 1);
				if (readPicture[i][j]) b = (byte) (b | 0x01);
				l++;
				if (l==8) {
					msg[k] = b;
					l = 0;
					k++;
				}
			}
			
			if (right) {
				j--;
				right = false;
			} else {
				if (up) {
					if (i==0) {
						j--;
						up = false;
						if (j==6) j--;
					} else {
						j++;
						i--;
					}
				} else {
					if (i==N-1) {
						j--;
						up = true;
					} else {
						j++;
						i++;
					}
				}
				right = true;
			}
			
			if (j<0) return false;	
		}
		
		return true;
	}
	
}
