/*
 * 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.query.processor.xml;

import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;

import javax.xml.transform.TransformerException;

import net.sf.saxon.value.DateTimeValue;
import net.sf.saxon.value.GYearMonthValue;

import org.teiid.api.exception.query.FunctionExecutionException;
import org.teiid.core.types.DataTypeManager;
import org.teiid.core.types.TransformationException;
import org.teiid.query.QueryPlugin;
import org.teiid.query.function.FunctionMethods;
import org.teiid.query.function.source.XMLSystemFunctions;
import org.teiid.query.util.CommandContext;



/**
 * This class will make a minimal effort to output xsd formatted values for a given
 * built-in type.  It will not attempt to narrow or otherwise fit most values into
 * their output space (months can be greater than 12, nonNegative numbers can be 
 * negative, etc.)
 */
public final class XMLValueTranslator {

    private static String NEGATIVE_INFINITY = "-INF"; //$NON-NLS-1$
    private static String POSITIVE_INFINITY = "INF"; //$NON-NLS-1$
    
    private static String GMONTHDAY_FORMAT = "--MM-dd"; //$NON-NLS-1$
    private static String GYEAR_FORMAT = "0000"; //$NON-NLS-1$
    
    public static final String DATETIME    = "dateTime"; //$NON-NLS-1$
    public static final String DOUBLE      = "double"; //$NON-NLS-1$
    public static final String FLOAT       = "float"; //$NON-NLS-1$
    public static final String GDAY        = "gDay"; //$NON-NLS-1$
    public static final String GMONTH      = "gMonth"; //$NON-NLS-1$
    public static final String GMONTHDAY   = "gMonthDay"; //$NON-NLS-1$
    public static final String GYEAR       = "gYear"; //$NON-NLS-1$
    public static final String GYEARMONTH  = "gYearMonth"; //$NON-NLS-1$
    
    public static final String STRING = "string"; //$NON-NLS-1$
    
    private static final Map<String, Integer> TYPE_CODE_MAP;
    
    private static final int DATETIME_CODE = 0;
    private static final int DOUBLE_CODE = 1;
    private static final int FLOAT_CODE = 2;
    private static final int GDAY_CODE = 3;
    private static final int GMONTH_CODE = 4;
    private static final int GMONTHDAY_CODE = 5;
    private static final int GYEAR_CODE = 6;
    private static final int GYEARMONTH_CODE = 7;
        
    static {
        TYPE_CODE_MAP = new HashMap<String, Integer>(20);
        TYPE_CODE_MAP.put(DATETIME, new Integer(DATETIME_CODE));
        TYPE_CODE_MAP.put(DOUBLE, new Integer(DOUBLE_CODE));
        TYPE_CODE_MAP.put(FLOAT, new Integer(FLOAT_CODE));
        TYPE_CODE_MAP.put(GDAY, new Integer(GDAY_CODE));
        TYPE_CODE_MAP.put(GMONTH, new Integer(GMONTH_CODE));
        TYPE_CODE_MAP.put(GMONTHDAY, new Integer(GMONTHDAY_CODE));
        TYPE_CODE_MAP.put(GYEAR, new Integer(GYEAR_CODE));
        TYPE_CODE_MAP.put(GYEARMONTH, new Integer(GYEARMONTH_CODE));   
    }
       
    /**
     * Translate the value object coming from the mapping class into the string that will be 
     * placed in the XML document for a tag.  Usually, the value.toString() method is just called
     * to translate to a string.  In some special cases, the runtimeType and builtInType
     * will be used to determine a custom translation to string where the Java object toString()
     * does not comply with the XML Schema spec.
     * 
     * NOTE: date objects are not checked for years less than 1
     *    
     * @param value The value
     * @param runtimeType The runtime type
     * @param builtInType The design-time atomic built-in type (or null if none)
     * @return String representing the value
     * @throws FunctionExecutionException 
     * @throws TransformationException 
     * @since 5.0
     */
    public static String translateToXMLValue(Object value, Class<?> runtimeType, String builtInType, CommandContext context) throws FunctionExecutionException, TransformationException {
        if (value == null) {
            return null;
        }
        
        Integer typeCode = TYPE_CODE_MAP.get(builtInType);
        
        String valueStr = null;
        
        if (builtInType == null || typeCode == null || runtimeType == DataTypeManager.DefaultDataClasses.STRING || STRING.equals(builtInType)) {
            valueStr = defaultTranslation(value);
        } else {
        
            int type = typeCode.intValue();
            
            switch (type) {
                case DATETIME_CODE:
				try {
					valueStr = XMLSystemFunctions.convertToAtomicValue(value).getStringValue();
				} catch (TransformerException e) {
					 throw new TransformationException(QueryPlugin.Event.TEIID30208, e, e.getMessage());
				}
                    break;
                case DOUBLE_CODE:
                    valueStr = doubleToDouble((Double)value);
                    break;
                case FLOAT_CODE:
                    valueStr = floatToFloat((Float)value);
                    break;
                case GDAY_CODE:
                    valueStr = bigIntegerTogDay((BigInteger)value);
                    break;
                case GMONTH_CODE:
                    valueStr = bigIntegerTogMonth((BigInteger)value);
                    break;
                case GMONTHDAY_CODE:
                    valueStr = FunctionMethods.format(context, (Timestamp)value, GMONTHDAY_FORMAT);
                    break;
                case GYEAR_CODE:
                    valueStr = FunctionMethods.format(context, (BigInteger)value, GYEAR_FORMAT);
                    break;
                case GYEARMONTH_CODE:
				DateTimeValue dtv;
				try {
					dtv = ((DateTimeValue)XMLSystemFunctions.convertToAtomicValue(value));
				} catch (TransformerException e) {
					 throw new TransformationException(QueryPlugin.Event.TEIID30209, e, e.getMessage());
				}
                	valueStr = new GYearMonthValue(dtv.getYear(), dtv.getMonth(), dtv.getTimezoneInMinutes(), true).getStringValue();
                    break;
                default:
                    valueStr = defaultTranslation(value);
                    break;
            }
            
        }        

        //Per defects 11789, 14905, 15117
        if (valueStr != null && valueStr.length()==0){
            valueStr = null;
        }
    
        return valueStr;
    }

    /**
     * Translate any non-null value to a string by using the Object toString() method.
     * This works in any case where the Java string representation of an object is the 
     * same as the expected XML Schema output form.
     *   
     * @param value Value returned from a mapping class
     * @return String content to put in XML output
     * @throws TransformationException 
     * @since 5.0
     */
    static String defaultTranslation(Object value) throws TransformationException {
        return (String)DataTypeManager.transformValue(value, DataTypeManager.DefaultDataClasses.STRING);
    }
    
    /**
     * Translate runtime float to xs:float string value.  The normal Java representation 
     * for floats is fine except the strings used for positive and negative infinity 
     * are different.
     *  
     * @param f Runtime float
     * @return String representing xs:float
     * @throws TransformationException 
     * @since 5.0
     */
    static String floatToFloat(Float f) throws TransformationException {
        if(f.floatValue() == Float.NEGATIVE_INFINITY) {
            return NEGATIVE_INFINITY;
        } else if(f.floatValue() == Float.POSITIVE_INFINITY) {
            return POSITIVE_INFINITY;
        }
        return defaultTranslation(f);
    }

    /**
     * Translate runtime double to xs:double string value.  The normal Java representation 
     * for doubles is fine except the strings used for positive and negative infinity 
     * are different.
     *  
     * @param d Runtime double
     * @return String representing xs:double
     * @throws TransformationException 
     * @since 5.0
     */
    static String doubleToDouble(Double d) throws TransformationException {
        if(d.doubleValue() == Double.NEGATIVE_INFINITY) {
            return NEGATIVE_INFINITY;
        } else if(d.doubleValue() == Double.POSITIVE_INFINITY) {
            return POSITIVE_INFINITY;
        }                    
        return defaultTranslation(d);
    }

    /**
     * gMonths out of the valid range are returned in tact. 
     * @param value
     * @return
     * @since 5.0
     */
    static String bigIntegerTogMonth(BigInteger value) {
        long month = value.longValue();
        
        if(month < 10) {
            // Add leading 0
            return "--0" + month; //$NON-NLS-1$
        } 

        // Don't need leading 0
        return "--" + month; //$NON-NLS-1$        
    }
    
    /**
     * gDays out of the valid range are returned in tact. 
     * @param value
     * @return
     * @since 5.0
     */
    static String bigIntegerTogDay(BigInteger value) {
        long day = value.longValue();
        
        if(day < 10) {
            // Add leading 0
            return "---0" + day; //$NON-NLS-1$
        } 

        // Don't need leading 0
        return "---" + day; //$NON-NLS-1$        
    }
    
}
