/*
 * JBoss, Home of Professional Open Source
 *
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * by the @author tags. See the COPYRIGHT.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.teiid.netty.handler.codec.serialization;

import java.io.*;
import java.nio.charset.Charset;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.teiid.client.DQP;
import org.teiid.client.RequestMessage;
import org.teiid.client.ResultsMessage;
import org.teiid.client.lob.LobChunk;
import org.teiid.client.metadata.MetadataResult;
import org.teiid.client.metadata.ParameterInfo;
import org.teiid.client.plan.Annotation;
import org.teiid.client.plan.PlanNode;
import org.teiid.client.security.ILogon;
import org.teiid.client.security.InvalidSessionException;
import org.teiid.client.security.LogonException;
import org.teiid.client.security.LogonResult;
import org.teiid.client.security.SessionToken;
import org.teiid.client.security.TeiidSecurityException;
import org.teiid.client.util.ExceptionHolder;
import org.teiid.client.xa.XATransactionException;
import org.teiid.client.xa.XidImpl;
import org.teiid.core.ComponentNotFoundException;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidException;
import org.teiid.core.TeiidProcessingException;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.types.*;
import org.teiid.core.types.InputStreamFactory.StreamFactoryReference;
import org.teiid.core.util.ReaderInputStream;
import org.teiid.jdbc.JDBCPlugin;
import org.teiid.net.socket.Handshake;
import org.teiid.net.socket.Message;
import org.teiid.net.socket.ServiceInvocationStruct;


/**
 * @author The Netty Project (netty-dev@lists.jboss.org)
 * @author Trustin Lee (tlee@redhat.com)
 *
 * @version $Rev: 6 $, $Date: 2008-08-07 20:40:10 -0500 (Thu, 07 Aug 2008) $
 *
 */
public class CompactObjectOutputStream extends ObjectOutputStream {

    static final int TYPE_PRIMITIVE = 0;
    static final int TYPE_NON_PRIMITIVE = 1;
    
    public static final Map<Class<?>, Integer> KNOWN_CLASSES = new HashMap<Class<?>, Integer>();
    public static final Map<Integer, Class<?>> KNOWN_CODES = new HashMap<Integer, Class<?>>();
    
    private List<InputStream> streams = new LinkedList<InputStream>();
    private List<StreamFactoryReference> references = new LinkedList<StreamFactoryReference>();
    
    public static void addKnownClass(Class<?> clazz, byte code) {
    	KNOWN_CLASSES.put(clazz, Integer.valueOf(code));
    	if (KNOWN_CODES.put(Integer.valueOf(code), clazz) != null) {
    		 throw new TeiidRuntimeException(JDBCPlugin.Event.TEIID20007, JDBCPlugin.Util.gs(JDBCPlugin.Event.TEIID20007));
    	}
    }
    
    static {
    	addKnownClass(ServiceInvocationStruct.class, (byte)2);
    	addKnownClass(Handshake.class, (byte)3);
    	addKnownClass(Message.class, (byte)4);
    	addKnownClass(SerializableReader.class, (byte)5);
    	addKnownClass(SerializableInputStream.class, (byte)6);
    	
    	addKnownClass(DQP.class, (byte)10);
    	addKnownClass(LobChunk.class, (byte)11);
    	addKnownClass(RequestMessage.class, (byte)12);
    	addKnownClass(ResultsMessage.class, (byte)13);
    	addKnownClass(PlanNode.class, (byte)14);
    	addKnownClass(PlanNode.Property.class, (byte)15);
    	addKnownClass(Annotation.class, (byte)16);
    	addKnownClass(MetadataResult.class, (byte)17);
    	addKnownClass(ParameterInfo.class, (byte)18);
    	addKnownClass(XidImpl.class, (byte)19);
    	addKnownClass(BlobImpl.class, (byte)20);
    	addKnownClass(ClobImpl.class, (byte)21);
    	addKnownClass(SQLXMLImpl.class, (byte)22);
    	addKnownClass(BlobType.class, (byte)23);
    	addKnownClass(ClobType.class, (byte)24);
    	addKnownClass(XMLType.class, (byte)25);
    	addKnownClass(XATransactionException.class, (byte)26);
    	
    	addKnownClass(ILogon.class, (byte)30);
    	addKnownClass(LogonResult.class, (byte)31);
    	addKnownClass(SessionToken.class, (byte)32);
    	addKnownClass(LogonException.class, (byte)33);
    	addKnownClass(TeiidSecurityException.class, (byte)34);
    	addKnownClass(InvalidSessionException.class, (byte)35);
    	
    	addKnownClass(ExceptionHolder.class, (byte)40);
    	addKnownClass(TeiidRuntimeException.class, (byte)41);
    	addKnownClass(TeiidComponentException.class, (byte)42);
    	addKnownClass(TeiidException.class, (byte)43);
    	addKnownClass(TeiidProcessingException.class, (byte)44);
    	addKnownClass(ComponentNotFoundException.class, (byte)45);
    }
    
    public CompactObjectOutputStream(OutputStream out) throws IOException {
        super(out);
        enableReplaceObject(true);
    }
    
    public List<InputStream> getStreams() {
		return streams;
	}
    
    @Override
    public void reset() throws IOException {
    	super.reset();
    	streams.clear();
    	references.clear();
    }
    
    public List<StreamFactoryReference> getReferences() {
		return references;
	}

    @Override
    protected void writeStreamHeader() throws IOException {
        writeByte(STREAM_VERSION);
    }

    @Override
    protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException {
        if (desc.forClass().isPrimitive() 
        		|| !(Externalizable.class.isAssignableFrom(desc.forClass()))) {
            write(TYPE_PRIMITIVE);
            super.writeClassDescriptor(desc);
        } else {
        	Integer b = KNOWN_CLASSES.get(desc.forClass());
        	if (b != null) {
        		write(b.intValue());
        	} else {
	            write(TYPE_NON_PRIMITIVE);
	            writeUTF(desc.getName());
        	}
        }
    }
        
    @Override
    protected Object replaceObject(Object obj) throws IOException {
    	if (obj instanceof BaseLob) {
    		try {
		    	if (obj instanceof SQLXMLImpl) {
					streams.add(((SQLXMLImpl)obj).getBinaryStream());
		    		StreamFactoryReference sfr = new SQLXMLImpl();
		    		references.add(sfr);
		    		return sfr;
		    	} else if (obj instanceof ClobImpl) {
		    		streams.add(new ReaderInputStream(((ClobImpl)obj).getCharacterStream(), Charset.forName(Streamable.ENCODING)));
		    		StreamFactoryReference sfr = new ClobImpl();
		    		references.add(sfr);
		    		return sfr;
		    	} else if (obj instanceof BlobImpl) {
		    		streams.add(((Blob)obj).getBinaryStream());
		    		StreamFactoryReference sfr = new BlobImpl();
		    		references.add(sfr);
		    		return sfr;
		    	}
    		} catch (SQLException e) {
    			throw new IOException(e);
    		}
    	}
    	else if (obj instanceof Serializable) {
    		return obj;
    	}
    	else {
			try {
		    	if (obj instanceof Reader) {
		    		streams.add(new ReaderInputStream((Reader)obj, Charset.forName(Streamable.ENCODING)));
		    		StreamFactoryReference sfr = new SerializableReader();
		    		references.add(sfr);
		    		return sfr;
		    	} else if (obj instanceof InputStream) {
		    		streams.add((InputStream)obj);
		    		StreamFactoryReference sfr = new SerializableInputStream();
		    		references.add(sfr);
		    		return sfr;
		    	} else if (obj instanceof SQLXML) {
					streams.add(((SQLXML)obj).getBinaryStream());
		    		StreamFactoryReference sfr = new SQLXMLImpl();
		    		references.add(sfr);
		    		return sfr;
		    	} else if (obj instanceof Clob) {
		    		streams.add(new ReaderInputStream(((Clob)obj).getCharacterStream(), Charset.forName(Streamable.ENCODING)));
		    		StreamFactoryReference sfr = new ClobImpl();
		    		references.add(sfr);
		    		return sfr;
		    	} else if (obj instanceof Blob) {
		    		streams.add(((Blob)obj).getBinaryStream());
		    		StreamFactoryReference sfr = new BlobImpl();
		    		references.add(sfr);
		    		return sfr;
		    	}
			} catch (SQLException e) {
				throw new IOException(e);
			}
    	}
    	return super.replaceObject(obj);
    }
    
    static class SerializableInputStream extends InputStream implements Externalizable, StreamFactoryReference {

		private InputStreamFactory isf;
    	private InputStream is;
    	
    	public SerializableInputStream() {
		}
    	
    	public void setStreamFactory(InputStreamFactory streamFactory) {
    		this.isf = streamFactory;
    	}
    	
		@Override
		public int read() throws IOException {
			if (is == null) {
				is = isf.getInputStream();
			}
			return is.read();
		}
		
		@Override
		public void close() throws IOException {
			isf.free();
		}

		@Override
		public void readExternal(ObjectInput in) throws IOException,
				ClassNotFoundException {
		}

		@Override
		public void writeExternal(ObjectOutput out) throws IOException {
		}
    }
    
    static class SerializableReader extends Reader implements Externalizable, StreamFactoryReference {

		private InputStreamFactory isf;
    	private Reader r;
    	
    	public SerializableReader() {
		}
    	
    	public void setStreamFactory(InputStreamFactory streamFactory) {
    		this.isf = streamFactory;
    	}

		@Override
		public void close() throws IOException {
			isf.free();
		}

		@Override
		public int read(char[] cbuf, int off, int len) throws IOException {
			if (r == null) {
				r = new InputStreamReader(isf.getInputStream(), Streamable.ENCODING);
			}
			return r.read(cbuf, off, len);
		}

		@Override
		public void readExternal(ObjectInput in) throws IOException,
				ClassNotFoundException {
		}

		@Override
		public void writeExternal(ObjectOutput out) throws IOException {
		}
    }
    
}
