/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */package org.teiid.transport;

import static org.teiid.odbc.PGUtil.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.handler.ssl.SslHandler;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StreamCorruptedException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;

import javax.net.ssl.SSLEngine;

import org.teiid.client.util.ResultsFuture;
import org.teiid.core.types.ArrayImpl;
import org.teiid.core.types.GeometryType;
import org.teiid.core.util.ObjectConverterUtil;
import org.teiid.core.util.PropertiesUtils;
import org.teiid.core.util.ReflectionHelper;
import org.teiid.core.util.SqlUtil;
import org.teiid.core.util.StringUtil;
import org.teiid.core.util.TimestampWithTimezone;
import org.teiid.jdbc.ResultSetImpl;
import org.teiid.jdbc.TeiidSQLException;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.logging.MessageLevel;
import org.teiid.net.socket.ServiceInvocationStruct;
import org.teiid.odbc.ODBCClientRemote;
import org.teiid.odbc.PGUtil.PgColInfo;
import org.teiid.query.function.GeometryUtils;
import org.teiid.runtime.RuntimePlugin;
import org.teiid.transport.pg.PGbytea;
import org.teiid.transport.pg.TimestampUtils;
/**
 * Represents the messages going from Server --> PG ODBC Client  
 * Some parts of this code is taken from H2's implementation of ODBC
 */
@SuppressWarnings("nls")
public class PgBackendProtocol extends ChannelOutboundHandlerAdapter implements ODBCClientRemote {
    
    public static final String APPLICATION_NAME = "application_name"; //$NON-NLS-1$
    public static final String DEFAULT_APPLICATION_NAME = "ODBC"; //$NON-NLS-1$

	public static final String SSL_HANDLER_KEY = "sslHandler";

    private final class SSLEnabler implements ChannelFutureListener {
    	
    	private SSLEngine engine;
    	
		public SSLEnabler(SSLEngine engine) {
			this.engine = engine;
		}

		@Override
		public void operationComplete(ChannelFuture future) throws Exception {
			if (future.isSuccess()) {
				SslHandler handler = new SslHandler(engine);
				future.channel().pipeline().addFirst(SSL_HANDLER_KEY, handler);
			}
		}
	}
	
	private final class ResultsWorkItem implements Runnable {
		private final List<PgColInfo> cols;
		private final ResultSetImpl rs;
		private final ResultsFuture<Integer> result;
		private final short[] resultColumnFormat;
		private int rows2Send;
		private int rowsSent = 0;
		private int rowsInBuffer = 0;
		String sql;

		private ResultsWorkItem(List<PgColInfo> cols, ResultSetImpl rs, ResultsFuture<Integer> result, int rows2Send, short[] resultColumnFormat) {
			this.cols = cols;
			this.rs = rs;
			this.result = result;
			this.rows2Send = rows2Send;
			this.resultColumnFormat = resultColumnFormat;
			initBuffer(maxBufferSize / 8);
		}

		@Override
		public void run() {
			while (true) {
				try {
			    	nextFuture = rs.submitNext();
			    	synchronized (nextFuture) {
				    	if (!nextFuture.isDone()) {
					    	nextFuture.addCompletionListener(new ResultsFuture.CompletionListener<Boolean>() {
					    		@Override
					    		public void onCompletion(ResultsFuture<Boolean> future) {
					    			if (processRow(future)) {
					    				if (rowsSent != rows2Send) {
					    					ResultsWorkItem.this.run();
					    				}
					    			}
					    		}
							});
					    	return;
				    	}
			    	}
			    	if (!processRow(nextFuture)) {
			    		break;
			    	}
				} catch (Throwable t) {
					result.getResultsReceiver().exceptionOccurred(t);
				}
			}
		}
		
		private boolean processRow(ResultsFuture<Boolean> future) {
			nextFuture = null;
			boolean processNext = true;
			try {
    			if (future.get()) {
    				sendDataRow(rs, cols, resultColumnFormat);
    				rowsSent++;
    				rowsInBuffer++;
    				boolean done = rowsSent == rows2Send;
    				flushResults(done);
    				processNext = !done;
    				if (done) {
    					if (sql != null) {
    						sendCommandComplete(sql, rowsSent);
    					}
    					result.getResultsReceiver().receiveResults(rowsSent);
    				}
    			} else {
    				sendContents();
    				if (sql != null) {
		    			sendCommandComplete(sql, rowsSent);
		    		}
    				result.getResultsReceiver().receiveResults(rowsSent);
    				processNext = false;
    			}
			} catch (Throwable t) {
				result.getResultsReceiver().exceptionOccurred(t);
				return false;
			}
			return processNext;
		}
		
		private void flushResults(boolean force) {
			int avgRowsize = dataOut.writerIndex()/rowsInBuffer;
			if (force || (maxBufferSize - dataOut.writerIndex()) < (avgRowsize*2)) {
				sendContents();
				initBuffer(maxBufferSize / 8);
				rowsInBuffer = 0;
			}			
		}
	}

	public static final String DEFAULT_ENCODING = "UTF8";
	public static final String CLIENT_ENCODING = "client_encoding";

    private ByteBuf dataOut;
	private OutputStreamWriter writer;

    private Properties props;    
    private Charset encoding = Charset.forName("UTF-8");
    private String clientEncoding = DEFAULT_ENCODING;
    private ReflectionHelper clientProxy = new ReflectionHelper(ODBCClientRemote.class);
    private ChannelHandlerContext ctx;
    private Object message;
    private int maxLobSize = (2*1024*1024); // 2 MB
	private final int maxBufferSize;
	private boolean requireSecure;
    
	private volatile ResultsFuture<Boolean> nextFuture;

	private SSLConfiguration config;

	public PgBackendProtocol(int maxLobSize, int maxBufferSize, SSLConfiguration config, boolean requireSecure) {
    	this.maxLobSize = maxLobSize;
    	this.maxBufferSize = maxBufferSize;
    	this.config = config;
		this.requireSecure = requireSecure;
    }
    
	@Override
	public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
		this.ctx = ctx;
		this.message = msg;
		ServiceInvocationStruct serviceStruct = (ServiceInvocationStruct)msg;

		try {
			Method m = this.clientProxy.findBestMethodOnTarget(serviceStruct.methodName, serviceStruct.args);
			try {
				m.invoke(this, serviceStruct.args);
			} catch (InvocationTargetException e) {
				throw e.getCause();
			}		
		} catch (Throwable e) {
			terminate(e);
		}
	}
		
	@Override
	public void initialized(Properties props) {
		this.props = props;
		setEncoding(props.getProperty("client_encoding", this.clientEncoding), true);
	}
	
	@Override
	public void useClearTextAuthentication() {
		if (requireSecure && config != null && config.isClientEncryptionEnabled()) {
			sendErrorResponse(RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40125));
		} else {
			sendAuthenticationCleartextPassword();
		}
	}
	
	@Override
	public void useAuthenticationGSS() {
		sendAuthenticationGSS();
	}
	
	@Override
	public void authenticationGSSContinue(byte[] serviceToken) {
		sendAuthenticationGSSContinue(serviceToken);
	}
	
	@Override
	public void authenticationSucess(int processId, int screctKey) {
		sendAuthenticationOk();
		// server_version, server_encoding, client_encoding, application_name, 
		// is_superuser, session_authorization, DateStyle, IntervalStyle, TimeZone, 
		// integer_datetimes, and standard_conforming_strings. 
		// (server_encoding, TimeZone, and integer_datetimes were not reported 
		// by releases before 8.0; standard_conforming_strings was not reported by 
		// releases before 8.1; IntervalStyle was not reported by releases before 8.4; 
		// application_name was not reported by releases before 9.0.)
		
		sendParameterStatus("client_encoding", clientEncoding);
		sendParameterStatus("DateStyle", this.props.getProperty("DateStyle", "ISO"));
		sendParameterStatus("integer_datetimes", "off");
		sendParameterStatus("is_superuser", "off");
		sendParameterStatus("server_encoding", "SQL_ASCII");
		sendParameterStatus("server_version", "8.1.4");
		sendParameterStatus("session_authorization", this.props.getProperty("user"));
		sendParameterStatus("standard_conforming_strings", "on");
		sendParameterStatus(APPLICATION_NAME, this.props.getProperty(APPLICATION_NAME, DEFAULT_APPLICATION_NAME));
		
		// TODO PostgreSQL TimeZone
		sendParameterStatus("TimeZone", "CET");
		
		sendBackendKeyData(processId, screctKey);
	}

	@Override
	public void prepareCompleted(String preparedName) {
		sendParseComplete();
	}
	
	@Override
	public void bindComplete() {
		sendBindComplete();
	}

	@Override
	public void errorOccurred(String msg) {
		sendErrorResponse(msg);
	}

	@Override
	public void errorOccurred(Throwable t) {
		sendErrorResponse(t);
	}

	@Override
	public void ready(boolean inTransaction, boolean failedTransaction) {
		sendReadyForQuery(inTransaction, failedTransaction);
	}
	
	public void setEncoding(String value, boolean init) {
		if (value == null || value.equals(this.clientEncoding)) {
			return;
		}
		this.clientEncoding = value;
		Charset cs = PGCharsetConverter.getCharset(value);
		if (cs != null) {
			this.encoding = cs;
			if (!init) {
				sendParameterStatus(CLIENT_ENCODING, value);
			}
		} else {
			LogManager.logWarning(LogConstants.CTX_ODBC, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40105, value));
		}
	}
	
	public String getClientEncoding() {
		return clientEncoding;
	}
	
	public Charset getEncoding() {
		return encoding;
	}

	@Override
	public void sendParameterDescription(int[] paramType) {
		startMessage('t');
		writeShort(paramType.length);
		for (int i = 0; i < paramType.length; i++) {
			writeInt(paramType[i]);
		}
		sendMessage();
	}

	@Override
	public void sendResultSetDescription(List<PgColInfo> cols,
	        short[] resultColumnFormat) {
		sendRowDescription(cols, resultColumnFormat);
	}
	
	@Override
	public void sendResults(String sql, ResultSetImpl rs, List<PgColInfo> cols,
	        ResultsFuture<Integer> result, CursorDirection direction,
	        int rowCount, boolean describeRows, short[] resultColumnFormat) {
		if (nextFuture != null) {
			sendErrorResponse(new IllegalStateException("Pending results have not been sent")); //$NON-NLS-1$
		}
    	
    	if (describeRows) {
    		sendRowDescription(cols, resultColumnFormat);
    	}
    	ResultsWorkItem r;
		try {
			Boolean singleResult = null;
			//TODO: all of these need a non-blocking form
			if (direction != CursorDirection.FORWARD) {
				switch (direction) {
				case ABSOLUTE:
					singleResult = rs.absolute(rowCount);
					break;
				case RELATIVE:
					singleResult = rs.relative(rowCount);
					break;
				case FIRST:
					singleResult = rs.first();
					break;
				case LAST:
					singleResult = rs.last();
					break;
				}
				rowCount = 1;
			}
			r = new ResultsWorkItem(cols, rs, result, rowCount, resultColumnFormat);
	    	r.sql = sql;
	    	if (singleResult != null) {
	    		ResultsFuture<Boolean> resultsFuture = new ResultsFuture<Boolean>();
				resultsFuture.getResultsReceiver().receiveResults(singleResult);
	    		r.processRow(resultsFuture);
	    	} else {
	    		r.run();
	    	}
		} catch (SQLException e) {
			result.getResultsReceiver().exceptionOccurred(e);
		}
	}

	@Override
	public void sendUpdateCount(String sql, int updateCount) {
		sendCommandComplete(sql, updateCount);
	}

	@Override
	public void statementClosed() {
		startMessage('3');
		sendMessage();
	}

	@Override
	public void terminated() {
		trace("channel being terminated");
		// no need to send any reply; this is showing as malformed packet.
		this.ctx.channel().close();
	}
	
	@Override
	public void flush() {
		this.dataOut = null;
		this.writer = null;
		this.ctx.flush();
	}

	@Override
	public void emptyQueryReceived() {
		sendEmptyQueryResponse();
	}
	
	private void terminate(Throwable t) {
	    LogManager.logDetail(LogConstants.CTX_ODBC, "channel being terminated - ", t.getMessage());
		this.ctx.channel().close();
	}

	private void sendEmptyQueryResponse() {
		startMessage('I');
		sendMessage();
	}

	@Override
	public void sendCommandComplete(String sql, Integer count) {
		startMessage('C');
		String tag = getCompletionTag(sql, count);
		writeString(tag);
		sendMessage();
	}

	public static String getCompletionTag(String sql, Integer count) {
		String tag;
		if (StringUtil.startsWithIgnoreCase(sql, "BEGIN") || StringUtil.startsWithIgnoreCase(sql, "START TRANSACTION")) {
			tag = "BEGIN";
		} else if (sql.indexOf(' ') == -1 || sql.equals("CLOSE CURSOR")) {
			//should already be a completion tag
			tag = sql.toUpperCase();
			if (count != null) {
				tag += " " + count;
			}
		} else if (StringUtil.startsWithIgnoreCase(sql, "SET ")) {
			tag = "SET";
		} else {
			tag = SqlUtil.getKeyword(sql).toUpperCase();
			if (tag.equals("EXEC") || tag.equals("CALL")) {
				tag = "SELECT"; 
			}
			if (count != null && !(tag.equalsIgnoreCase("ROLLBACK") || tag.equalsIgnoreCase("SAVEPOINT") || tag.equalsIgnoreCase("RELEASE"))) {
				tag += " " + count;
			}
		}
		return tag;
	}

	private void sendDataRow(ResultSet rs, List<PgColInfo> cols, short[] resultColumnFormat) throws SQLException, IOException {
		startMessage('D', -1);
		int lengthIndex = this.dataOut.writerIndex() - 4;
		writeShort(cols.size());
		for (int i = 0; i < cols.size(); i++) {
			int dataBytesIndex = this.dataOut.writerIndex();
			writeInt(-1);
			if (!isBinary(cols.get(i).type)
			        || (resultColumnFormat==null || (resultColumnFormat.length==1?resultColumnFormat[0]==0:resultColumnFormat[i]==0))) {
	            getContent(rs, cols.get(i), i+1);
			} else {
                getBinaryContent(rs, cols.get(i), i+1);
			}
			writer.flush();
			if (!rs.wasNull()) {
				int bytes = this.dataOut.writerIndex() - dataBytesIndex - 4;
				this.dataOut.setInt(dataBytesIndex, bytes);
			}
		}
		this.dataOut.setInt(lengthIndex, this.dataOut.writerIndex() - lengthIndex);
	}
	
    private void getBinaryContent(ResultSet rs, PgColInfo col, int column) throws SQLException, TeiidSQLException, IOException {
	    switch (col.type) {
	    case PG_TYPE_INT2:
	        short sval = rs.getShort(column);
	        if (!rs.wasNull()) {
	            dataOut.writeShort(sval);
	        }
	        break;
        case PG_TYPE_INT4:
            int ival = rs.getInt(column);
            if (!rs.wasNull()) {
                dataOut.writeInt(ival);
            }
            break;
        case PG_TYPE_INT8:
            long lval = rs.getLong(column);
            if (!rs.wasNull()) {
                dataOut.writeLong(lval);
            }
            break;
        case PG_TYPE_FLOAT4:
            float fval = rs.getFloat(column);
            if (!rs.wasNull()) {
                dataOut.writeInt(Float.floatToIntBits(fval));
            }
            break;
        case PG_TYPE_FLOAT8:
            double dval = rs.getDouble(column);
            if (!rs.wasNull()) {
                dataOut.writeLong(Double.doubleToLongBits(dval));
            }
            break;
	    case PG_TYPE_BYTEA:
	        Blob blob = rs.getBlob(column);
            if (blob != null) {
                try {
                    byte[] bytes = ObjectConverterUtil.convertToByteArray(blob.getBinaryStream(), this.maxLobSize);
                    write(bytes);
                } catch(OutOfMemoryError e) {
                    throw new StreamCorruptedException("data too big: " + e.getMessage()); //$NON-NLS-1$ 
                }
            }
            break;
	    case PG_TYPE_DATE:
	        Date d = rs.getDate(column);
	        if (d != null) {
	            long millis = d.getTime();
	            millis += TimestampWithTimezone.getCalendar().getTimeZone().getOffset(millis);
	            long secs = TimestampUtils.toPgSecs(millis / 1000);
	            dataOut.writeInt((int) (secs / 86400));
	        }
	        break;
	    default:
	        throw new AssertionError();
	    }
	}
	
	private void getContent(ResultSet rs, PgColInfo col, int column) throws SQLException, TeiidSQLException, IOException {
		switch (col.type) {
			case PG_TYPE_BOOL:
			case PG_TYPE_BPCHAR:
		    case PG_TYPE_DATE:
		    case PG_TYPE_FLOAT4:
		    case PG_TYPE_FLOAT8:
		    case PG_TYPE_INT2:
		    case PG_TYPE_INT4:
		    case PG_TYPE_INT8:
		    case PG_TYPE_NUMERIC:
		    case PG_TYPE_TIME:
		    case PG_TYPE_TIMESTAMP_NO_TMZONE:
		    case PG_TYPE_VARCHAR:
		    	String value = rs.getString(column);
		    	if (value != null) {
			    	writer.write(value);
		    	}
		    	break;
		    case PG_TYPE_GEOMETRY:
		        Object val = rs.getObject(column);
		        if (val != null) {
	                Blob blob = GeometryUtils.geometryToEwkb((GeometryType)rs.unwrap(ResultSetImpl.class).getRawCurrentValue());
                    String hexewkb = PropertiesUtils.toHex(blob.getBytes(1, (int) blob.length()));
                    writer.write(hexewkb);
		        }
		        break;
		    case PG_TYPE_XML:
		    case PG_TYPE_TEXT:
		    	Reader r = rs.getCharacterStream(column);
		    	if (r != null) {
		    		try {
		    			ObjectConverterUtil.write(writer, r, this.maxLobSize, false);
		    		} finally {
		    			r.close();
		    		}
		    	}		        	
		    	break;
		    	
		    case PG_TYPE_BYTEA:
		    	Blob blob = rs.getBlob(column);
		    	if (blob != null) {
		    		try {
			    		String blobString = PGbytea.toPGString(ObjectConverterUtil.convertToByteArray(blob.getBinaryStream(), this.maxLobSize));
			    		writer.write(blobString);
		    		} catch(OutOfMemoryError e) {
		    			throw new StreamCorruptedException("data too big: " + e.getMessage()); //$NON-NLS-1$ 
		    		}
		    	}
		    	break;
		    	
		    case PG_TYPE_CHARARRAY:
		    case PG_TYPE_TEXTARRAY:
		    case PG_TYPE_OIDARRAY:
		    case PG_TYPE_BOOLARRAY:
		    case PG_TYPE_INT2ARRAY:
		    case PG_TYPE_INT4ARRAY:
		    case PG_TYPE_INT8ARRAY:
		    case PG_TYPE_FLOAT4ARRAY:
		    case PG_TYPE_FLOAT8ARRAY:
		    case PG_TYPE_NUMERICARRAY:
		    case PG_TYPE_DATEARRAY:
		    case PG_TYPE_TIMEARRAY:
		    case PG_TYPE_TIMESTAMP_NO_TMZONEARRAY:
		    	{
		    	Array obj = rs.getArray(column);
		    	if (obj != null) {
		    		writer.append("{");
			    	boolean first = true;
			    	Object array = obj.getArray();
					int length = java.lang.reflect.Array.getLength(array);
			    	for (int i = 0; i < length; i++) {
			    		if (!first) {
			    			writer.append(",");
			    		}
			    		else {
			    			first = false;
			    		}
			    		Object o = java.lang.reflect.Array.get(array, i);
			    		if (o != null) {
				    		if (col.type == PG_TYPE_TEXTARRAY) {
				    			escapeQuote(writer, o.toString());
				    		}
				    		else {
				    			writer.append(o.toString());
				    		}
			    		}
			    	}
			    	writer.append("}");
		    	}
		    	}
		    	break;
		    case PG_TYPE_INT2VECTOR:
		    case PG_TYPE_OIDVECTOR:
		    	{
	    		ArrayImpl obj = (ArrayImpl)rs.getObject(column);
		    	if (obj != null) {
			    	boolean first = true;
			    	for (Object o:obj.getValues()) {
			    		if (!first) {
			    			writer.append(" ");
			    		}
			    		else {
			    			first = false;
			    		}
			    		if (o != null) {
			    			writer.append(o.toString());
			    		} else {
			    			writer.append('0');
			    		}
			    	}
		    	}	
		    	}
		    	break;
		    	
		    default:
		    	Object obj = rs.getObject(column);
		    	if (obj != null) {
		    		throw new TeiidSQLException("unknown datatype "+ col.type+ " failed to convert");
		    	}
		    	break;
		}
	}
	
	public static void escapeQuote(Writer sb, String s) throws IOException {
		sb.append('"');
		for (int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			if (c == '"' || c == '\\') {
				sb.append('\\');
			}

			sb.append(c);
		}
		sb.append('"');
	}	
	
	@Override
	public void sendSslResponse() {
		SSLEngine engine = null;
		try {
			if (config != null) {
				engine = config.getServerSSLEngine();
			}
		} catch (IOException e) {
			LogManager.logError(LogConstants.CTX_ODBC, e, RuntimePlugin.Util.gs(secureData()?RuntimePlugin.Event.TEIID40122:RuntimePlugin.Event.TEIID40016));
		} catch (GeneralSecurityException e) {
			LogManager.logError(LogConstants.CTX_ODBC, e, RuntimePlugin.Util.gs(secureData()?RuntimePlugin.Event.TEIID40122:RuntimePlugin.Event.TEIID40016));
		}
		ByteBuf buffer = Unpooled.buffer(1);
		ChannelPromise promise = this.ctx.newPromise();
		if (engine == null) {
			if (secureData()) {
				sendErrorResponse(RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40124));
				return;
			}
			buffer.writeByte('N');
		} else {
			promise.addListener(new SSLEnabler(engine));
			buffer.writeByte('S');
		}
		this.ctx.writeAndFlush(buffer, promise);
	}
	
	private void sendErrorResponse(Throwable t) {
		if (t instanceof SQLException) {
			//we are just re-logging an exception raised by the engine
			if (LogManager.isMessageToBeRecorded(LogConstants.CTX_ODBC, MessageLevel.DETAIL)) {
				LogManager.logDetail(LogConstants.CTX_ODBC, t, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40020)); //$NON-NLS-1$ //$NON-NLS-2$
			}
		} else {
			//should be in the odbc layer
			LogManager.logError(LogConstants.CTX_ODBC, t, RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40015));
		}
		SQLException e = TeiidSQLException.create(t);
		startMessage('E');
		write('S');
		writeString("ERROR");
		write('C');
		writeString(e.getSQLState());
		write('M');
		writeString(e.getMessage());
		write('D');
		writeString(e.toString());
		write(0);
		sendMessage();
	}
	
	boolean isBinary(int oid) {
	    switch (oid) {
	    case PG_TYPE_INT2:
	    case PG_TYPE_INT4:
	    case PG_TYPE_INT8:
	    case PG_TYPE_FLOAT4:
	    case PG_TYPE_FLOAT8:
	    case PG_TYPE_BYTEA:
	    case PG_TYPE_DATE:
	        return true;
	    }
	    return false;
	}
	
	private void sendRowDescription(List<PgColInfo> cols, short[] resultColumnFormat) {
		if (cols == null) {
			//send NoData
			startMessage('n');
			sendMessage();
			return;
		}
		startMessage('T');
		writeShort(cols.size());
		for (int i = 0; i < cols.size(); i++) {
		    PgColInfo info = cols.get(i);
			writeString(info.name);
			// rel ID
			writeInt(info.reloid);
			// attribute number of the column
			writeShort(info.attnum);
			// data type
			writeInt(info.type);
			// pg_type.typlen
			writeShort(getTypeSize(info.type, info.precision));
			// pg_attribute.atttypmod
			writeInt(info.mod);
			if (!isBinary(info.type) || resultColumnFormat == null) {
			    //text
			    writeShort(0);
			} else {
                writeShort(resultColumnFormat[resultColumnFormat.length == 1?0:i]);
			}
		}
		sendMessage();
	}

	private int getTypeSize(int pgType, int precision) {
		switch (pgType) {
		case PG_TYPE_VARCHAR:
			return Math.max(255, precision + 10);
		default:
			return precision + 4;
		}
	}

	private void sendErrorResponse(String message) {
		LogManager.logWarning(LogConstants.CTX_ODBC, message); //$NON-NLS-1$
		startMessage('E');
		write('S');
		writeString("ERROR");
		write('C');
		// PROTOCOL VIOLATION
		writeString("08P01");
		write('M');
		writeString(message);
		write(0);
		sendMessage();
	}
	
	private void sendParseComplete() {
		startMessage('1');
		sendMessage();
	}

	private void sendBindComplete() {
		startMessage('2');
		sendMessage();
	}
	
	@Override
	public void sendPortalSuspended() {
		startMessage('s');
		sendMessage();
	}	

	private void sendAuthenticationCleartextPassword() {
		startMessage('R');
		writeInt(3);
		sendMessage();
	}
	
	private void sendAuthenticationGSS() {
		startMessage('R');
		writeInt(7);
		sendMessage();
	}
	
	private void sendAuthenticationGSSContinue(byte[] serviceToken)  {
		startMessage('R');
		writeInt(8);
		write(serviceToken);
		sendMessage();
	}	
	
	private void sendAuthenticationOk() {
		startMessage('R');
		writeInt(0);
		sendMessage();
	}

	private void sendReadyForQuery(boolean inTransaction, boolean failedTransaction) {
		startMessage('Z');
		char c;
		if (failedTransaction) {
			// failed transaction block
			c = 'E';
		}
		else {
			if (inTransaction) {
				// in a transaction block
				c = 'T';				
			} else {
				// idle
				c = 'I';				
			}
		}
		write((byte) c);
		sendMessage();
	}

	private void sendBackendKeyData(int processId, int screctKey) {
		startMessage('K');
		writeInt(processId);
		writeInt(screctKey);
		sendMessage();
	}

	@Override
	public void sendParameterStatus(String param, String value) {
		startMessage('S');
		writeString(param);
		writeString(value);
		sendMessage();
	}
	
	@Override
	public void functionCallResponse(byte[] data) {
		startMessage('V');
		if (data == null) {
			writeInt(-1);
		}
		else {
			writeInt(data.length);
			write(data);
		}
		sendMessage();
	}
	
	@Override
	public void functionCallResponse(int data) {
		startMessage('V');
		writeInt(4);
		writeInt(data);
		sendMessage();
	}

	private void writeString(String s) {
		write(s.getBytes(this.encoding));
		write(0);
	}

	private void writeInt(int i) {
		dataOut.writeInt(i);
	}

	private void writeShort(int i) {
		dataOut.writeShort(i);
	}

	private void write(byte[] data) {
		dataOut.writeBytes(data);
	}

	private void write(int b) {
		dataOut.writeByte(b);
	}

	private void startMessage(char newMessageType) {
		startMessage(newMessageType, 32);
	}

	private void startMessage(char newMessageType, int estimatedLength) {
		if (estimatedLength > -1) {
			initBuffer(estimatedLength);
		}
		this.dataOut.writeByte((byte)newMessageType);
		int nextByte = this.dataOut.writerIndex() + 4;
		this.dataOut.ensureWritable(nextByte);
		this.dataOut.writerIndex(nextByte);
	}

	private void initBuffer(int estimatedLength) {
		this.dataOut = Unpooled.buffer(estimatedLength).order(ByteOrder.BIG_ENDIAN);
		ByteBufOutputStream cbos = new ByteBufOutputStream(this.dataOut);
		this.writer = new OutputStreamWriter(cbos, this.encoding);
	}

	private void sendMessage() {
		int pos = this.dataOut.writerIndex();
		this.dataOut.setInt(1, pos - 1);
		sendContents();
	}
	
	private void sendContents() {
		ByteBuf cb = this.dataOut;
		this.dataOut = null;
		this.writer = null;
		this.ctx.writeAndFlush(cb);
	}

	private static void trace(String... msg) {
		LogManager.logTrace(LogConstants.CTX_ODBC, (Object[])msg);
	}
	
	public boolean secureData() {
		return requireSecure && config != null && config.isSslEnabled();
	}
	
}
