package org.postgresql;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.sql.SQLException;

import org.postgresql.coreDriver.Encoding;
import org.postgresql.util.FailOverException;

/*
 * $Id: PG_Stream.java,v 1.1 2005/03/02 16:07:32 jsalas Exp $
 *
 * This class is used by Connection & PGlobj for communicating with the
 * backend.
 *
 * @see java.sql.Connection
 */
//	This class handles all the Streamed I/O for a org.postgresql connection
public class PG_Stream {
	private Socket connection;
	private InputStream pg_input;
	private BufferedOutputStream pg_output;
	private byte[] byte_buf = new byte[8 * 1024];
	
	/*
	 * Constructor:  Connect to the PostgreSQL back end and return
	 * a stream connection.
	 *
	 * @param host the hostname to connect to
	 * @param port the port number that the postmaster is sitting on
	 * @exception IOException if an IOException occurs below it.
	 */
	public PG_Stream(String host, int port) throws FailOverException {
		try {
			connection = new Socket(host, port);
			
			// Submitted by Jason Venner <jason@idiom.com> adds a 10x speed
			// improvement on FreeBSD machines (caused by a bug in their TCP Stack)
			connection.setTcpNoDelay(true);
			
			
			// HACER : Este tiempo hay que bajarlo
			// Timeout in reception of 100 miliseconds
			//System.out.println("PG_Stream.PG_Stream: Established timeout for read() in socket to 15000"); //JSalas
			connection.setSoTimeout(0 /* TODO 15000 JSalas a 15000 no funciona*/);
			//System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1500000");
			// Buffer sizes submitted by Sverre H Huseby <sverrehu@online.no>
			pg_input = new BufferedInputStream(connection.getInputStream(), 8192);
			pg_output = new BufferedOutputStream(connection.getOutputStream(), 8192);
		} catch (IOException e) {
			throw new FailOverException("postgresql.stream.ioerror");
		}
	}
	
	/*
	 * Sends a single character to the back end
	 *
	 * @param val the character to be sent
	 * @exception IOException if an I/O error occurs
	 */
	public void SendChar(int val) throws FailOverException {
		try {
			pg_output.write( (byte) val);
		} catch (IOException e) {
			throw new FailOverException("postgresql.stream.ioerror");
		}
	}
	
	/*
	 * Sends an integer to the back end
	 *
	 * @param val the integer to be sent
	 * @param siz the length of the integer in bytes (size of structure)
	 * @exception IOException if an I/O error occurs
	 */
	public void SendInteger(int val, int siz) throws FailOverException {
		try {
			byte[] buf = new byte[siz];
			
			while (siz-- > 0) {
				buf[siz] = (byte) (val & 0xff);
				val >>= 8;
			}
			Send(buf);
		} catch (Exception e) {
			throw new FailOverException("postgresql.stream.ioerror");
		}
	}
	
	/*
	 * Send an array of bytes to the backend
	 *
	 * @param buf The array of bytes to be sent
	 * @exception IOException if an I/O error occurs
	 */
	public void Send(byte buf[]) throws FailOverException {
		try {
			pg_output.write(buf);
		} catch (IOException e) {
			throw new FailOverException("postgresql.stream.ioerror");
		}
		
	}
	
	/*
	 * Send an exact array of bytes to the backend - if the length
	 * has not been reached, send nulls until it has.
	 *
	 * @param buf the array of bytes to be sent
	 * @param siz the number of bytes to be sent
	 * @exception IOException if an I/O error occurs
	 */
	public void Send(byte buf[], int siz) throws FailOverException {
		try {
			Send(buf, 0, siz);
		} catch (Exception e) {
			throw new FailOverException("postgresql.stream.ioerror");
		}
		
	}
	
	/*
	 * Send an exact array of bytes to the backend - if the length
	 * has not been reached, send nulls until it has.
	 *
	 * @param buf the array of bytes to be sent
	 * @param off offset in the array to start sending from
	 * @param siz the number of bytes to be sent
	 * @exception IOException if an I/O error occurs
	 */
	public void Send(byte buf[], int off, int siz) throws FailOverException {
		int i;
		
		try {
			pg_output.write(buf, off,
					( (buf.length - off) < siz ? (buf.length - off) : siz));
			if ( (buf.length - off) < siz) {
				for (i = buf.length - off; i < siz; ++i) {
					pg_output.write(0);
				}
			}
		} catch (IOException e) {
			throw new FailOverException("postgresql.stream.ioerror");
		}
		
	}
	
	//ADDED by JSALAS
	private boolean send_suspect() throws FailOverException{
		return true;
		//return send_suspect_old();
	}
	
	//ADDED by JSALAS
	public int ReceiveCharWithOutSuspect() throws FailOverException {
		int c = 0;
		
		try {
			c = pg_input.read();
			if (c < 0) {
				//System.out.println("PG_Stream.ReceiveCharWithOutSuspect(): postgresql.stream.eof");
				throw new FailOverException("postgresql.stream.eof");
			}			
		
		} catch (IOException e) {
			//System.out.println("PG_Stream.ReceiveCharWithOutSuspect(): postgresql.stream.ioerror");					
			throw new FailOverException("postgresql.stream.ioerror");
		}
		return c;
	}
	
	private boolean send_suspect_old() throws FailOverException{
		SendChar('S');
		int c = ReceiveCharWithOutSuspect();
		if ((char)c == 'W'){
			return false;
		}
		return true;
	}
	
	/*
	 * Receives a single character from the backend
	 *
	 * @return the character received
	 * @exception SQLException if an I/O Error returns
	 */
	public int ReceiveChar() throws FailOverException {
		int c = 0;
		long inittime = 0, finishtime = 0;//JSALAS		
		boolean confirm_suspect = false;//ADDED by JSALAS
		while (!confirm_suspect){//ADDED by JSALAS
			try {
				inittime = System.currentTimeMillis();//JSALAS
				c = pg_input.read();
				finishtime = System.currentTimeMillis();//JSALAS
				//System.out.println("C:<" + (char)c + ">" + "I:<" + inittime + ">" + "F:<" + finishtime + ">" + "A:<" + (finishtime-inittime) + ">");
				if (c < 0) {
					System.out.println("PG_Stream.ReceiveChar(): postgresql.stream.eof");
					throw new FailOverException("postgresql.stream.eof");
				}
				confirm_suspect = true;//ADDED by JSALAS
			
			} catch (IOException e) {
				System.out.println("PG_Stream.ReceiveChar(): postgresql.stream.ioerror");
				confirm_suspect = send_suspect();//ADDED by JSALAS
				if (confirm_suspect) {//ADDED by JSALAS			
					throw new FailOverException("postgresql.stream.ioerror");
				}
			}
		}
		return c;
	}
	
	/*
	 * Receives an integer from the backend
	 *
	 * @param siz length of the integer in bytes
	 * @return the integer received from the backend
	 * @exception SQLException if an I/O error occurs
	 */
	public int ReceiveInteger(int siz) throws FailOverException {
		int n = 0;
		int i = 0;
		
		int round = 0;//ADDED by Jsalas
		boolean confirm_suspect = false;//ADDED by JSALAS
		while (!confirm_suspect){//ADDED by JSALAS
			try {
				for (i = round/*ADDED by JSALAS*//*0*/; i < siz; i++) {
					int b = pg_input.read();
					
					if (b < 0) {
						throw new FailOverException("postgresql.stream.eof");
					}
					n = n | (b << (8 * i));
				}
				confirm_suspect = true;//ADDED by JSALAS
			
			} catch (IOException e) {
				confirm_suspect = send_suspect();//ADDED by JSALAS
				if (confirm_suspect) {//ADDED by JSALAS
					throw new FailOverException("postgresql.stream.ioerror");
				}
				round = i;//ADDED by JSALAS
			}
		}
		return n;
	}
	
	/*
	 * Receives an integer from the backend
	 *
	 * @param siz length of the integer in bytes
	 * @return the integer received from the backend
	 * @exception SQLException if an I/O error occurs
	 */
	public int ReceiveIntegerR(int siz) throws FailOverException {
		int n = 0;
		int i = 0;
		
		int round = 0;//ADDED by JSALAS
		boolean confirm_suspect = false;//ADDED by JSALAS
		while (!confirm_suspect){//ADDED by JSALAS
			try {
				for (i = round/*ADDED by JSALAS*//*0*/; i < siz; i++) {
					int b = pg_input.read();
					
					if (b < 0) {
						throw new FailOverException("postgresql.stream.eof");
					}
					n = b | (n << 8);
				}
				confirm_suspect = true;//ADDED by JSALAS
			}
			catch (IOException e) {
				confirm_suspect = send_suspect();
				if (confirm_suspect) {
					throw new FailOverException("postgresql.stream.ioerror");
				}
				round = i;//ADDED by JSALAS
			}
		}
		return n;
	}
	
	//MODIFIED by JSALAS
	/*
	 * Receives a null-terminated string from the backend.	If we don't see a
	 * null, then we assume something has gone wrong.
	 *
	 * @param encoding the charset encoding to use.
	 * @return string from back end
	 * @exception SQLException if an I/O error occurs, or end of file
	 */
	public String ReceiveString(Encoding encoding) throws SQLException, FailOverException{
		int s = 0;
		byte[] rst = byte_buf;
		
		int buflen = rst.length;
		boolean done = false;
		
		long inittime = 0, finishtime = 0;//JSALAS										
		while (!done) {
			try {
				while (s < buflen) {
					//System.out.println("wait char"); //JSALAS
					inittime = System.currentTimeMillis();//JSALAS
					int c = pg_input.read();
					finishtime = System.currentTimeMillis();//JSALAS
					//System.out.println("C:<" + (char)c + ">" + "I:<" + inittime + ">" + "F:<" + finishtime + ">" + "A:<" + (finishtime-inittime) + ">");
					//System.out.println("recv char " + (char)c); //JSALAS
					if (c < 0) {
						throw new FailOverException("postgresql.stream.eof");
					}
					else if (c == 0) {
						rst[s] = 0;
						done = true;
						break;
					}
					else {
						rst[s++] = (byte) c;
					}
					if (s >= buflen) { // Grow the buffer
						buflen = (int) (buflen * 2); // 100% bigger
						byte[] newrst = new byte[buflen];
						System.arraycopy(rst, 0, newrst, 0, s);
						rst = newrst;
					}
				}
			} catch (IOException e) {
				boolean confirm_suspect = send_suspect();
				if (confirm_suspect) {
					throw new FailOverException("postgresql.stream.ioerror");
				}
			}
		}
		return encoding.decode(rst, 0, s);
	}
	
	/*
	 * Read a tuple from the back end.	A tuple is a two dimensional
	 * array of bytes
	 *
	 * @param nf the number of fields expected
	 * @param bin true if the tuple is a binary tuple
	 * @return null if the current response has no more tuples, otherwise
	 *	an array of strings
	 * @exception SQLException if a data I/O error occurs
	 */
	public byte[][] ReceiveTuple(int nf, boolean bin) throws FailOverException {
		byte[][] answer;
		
		//try {//MODIFIED by JSALAS
		int i, bim = (nf + 7) / 8;
		byte[] bitmask = Receive(bim);
		//System.out.println("Recibido bitmask. Numero de campos " + nf);
		answer = new byte[nf][0];
		
		int whichbit = 0x80;
		int whichbyte = 0;
		
		for (i = 0; i < nf; ++i) {
			boolean isNull = ((bitmask[whichbyte] & whichbit) == 0);
			whichbit >>= 1;
			if (whichbit == 0) {
				++whichbyte;
				whichbit = 0x80;
			}
			if (isNull) {
				answer[i] = null;
			}
			else {
				int len = ReceiveIntegerR(4);
				
				if (!bin) {
					len -= 4;
				}
				if (len < 0) {
					len = 0;
				}
				answer[i] = Receive(len);
				// Modificado por mi
				
				 //System.out.println("Length: " + len);
				 //System.out.println("Answer : " + new String(answer[i]));
				
			}
		}
		//} catch (Exception e) {//MODIFIED by JSALAS
			//throw new FailOverException("postgresql.stream.ioerror");//MODIFIED by JSALAS
		//}//MODIFIED by JSALAS
				
		return answer;
	}
	
	/*
	 * Reads in a given number of bytes from the backend
	 *
	 * @param siz number of bytes to read
	 * @return array of bytes received
	 * @exception SQLException if a data I/O error occurs
	 */
	private byte[] Receive(int siz) throws FailOverException {
		//try {//MODIFIED by JSALAS
		byte[] answer = new byte[siz];
		Receive(answer, 0, siz);
		return answer;
		//} catch (Exception e) {//MODIFIED by JSALAS
		//	throw new FailOverException("postgresql.stream.ioerror");//MODIFIED by JSALAS
		//}//MODIFIED by JSALAS
	}
	
	/*
	 * Reads in a given number of bytes from the backend
	 *
	 * @param buf buffer to store result
	 * @param off offset in buffer
	 * @param siz number of bytes to read
	 * @exception SQLException if a data I/O error occurs
	 */
	public void Receive(byte[] b, int off, int siz) throws FailOverException {
		int s = 0;
		
		boolean confirm_suspect = false;
		while (!confirm_suspect){
			try {
				while (s < siz) {
					int w = pg_input.read(b, off + s, siz - s);
					if (w < 0) {
						throw new FailOverException("Receive.postgresql.stream.eof");
					}
					s += w;
				}
				confirm_suspect = true;
				
			} catch (IOException e) {
				confirm_suspect = send_suspect();
				if (confirm_suspect) {
					throw new FailOverException("Receive.postgresql.stream.ioerror");
				}
			}
		}
	}
	
	/****************************************/
	/* CAMBIADO POR MI PARA HACER LA PRUEBA */
	/****************************************/
	
	public String Receive() throws FailOverException {
		String recibido = new String();
		int c = 0;
		try {
			while (c != '#') {
				c = pg_input.read();
				//System.out.println("Recibido el caracter : " + (char)c);
				if (c != '#') {
					recibido = recibido + (char) c;
				}
			}
		}
		catch (IOException e) {
			throw new FailOverException("Receive.postgresql.stream.ioerror");
		}
		return recibido;
	}
	
	/****************************************/
	/*              HASTA AQUI              */
	/****************************************/
	/*
	 * This flushes any pending output to the backend. It is used primarily
	 * by the Fastpath code.
	 * @exception SQLException if an I/O error occurs
	 */
	public void flush() throws FailOverException {
		try {
			pg_output.flush();
		}
		catch (IOException e) {
			throw new FailOverException("postgresql.stream.flush");
		}
	}
	
	/*
	 * Closes the connection
	 *
	 * @exception IOException if a IO Error occurs
	 */
	public void close() throws IOException {
		if (pg_output != null){
			pg_output.close();
			pg_output = null;
		}
		if (pg_input != null){
			pg_input.close();
			pg_input = null;
		}
		if (connection != null){
			connection.close();
			connection = null;
		}
	}
	
	public void printSocket () {
		System.out.println ("Socket : " + connection.toString());
	}
}