/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.j2ee.persistence.entitygenerator;

import java.lang.ref.SoftReference;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Types;
import java.util.*;
import org.netbeans.modules.dbschema.ColumnElement;

/**
 * provides class wrapper for java.sql.Types.
 * @see java.sql.Types
 * @author Christopher Webster
 */
class SQLType {
    private int sqlType;
    private String stringValue;
    /* list of classes that could be used to represent this sql type in Java */
    private Class[] typeList;
    private boolean supportsFinder;
    private static final SQLType UNKNOWN = new SQLType(Types.OTHER, 
                                                       "UNKNOWN", // NOI18N
                                                       new Class[0], false);
        
    private SQLType(int type, String value, Class[] tList, boolean supportsFinder) {
        sqlType = type;
        stringValue = value;
        typeList = tList;
        this.supportsFinder = supportsFinder;
    }

   /** 
    * Override Object.equals
    */
    public boolean equals(Object other) {
        if (other == null || !getClass().isInstance(other)){
	    return false;
        }  
	return ((SQLType)other).sqlType == sqlType;
    }

    /**
     * Override Object.hashCode
     */
    public int hashCode() {
        return sqlType;
    }
  
    /**
     * Provide string representation of sql type. For example, 
     * java.sql.Types.BIT returns BIT.
     */
    public String toString() {
        return stringValue;
    }
        
    /**
     * provide original sql type
     * @return sql type from java.sql.Types
     */
    public int sqlType() {
        return sqlType;
    }    
    
    /**
     * Return the string value associated with this sql type
     * @return String value used in the method signature 
     */
    String getMemberType(ColumnElement element) {
        String memberType = java.io.Serializable.class.getName();
        Class memberTypeClass = null;
        
        if (element.isCharacterType()) {
            memberTypeClass = getClassForCharType(element.getLength(), 
                    element.isNullable());
        } else if (element.isNumericType()) {
            memberTypeClass = getClassForNumericType(element.getPrecision(), 
                    element.getScale(), element.isNullable());
        }

        if (memberTypeClass != null) {
            return memberTypeClass.getName();
        }

        if (typeList.length > 0) {
            memberType = typeList[0].getName();
        }
        
        /* Alters the name in the case of byte arrays */
        if (memberType.equals("[B")) { // NOI18N
            memberType = "byte []"; // NOI18N
        }
        return memberType;
    }
    
    public String getFirstNonPrimitiveType() {
        String typeName = null;
        for (Class clazz : typeList) {
            if (!clazz.isPrimitive()) {
                typeName = clazz.getName();
                break;
            }
        }
        /* Alters the name in the case of byte arrays */
        if ("[B".equals(typeName)) { // NOI18N
            typeName = "byte []"; // NOI18N
        }
        return typeName;
    }

    // returns the relevant Class from the typeList if it is dependent on
    // length, <code>null</code> if the rules in the no-arg getMemberType 
    // method are sufficient
    private Class getClassForCharType (Integer length, boolean isNullable) {
        switch (sqlType) {
            case Types.CHAR:
                if ((length != null) && (length.intValue() == 1)) {
                     return typeList[1];//typeList[isNullable ? 1 : 2] is replaced with String always because of issue #195674 (openjpa fo Character handling)
                }
            default:
                return null;
        }
    }

    // returns the relevant Class from the typeList if it is dependent on
    // precision, scale, or nullability, <code>null</code> if the rules in 
    // the no-arg getMemberType method are sufficient
    private Class getClassForNumericType(Integer precision, 
            Integer scale, boolean isNullable) {
        int precValue = ((precision == null) ? -1 : precision.intValue());
        int scaleValue = ((scale == null) ? -1 : scale.intValue());

        switch (sqlType) {
            case Types.DECIMAL:
            case Types.NUMERIC:
                // some jdbc drivers put in null or zero for a numeric
                // specified without precision or scale
                // return BigInteger in that case
                if ((precValue <= 0) && (scaleValue <= 0)){
                    return BigInteger.class;
                }

                if (scaleValue > 0) {
                    return BigDecimal.class;
                }
                if (precValue > 18) {
                    return BigInteger.class;
                }
                if (precValue > 9) {
                    return (isNullable ? Long.class : Long.TYPE);
                }
                if (precValue > 4) {
                    return (isNullable ? Integer.class : Integer.TYPE);
                }
                return (isNullable ? Short.class : Short.TYPE);
            case Types.FLOAT:
            case Types.DOUBLE:
            case Types.REAL:
            case Types.BIGINT:
            case Types.INTEGER:
            case Types.SMALLINT:
            case Types.TINYINT:
            case Types.BIT:
                return typeList[isNullable ? 0 : 1];
            default:
                return null;
        }
    }
    
    public boolean supportsFinder() {
        return supportsFinder;
    }
    
    /**
     * Return create SQLType.
     * @param sqlType value from java.sql.Types
     * @return SQLType or null if type cannot be created
     */
    public static SQLType getSQLType(int sqlType) {
        List types = getSQLTypes();
        int ind = types.indexOf(new SQLType(sqlType, null, null, false));
	return ind==-1?UNKNOWN:(SQLType)types.get(ind);
    }
    
    private static List getSQLTypes() {
        List types = (List) typeCache.get();
        if (types == null) {
            types = getAllSQLTypes();
            typeCache = new SoftReference(types);
        }
        return types;
    }
    
    private static List getAllSQLTypes() {
        return Arrays.asList(new SQLType[] {
            new SQLType(Types.ARRAY, "ARRAY",   //NOI18N
                new Class[0], false), 
            new SQLType(Types.BIGINT, "BIGINT",     //NOI18N
                new Class[] {java.math.BigInteger.class, Long.TYPE}, true),
            new SQLType(Types.DECIMAL, "DECIMAL",   //NOI18N
                new Class[] {java.math.BigDecimal.class, java.math.BigInteger.class, 
                    Short.class, Short.TYPE, Integer.class, Integer.TYPE, 
                    Long.class, Long.TYPE}, true),
            new SQLType(Types.NUMERIC, "NUMERIC",   //NOI18N
                new Class[] {java.math.BigDecimal.class, java.math.BigInteger.class, 
                    Short.class, Short.TYPE, Integer.class, Integer.TYPE, 
                    Long.class, Long.TYPE}, true),
            new SQLType(Types.BLOB, "BLOB",         //NOI18N
                new Class[0], false),
            new SQLType(Types.CHAR, "CHAR",         //NOI18N
                new Class[] {java.lang.String.class, Character.class, Character.TYPE}, true),
            new SQLType(Types.NCHAR, "NCHAR",         //NOI18N
                new Class[] {java.lang.String.class, Character.class, Character.TYPE}, true),
            new SQLType(Types.LONGVARCHAR, "LONGVARCHAR",   //NOI18N
                new Class[] {java.lang.String.class}, true),
            new SQLType(Types.LONGNVARCHAR, "LONGNVARCHAR",   //NOI18N
                new Class[] {java.lang.String.class}, true),
            new SQLType(Types.VARCHAR, "VARCHAR",           //NOI18N
                new Class[] {java.lang.String.class}, true),
            new SQLType(Types.NVARCHAR, "NVARCHAR",           //NOI18N
                new Class[] {java.lang.String.class}, true),
            new SQLType(Types.CLOB, "CLOB",                 //NOI18N
                new Class[] {java.lang.String.class}, false),
            new SQLType(Types.NCLOB, "NCLOB",                 //NOI18N
                new Class[] {java.lang.String.class}, false),
            new SQLType(Types.DATE, "DATE",                 //NOI18N
                new Class[] {java.sql.Date.class}, false),
            new SQLType(Types.FLOAT, "FLOAT",               //NOI18N
                new Class[] {Double.class, Double.TYPE}, true),
            new SQLType(Types.DOUBLE, "DOUBLE",             //NOI18N
                new Class[] {Double.class, Double.TYPE}, true),
            new SQLType(Types.REAL, "REAL",                 //NOI18N
                new Class[] {Float.class, Float.TYPE}, true),   
            new SQLType(Types.INTEGER, "INTEGER",           //NOI18N
                new Class[] {Integer.class, Integer.TYPE}, true),
            new SQLType(Types.JAVA_OBJECT, "JAVA_OBJECT",   //NOI18N
                new Class[0], false),
	    new SQLType(Types.NULL, "NULL",                 //NOI18N
                new Class[0], false),
            new SQLType(Types.OTHER, "OTHER",               //NOI18N
                new Class[]{Object.class}, true),    
            new SQLType(Types.STRUCT, "STRUCT",             //NOI18N
                new Class[0], false),
            new SQLType(Types.DISTINCT, "DISTINCT",         //NOI18N
                new Class[0], false),
            new SQLType(Types.BINARY, "BINARY",             //NOI18N
                new Class[] { byte[].class}, false),
            new SQLType(Types.BIT, "BIT",                   //NOI18N
                new Class[] {Boolean.class, Boolean.TYPE}, true),
            new SQLType(Types.BOOLEAN, "BOOLEAN",           //NOI18N
                new Class[] {Boolean.class, Boolean.TYPE}, true),
            new SQLType(Types.VARBINARY, "VARBINARY",       //NOI18N
                new Class[] {byte[].class}, false),
            new SQLType(Types.LONGVARBINARY, "LONGVARBINARY",   //NOI18N
                new Class[] {byte[].class}, false),
            new SQLType(Types.REF, "REF",                   //NOI18N
                new Class[0], false),
            new SQLType(Types.SMALLINT, "SMALLINT",         //NOI18N
                new Class[] {Short.class, Short.TYPE}, true),
            new SQLType(Types.TINYINT, "TINYINT",           //NOI18N
                new Class[] {Short.class, Short.TYPE}, true),
            new SQLType(Types.TIME, "TIME",                 //NOI18N
                new Class[] {java.sql.Time.class}, false),
            new SQLType(Types.TIMESTAMP, "TIMESTAMP",       //NOI18N
                new Class[] {java.sql.Timestamp.class}, false)
        });
    }
    
    private static SoftReference typeCache = new SoftReference(null);
}
    
