/*
 * 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.translator.accumulo;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Arrays;

import org.apache.accumulo.core.client.lexicoder.BigIntegerLexicoder;
import org.apache.accumulo.core.client.lexicoder.BytesLexicoder;
import org.apache.accumulo.core.client.lexicoder.DateLexicoder;
import org.apache.accumulo.core.client.lexicoder.DoubleLexicoder;
import org.apache.accumulo.core.client.lexicoder.IntegerLexicoder;
import org.apache.accumulo.core.client.lexicoder.LongLexicoder;
import org.apache.accumulo.core.client.lexicoder.StringLexicoder;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.types.BinaryType;
import org.teiid.core.types.BlobImpl;
import org.teiid.core.types.BlobType;
import org.teiid.core.types.ClobImpl;
import org.teiid.core.types.DataTypeManager;
import org.teiid.core.types.GeometryType;
import org.teiid.core.types.InputStreamFactory;
import org.teiid.core.types.SQLXMLImpl;
import org.teiid.core.types.TransformationException;
import org.teiid.core.util.ObjectConverterUtil;

public class AccumuloDataTypeManager {
	public static byte[] EMPTY_BYTES = new byte[0];
	private static BytesLexicoder bytesLexicoder = new BytesLexicoder();
	private static BigIntegerLexicoder bigIntegerLexicoder = new BigIntegerLexicoder();
	private static DateLexicoder dateLexicoder = new DateLexicoder();
	private static DoubleLexicoder doubleLexicoder = new DoubleLexicoder();
	private static IntegerLexicoder integerLexicoder = new IntegerLexicoder();
	private static LongLexicoder longLexicoder = new LongLexicoder();
	private static StringLexicoder stringLexicoder = new StringLexicoder();
	private static Charset UTF_8 = Charset.forName("UTF-8");
	public static byte[] serialize(Object value) {
        if (value == null) {
            return EMPTY_BYTES;
        }
	    
        try {
            if (value instanceof Clob) {
                // TODO:Accumulo streaming support would have been good?
                // this type materialization of the value is BAD
                Clob clob = (Clob)value;
                return ObjectConverterUtil.convertToByteArray(clob.getAsciiStream());
            } else if (value instanceof GeometryType) {
                GeometryType geometry = (GeometryType)value;
                //TODO: handle srid
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectConverterUtil.write(baos, geometry.getBinaryStream(), -1, true);
                int srid = geometry.getSrid();
                baos.write((srid >>> 24) & 0xFF);
                baos.write((srid >>> 16) & 0xFF);
                baos.write((srid >>>  8) & 0xFF);
                baos.write((srid >>>  0) & 0xFF);
                return baos.toByteArray();
            } else if (value instanceof Blob) {
                // TODO: same as CLOB
                Blob blob = (Blob)value;
                return ObjectConverterUtil.convertToByteArray(blob.getBinaryStream());
            } else if (value instanceof SQLXML) {
                // TODO: same as CLOB
                SQLXML xml = (SQLXML)value;
                return ObjectConverterUtil.convertToByteArray(xml.getBinaryStream());
            } else if (value instanceof BinaryType) {
                BinaryType binary = (BinaryType)value;
                return binary.getBytes();
            } else if (value instanceof byte[]) {
                return bytesLexicoder.encode((byte[])value);
            }
            else if (value instanceof Object[] ) {
                throw new TeiidRuntimeException(AccumuloPlugin.Event.TEIID19003, 
                        AccumuloPlugin.Util.gs(AccumuloPlugin.Event.TEIID19003));
            }            
            else if (value instanceof String ||
                        value instanceof Boolean ||
                        value instanceof Byte ||
                        value instanceof Short ||
                        value instanceof Character ||
                        value instanceof Integer ||
                        value instanceof Long ||
                        value instanceof BigInteger ||
                        value instanceof BigDecimal ||
                        value instanceof Float ||
                        value instanceof Double ||
                        value instanceof Date ||
                        value instanceof Time ||
                        value instanceof Timestamp) {
                return ((String)DataTypeManager.transformValue(value, String.class)).getBytes(UTF_8);
            }
            else {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(value);
                oos.close();
                baos.close();
                return baos.toByteArray();
            }
        } catch (IOException e) {
            throw new TeiidRuntimeException(e);
        } catch (SQLException e) {
            throw new TeiidRuntimeException(e);
        } catch (TransformationException e) {
            throw new TeiidRuntimeException(e);
        }        
	}
	
    public static Object deserialize(final byte[] value, final Class<?> expectedType) {
        if (value == null || Arrays.equals(value, EMPTY_BYTES)) {
            return null;
        }
        
        try {
            if (expectedType.isAssignableFrom(Clob.class)) {
                return new ClobImpl(new InputStreamFactory() {
                    @Override
                    public InputStream getInputStream() throws IOException {
                        return ObjectConverterUtil.convertToInputStream(value);
                    }
                }, -1);           
            } else if (expectedType.isAssignableFrom(Blob.class)) {
                return new BlobType(new BlobImpl(new InputStreamFactory() {
                    @Override
                    public InputStream getInputStream() throws IOException {
                        return ObjectConverterUtil.convertToInputStream(value);
                    }
                }));            
            } else if (expectedType.isAssignableFrom(SQLXML.class)) {
                return new SQLXMLImpl(new InputStreamFactory() {
                    @Override
                    public InputStream getInputStream() throws IOException {
                        return ObjectConverterUtil.convertToInputStream(value);
                    }
                });            
            } else if (expectedType.isAssignableFrom(BinaryType.class)) {
            	
                return new BinaryType(value);
            } else if (expectedType.isAssignableFrom(GeometryType.class)) {
                GeometryType result = new GeometryType(Arrays.copyOf(value, value.length -4));
                int srid = (((value[value.length - 4] & 0xff) << 24) + 
                		((value[value.length - 3] & 0xff) << 16) + 
                		((value[value.length - 2] & 0xff) << 8) + 
                		((value[value.length - 1] & 0xff) << 0));
                result.setSrid(srid);
                return result;
            } else if (expectedType.isAssignableFrom(byte[].class)) {
                return value;
            } else if (expectedType.isAssignableFrom(String.class)
                    || expectedType.isAssignableFrom(Boolean.class)
                    || expectedType.isAssignableFrom(Boolean.class)
                    || expectedType.isAssignableFrom(Byte.class)
                    || expectedType.isAssignableFrom(Short.class)
                    || expectedType.isAssignableFrom(Character.class)
                    || expectedType.isAssignableFrom(Integer.class)
                    || expectedType.isAssignableFrom(Long.class)
                    || expectedType.isAssignableFrom(BigInteger.class)
                    || expectedType.isAssignableFrom(BigDecimal.class)
                    || expectedType.isAssignableFrom(Float.class)
                    || expectedType.isAssignableFrom(Double.class)
                    || expectedType.isAssignableFrom(Date.class)
                    || expectedType.isAssignableFrom(Time.class)
                    || expectedType.isAssignableFrom(Timestamp.class)) {
                return DataTypeManager.transformValue(new String(value, UTF_8), expectedType);
            }
            else {
                ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(value));
                Object obj = ois.readObject();
                ois.close();
                return obj;
            }
        } catch (ClassNotFoundException e) {
            throw new TeiidRuntimeException(e);
        } catch (IOException e) {
            throw new TeiidRuntimeException(e);
        } catch (TransformationException e) {
            throw new TeiidRuntimeException(e);
        }
    }	
	
	private static byte[] toLexiCode(Object value) {
		if (value == null) {
			return EMPTY_BYTES;
		}
		
		try {
			if (value instanceof java.sql.Date
                                || value instanceof java.sql.Timestamp
                                || value instanceof java.sql.Time) {
			    return dateLexicoder.encode((java.util.Date)value);
			} else if (value instanceof Long) {
			    return longLexicoder.encode((Long)value);
			} else if (value instanceof Double) {
			    return doubleLexicoder.encode((Double)value);
			} else if (value instanceof Float) {
			    return doubleLexicoder.encode(((Float)value).doubleValue());
			} else if (value instanceof Integer) {
			    return integerLexicoder.encode((Integer)value);
			} else if (value instanceof BigInteger) {
			    return bigIntegerLexicoder.encode((BigInteger)value);
			} else if (value instanceof BigDecimal) {
			    return stringLexicoder.encode(((BigDecimal)value).toPlainString());
			} else if (value instanceof Byte) {
			    return integerLexicoder.encode(((Byte)value).intValue());
			} else if (value instanceof Short) {
			    return integerLexicoder.encode(((Short)value).intValue());
			} else if (value instanceof Clob) {
                // TODO:Accumulo streaming support would have been good?
                // this type materialization of the value is BAD
			    Clob clob = (Clob)value;
			    return bytesLexicoder.encode(ObjectConverterUtil.convertToByteArray(clob.getAsciiStream()));
			} else if (value instanceof Blob) {
                // TODO: same as CLOB
                Blob blob = (Blob)value;
                return bytesLexicoder.encode(ObjectConverterUtil.convertToByteArray(blob.getBinaryStream()));
            }  else if (value instanceof SQLXML) {
                // TODO: same as CLOB
                SQLXML xml = (SQLXML)value;
                return bytesLexicoder.encode(ObjectConverterUtil.convertToByteArray(xml.getBinaryStream()));
            } else if (value instanceof BinaryType) {
                BinaryType binary = (BinaryType)value;
                return bytesLexicoder.encode(binary.getBytes());
            } else if (value instanceof GeometryType) {
                GeometryType geometry = (GeometryType)value;
                return bytesLexicoder.encode(ObjectConverterUtil.convertToByteArray(geometry.getBinaryStream()));
            }  else if (value instanceof byte[]) {
                return bytesLexicoder.encode((byte[])value);
            }
			else if (value instanceof Object[] ) {
				throw new TeiidRuntimeException(AccumuloPlugin.Event.TEIID19003, AccumuloPlugin.Util.gs(AccumuloPlugin.Event.TEIID19003));
			}
			return stringLexicoder.encode(((String)DataTypeManager.transformValue(value, String.class)));
		} catch (TransformationException e) {
			throw new TeiidRuntimeException(e);
		} catch (SQLException e) {
			throw new TeiidRuntimeException(e);
		} catch (IOException e) {
			throw new TeiidRuntimeException(e);
		}
	}
		
	private static Object fromLexiCode(final byte[] value, final Class<?> expectedType) {
		if (value == null || Arrays.equals(value, EMPTY_BYTES)) {
			return null;
		}
		
		if (expectedType.isAssignableFrom(String.class)) {
			return stringLexicoder.decode(value);
		} else if (expectedType.isAssignableFrom(java.sql.Date.class)) {
            return new java.sql.Date(dateLexicoder.decode(value).getTime());
        } else if (expectedType.isAssignableFrom(java.sql.Timestamp.class)) {
            return new java.sql.Timestamp(dateLexicoder.decode(value).getTime());
        } else if (expectedType.isAssignableFrom(java.sql.Time.class)) {
            return new java.sql.Time(dateLexicoder.decode(value).getTime());
        } else if (expectedType.isAssignableFrom(Long.class)) {
            return longLexicoder.decode(value);
        } else if (expectedType.isAssignableFrom( Double.class)) {
            return doubleLexicoder.decode(value);
        } else if (expectedType.isAssignableFrom(Float.class)) {
            return doubleLexicoder.decode(value).floatValue();
        } else if (expectedType.isAssignableFrom(Integer.class)) {
            return integerLexicoder.decode(value);
        } else if (expectedType.isAssignableFrom(BigInteger.class)) {
            return bigIntegerLexicoder.decode(value);
        } else if (expectedType.isAssignableFrom(BigDecimal.class)) {
            return new BigDecimal(stringLexicoder.decode(value));
        } else if (expectedType.isAssignableFrom(Byte.class)) {
            return integerLexicoder.decode(value).byteValue();
        } else if (expectedType.isAssignableFrom(Short.class)) {
            return integerLexicoder.decode(value).shortValue();
        } else if (expectedType.isAssignableFrom(Clob.class)) {
            return new ClobImpl(new InputStreamFactory() {
                @Override
                public InputStream getInputStream() throws IOException {
                    return ObjectConverterUtil.convertToInputStream(bytesLexicoder.decode(value));
                }
            }, -1);           
        } else if (expectedType.isAssignableFrom(Blob.class)) {
            return new BlobType(new BlobImpl(new InputStreamFactory() {
                @Override
                public InputStream getInputStream() throws IOException {
                    return ObjectConverterUtil.convertToInputStream(bytesLexicoder.decode(value));
                }
                
            }));            
        } else if (expectedType.isAssignableFrom(SQLXML.class)) {
            return new SQLXMLImpl(new InputStreamFactory() {
                @Override
                public InputStream getInputStream() throws IOException {
                    return ObjectConverterUtil.convertToInputStream(bytesLexicoder.decode(value));
                }
            });            
        } else if (expectedType.isAssignableFrom(BinaryType.class)) {
            return new BinaryType(bytesLexicoder.decode(value));
        } else if (expectedType.isAssignableFrom(GeometryType.class)) {
            return new GeometryType(bytesLexicoder.decode(value));
        } else if (expectedType.isAssignableFrom(byte[].class)) {
            return bytesLexicoder.decode(value);
        }
        else {
            throw new TeiidRuntimeException(AccumuloPlugin.Event.TEIID19004,
                    AccumuloPlugin.Util.gs(AccumuloPlugin.Event.TEIID19004,
                            expectedType.getName()));    
        }
	}	
}
