

package gov.va.med.cds.persistence.hibernate.common;

import gov.va.med.cds.template.generated.basedatatypes.PointInTime;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.DoubleType;
import org.hibernate.type.StringType;
import org.hibernate.type.Type;
import org.hibernate.usertype.CompositeUserType;
import org.hibernate.usertype.ParameterizedType;


public class PointInTimeUserType
    implements
        CompositeUserType,
        ParameterizedType
{
    public static final String SET_VALUES = "setValues";

    public static final String SV_ALL = "all";
    public static final String SV_LITERAL_ONLY = "literalOnly";
    public static final String SV_NUMERIC_ONLY = "numericOnly";

    private String setValues;


    public void setParameterValues( Properties properties )
    {
        if ( properties != null )
        {
            setValues = properties.getProperty( SET_VALUES );
        }
    }


    public Object assemble( Serializable cached, SessionImplementor sessionImplementor, Object owner )
        throws HibernateException
    {
        return cached;
    }


    public Object deepCopy( Object value )
        throws HibernateException
    {
        return value;
    }


    public Serializable disassemble( Object value, SessionImplementor sessionImplementor )
        throws HibernateException
    {
        return ( Serializable )value;
    }


    public String[] getPropertyNames( )
    {
        return new String[] { "timestamp", "numeric" };
    }


    public Type[] getPropertyTypes( )
    {
        if ( this.setValues != null )
        {
            if ( isSetLiteral() )
            {
                return new Type[] { StringType.INSTANCE };
            }
            else if ( isSetNumeric() )
            {
                return new Type[] { DoubleType.INSTANCE };
            }
        }

        return new Type[] { StringType.INSTANCE, DoubleType.INSTANCE };
    }


    public Object getPropertyValue( Object value, int property )
        throws HibernateException
    {
        if ( value instanceof gov.va.med.cds.template.generated.basedatatypes.PointInTime)
        {
            PointInTime pointInTime = ( PointInTime )value;
            //return ( property == 1 ) ? isoTime2Double( pointInTime.getTimestamp() ) : pointInTime.getTimestamp();
            return ( property == 1 ) ? isoTime2Double( pointInTime.getLiteral() ) : pointInTime.getLiteral();
        }

        return null;
    }


    public void setPropertyValue( Object component, int property, Object value )
        throws HibernateException
    {
        throw new UnsupportedOperationException( "Immutable class PointInTime." );
    }


    public boolean equals( Object x, Object y )
        throws HibernateException
    {
        if ( x == null || y == null )
            return true;
        if ( !( x instanceof PointInTime ) || !( y instanceof PointInTime ) )
            return false;
        if ( x == y )
            return true;

        PointInTime pointInTimeX = ( PointInTime )x;
        PointInTime pointInTimeY = ( PointInTime )y;

        //if ( pointInTimeX.getTimestamp() == null || pointInTimeY.getTimestamp() == null )
        if ( pointInTimeX.getLiteral() == null || pointInTimeY.getLiteral()== null )
            return false;

        //return pointInTimeX.getTimestamp().equals( pointInTimeY.getTimestamp() );
        return pointInTimeX.getLiteral().equals( pointInTimeY.getLiteral() );
    }


    public int hashCode( Object x )
        throws HibernateException
    {
        if ( x == null || !( x instanceof PointInTime ) )
            return 0;

        PointInTime pointInTimeX = ( PointInTime )x;
        
        /* if ( pointInTimeX.getTimestamp() == null )
        return 0;

    	return pointInTimeX.getTimestamp().hashCode();*/
       
        if ( pointInTimeX.getLiteral() == null )
            return 0;

        return pointInTimeX.getLiteral().hashCode();
       
    }


    public boolean isMutable( )
    {
        return true;
    }


    public Object nullSafeGet( ResultSet rs, String[] names, SessionImplementor sessionImplementor, Object owner )
        throws HibernateException,
            SQLException
    {
        PointInTime pointInTime = null;

        String timestamp = rs.getString( names[0] );
        if ( !rs.wasNull() )
        {
            pointInTime = new PointInTime();
            pointInTime.setLiteral( timestamp );
            //pointInTime.setTimestamp( timestamp );
        }

        return pointInTime;
    }


    public void nullSafeSet( PreparedStatement st, Object value, int index, SessionImplementor sessionImplementor )
        throws HibernateException,
            SQLException
    {

        if ( value == null )
        {
            if ( isSetLiteral() )
            {
                st.setNull( index, StringType.INSTANCE.sqlType() );
                index++ ;
            }

            if ( isSetNumeric() )
            {
                st.setNull( index, DoubleType.INSTANCE.sqlType() );
            }
        }
        else
        {
            if ( value instanceof PointInTime )
            {
                PointInTime pointInTime = ( PointInTime )value;
                if ( isSetLiteral() )
                {
                   st.setString( index, pointInTime.getLiteral() ); // set existing date string
                   //st.setString( index, pointInTime.getTimestamp() ); // set existing date string
                    index++ ;
                }

                if ( isSetNumeric() )
                {
                    Double timestamp = isoTime2Double( pointInTime.getLiteral());
                    //Double timestamp = isoTime2Double( pointInTime.getTimestamp());
                    
                    if ( timestamp != null )
                    {
                        st.setDouble( index, timestamp );
                    }
                    else
                    {
                        st.setNull( index, DoubleType.INSTANCE.sqlType() );
                    }
                }
            }
            else
            {
                throw new HibernateException( "The value parameter must be of type " + PointInTime.class.getName() + ". The actual type was "
                                + value.getClass().getName() );
            }
        }
    }


    public Object replace( Object original, Object target, SessionImplementor sessionImplementor, Object owner )
        throws HibernateException
    {
        return original; // not quite sure what to do with this.
    }


    public Class<PointInTime> returnedClass( )
    {
        return PointInTime.class;
    }


    public int[] sqlTypes( )
    {
        return new int[] { StringType.INSTANCE.sqlType(), DoubleType.INSTANCE.sqlType() };
    }


    /**
     * builds the new double from date and time parts of the given string.
     * 
     * @param isoDate
     * @return builds the new double from date and time parts of the given string
     */
    public Double isoTime2Double( String isoDate )
    {
        Double retVal;

        if ( ( isoDate != null ) && ( isoDate.length() >= 4 ) )
        {
            // trim off the timezone offset and the milliseconds
            isoDate = isoDate.split( "[-+]" )[0].split( "\\." )[0];

            // Filter out non-conformant string dates
            if ( isNonConformatString( isoDate ) )
            {
                return null;
            }

            // split the time up into date and time parts.
            int dateMaxIndex = ( isoDate.length() >= 8 ) ? 8 : isoDate.length();
            String datePart = isoDate.substring( 0, dateMaxIndex ) + "0000".substring( 0, 8 - dateMaxIndex );
            String timePart = ( isoDate.length() > dateMaxIndex ) ? isoDate.substring( dateMaxIndex ) : "0";

            // subtract 1700 from the year to match Cache date format
            datePart = ( Integer.parseInt( datePart.substring( 0, 4 ) ) - 1700 ) + datePart.substring( 4 );

            // build the new double from date and time parts
            retVal = new Double( datePart + "." + timePart );
        }
        else
        {
            retVal = new Double( 0.0 );
        }

        return retVal;
    }


    /**
     * This return ensures the user string is conformant to at least the following rules: - All characters must be
     * numeric for the iso conversion
     *
     * @param isoDate
     * @return boolean
     */
    protected boolean isNonConformatString(String isoDate) 
    {
		boolean returnValue = false;

		if ( isoDate.length() < 4 ) 
		{
			returnValue = true;
		} 
		else 
		{
			for (int index = 0; index < isoDate.length(); index++) 
			{
				char isoDateCharacter = isoDate.charAt( index );
				if ( ! ( ( isoDateCharacter <= '9' ) && ( isoDateCharacter >= '0' ) ) ) 
				{
					returnValue = true;
					break;
				}
			}
		}

		return returnValue;
	}


    private boolean isSetLiteral( )
    {
        return ( this.setValues == null ) || this.setValues.equals( SV_ALL ) || this.setValues.equals( SV_LITERAL_ONLY );
    }


    private boolean isSetNumeric( )
    {
        return ( this.setValues == null ) || this.setValues.equals( SV_ALL ) || this.setValues.equals( SV_NUMERIC_ONLY );
    }


    public static void main( String[] args )
    {
        PointInTimeUserType pointInTimeUserType = new PointInTimeUserType();
        String isoString = "20071231235959.999-0600";
        Double isoDate = pointInTimeUserType.isoTime2Double( isoString );
        System.out.println( isoString + " => " + isoDate.doubleValue() );
        isoString = "20071231235959.999+0600";
        isoDate = pointInTimeUserType.isoTime2Double( isoString );
        System.out.println( isoString + " => " + isoDate.doubleValue() );
    }
}

