/*
 * 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.core.types;

import java.io.*;
import java.sql.Clob;
import java.sql.NClob;
import java.sql.SQLException;

import org.teiid.core.CorePlugin;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.util.EquivalenceUtil;
import org.teiid.core.util.ExternalizeUtil;
import org.teiid.core.util.HashCodeUtil;
import org.teiid.core.util.ObjectConverterUtil;


/**
 * This is wrapper on top of a "clob" object, which implements the "java.sql.Clob"
 * interface. This class also implements the Streamable interface
 */
public final class ClobType extends Streamable<Clob> implements NClob, Sequencable, Comparable<ClobType> {
	
	public enum Type {
		TEXT, JSON
	}

	private static final long serialVersionUID = 2753412502127824104L;
    
	private Type type = Type.TEXT;

    private int hash;
	
    public ClobType() {
    }
    
    public ClobType(Clob clob) {
    	super(clob);
    }
    
    /** 
     * @see java.sql.Clob#getAsciiStream()
     */
    public InputStream getAsciiStream() throws SQLException {
        return this.reference.getAsciiStream();
    }

    /** 
     * @see java.sql.Clob#getCharacterStream()
     */
    public Reader getCharacterStream() throws SQLException {
        return this.reference.getCharacterStream();
    }

    /** 
     * @see java.sql.Clob#getSubString(long, int)
     */
    public String getSubString(long pos, int len) throws SQLException {
        return this.reference.getSubString(pos, len);
    }
    
    @Override
    long computeLength() throws SQLException {
        return this.reference.length();
    }

    /** 
     * @see java.sql.Clob#position(java.sql.Clob, long)
     */
    public long position(Clob searchstr, long start) throws SQLException {
        return this.reference.position(searchstr, start);
    }

    /** 
     * @see java.sql.Clob#position(java.lang.String, long)
     */
    public long position(String searchstr, long start) throws SQLException {
        return this.reference.position(searchstr, start);
    }

    /** 
     * @see java.sql.Clob#setAsciiStream(long)
     */
    public OutputStream setAsciiStream(long pos) throws SQLException {
        return this.reference.setAsciiStream(pos);
    }

    /** 
     * @see java.sql.Clob#setCharacterStream(long)
     */
    public Writer setCharacterStream(long pos) throws SQLException {
        return this.reference.setCharacterStream(pos);
    }

    /** 
     * @see java.sql.Clob#setString(long, java.lang.String, int, int)
     */
    public int setString(long pos,
                         String str,
                         int offset,
                         int len) throws SQLException {
        return this.reference.setString(pos, str, offset, len);
    }

    /** 
     * @see java.sql.Clob#setString(long, java.lang.String)
     */
    public int setString(long pos, String str) throws SQLException {
        return this.reference.setString(pos, str);
    }

    /** 
     * @see java.sql.Clob#truncate(long)
     */
    public void truncate(long len) throws SQLException {
        this.reference.truncate(len);
    }    

    /**
     * Utility method to convert to String  
     * @param clob
     * @return string form of the clob passed.
     */
    public static String getString(Clob clob) throws SQLException, IOException {
        Reader reader = clob.getCharacterStream();
        try {
	        StringWriter writer = new StringWriter();
	        int c = reader.read();
	        while (c != -1) {
	            writer.write((char)c);
	            c = reader.read();
	        }
	        reader.close();
	        String data = writer.toString();
	        writer.close();
	        return data;        
        } finally {
        	reader.close();
        }
    }
    
    private final static int CHAR_SEQUENCE_BUFFER_SIZE = 1 << 12;
    
    public CharSequence getCharSequence() {
        return new CharSequence() {

        	private char[] buffer = new char[CHAR_SEQUENCE_BUFFER_SIZE];
        	private int bufLength;
        	private Reader reader;
        	private int beginPosition;
        	        	
            public int length() {
                long result;
                try {
                    result = ClobType.this.length();
                } catch (SQLException err) {
                      throw new TeiidRuntimeException(CorePlugin.Event.TEIID10051, err);
                } 
                if (((int)result) != result) {
                      throw new TeiidRuntimeException(CorePlugin.Event.TEIID10052, CorePlugin.Util.gs(CorePlugin.Event.TEIID10052));
                }
                return (int)result;
            }

            public char charAt(int index) {
                try {
                    if ((reader == null || index < beginPosition) && reader != null) {
                        reader.close();
                        reader = null;
                    }
                	if (buffer == null || index < beginPosition || index >= beginPosition + bufLength) {
                	    if (reference instanceof ClobImpl) {
                    	    if (reader == null) {
                                reader = getCharacterStream();
                    	    }
                    	    bufLength = reader.read(buffer, 0, buffer.length);
                	    } else {
                	        String stringBuffer = ClobType.this.getSubString(index + 1, CHAR_SEQUENCE_BUFFER_SIZE);
                	        bufLength = stringBuffer.length();
                	        buffer = stringBuffer.toCharArray();
                	    }
                        beginPosition = index;
                	}
                	return buffer[index - beginPosition];
                } catch (IOException err) {
                    throw new TeiidRuntimeException(CorePlugin.Event.TEIID10053, err);
                } catch (SQLException err) {
                      throw new TeiidRuntimeException(CorePlugin.Event.TEIID10053, err);
                } 
            }

            public CharSequence subSequence(int start,
                                            int end) {
                try {
                    return ClobType.this.getSubString(start + 1, end - start);
                } catch (SQLException err) {
                      throw new TeiidRuntimeException(CorePlugin.Event.TEIID10054, err);
                }
            }
            
        };
    }

    public void free() throws SQLException {
		this.reference.free();
	}

	public Reader getCharacterStream(long pos, long len) throws SQLException {
		return this.reference.getCharacterStream(pos, len);
	}
	
	@Override
	protected void readReference(ObjectInput in) throws IOException {
		char[] chars = new char[(int)length];
		for (int i = 0; i < chars.length; i++) {
			chars[i] = in.readChar();
		}
		this.reference = ClobImpl.createClob(chars);
	}
	
	/**
	 * Since we have the length in chars we'll just write out in double byte format.
	 * These clobs should be small, so the wasted space should be minimal.
	 */
	@Override
	protected void writeReference(final DataOutput out) throws IOException {
		Writer w = new Writer() {
			
			@Override
			public void write(char[] cbuf, int off, int len) throws IOException {
				for (int i = off; i < len; i++) {
					out.writeChar(cbuf[i]);
				}
			}
			
			@Override
			public void flush() throws IOException {
			}
			
			@Override
			public void close() throws IOException {
			}
		};
		Reader r;
		try {
			r = getCharacterStream();
		} catch (SQLException e) {
			throw new IOException(e);
		}
		try {
			int chars = ObjectConverterUtil.write(w, r, (int)length, false);
			if (length != chars) {
				throw new IOException("Expected length " + length + " but was " + chars + " for " + this.reference); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			}
		} finally {
			r.close();
		}
	}
	
	@Override
	public int compareTo(ClobType o) {
		try {
    		Reader cs1 = this.getCharacterStream();
    		Reader cs2 = o.getCharacterStream();
    		long len1 = this.length();
    		long len2 = o.length();
    		long n = Math.min(len1, len2);
		    for (long i = 0; i < n; i++) {
				int c1 = cs1.read();
				int c2 = cs2.read();
				if (c1 != c2) {
				    return c1 - c2;
				}
		    }
    		return Long.signum(len1 - len2);
		} catch (SQLException e) {
			  throw new TeiidRuntimeException(CorePlugin.Event.TEIID10056, e);
		} catch (IOException e) {
			  throw new TeiidRuntimeException(CorePlugin.Event.TEIID10057, e);
		}
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (!(obj instanceof ClobType)) {
			return false;
		}
		ClobType other = (ClobType)obj;
		if (EquivalenceUtil.areEqual(reference, other.reference)) {
			return true;
		}
		try {
		    if (length() != other.length()) {
		        return false;
		    }
			return this.compareTo(other) == 0;
		} catch (SQLException e) {
		    return false;
		} catch (TeiidRuntimeException e) {
			return false;
		}
	}
	
	@Override
	public int hashCode() {
	    try {
	        if (hash == 0) {
	            hash = HashCodeUtil.expHashCode(this.getCharSequence());
	        }
	        return hash;
	    } catch (TeiidRuntimeException e) {
	        return 0;
	    }
	}
	
	public Type getType() {
		return type;
	}
	
	public void setType(Type type) {
		this.type = type;
	}
	
	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		super.readExternal(in);
		try {
			this.type = ExternalizeUtil.readEnum(in, Type.class, Type.TEXT);
		} catch (OptionalDataException e) {
			this.type = Type.TEXT;
		} catch(IOException e) {
			this.type = Type.TEXT;
		}
	}
	
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		super.writeExternal(out);
		ExternalizeUtil.writeEnum(out, this.type);
	}

}
