/*
 * 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.apache.openjpa.meta;

import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.lib.meta.CFMetaDataParser;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.MetaDataException;

/**
 * Type constants for managed fields.
 *
 * @author Abe White
 */
public class JavaTypes {

    public static final int BOOLEAN = 0;
    public static final int BYTE = 1;
    public static final int CHAR = 2;
    public static final int DOUBLE = 3;
    public static final int FLOAT = 4;
    public static final int INT = 5;
    public static final int LONG = 6;
    public static final int SHORT = 7;
    // keep OBJECT as first non-primitive type code; other code relies on it
    public static final int OBJECT = 8;
    public static final int STRING = 9;
    public static final int NUMBER = 10;
    public static final int ARRAY = 11;
    public static final int COLLECTION = 12;
    public static final int MAP = 13;
    public static final int DATE = 14;
    public static final int PC = 15;
    public static final int BOOLEAN_OBJ = 16;
    public static final int BYTE_OBJ = 17;
    public static final int CHAR_OBJ = 18;
    public static final int DOUBLE_OBJ = 19;
    public static final int FLOAT_OBJ = 20;
    public static final int INT_OBJ = 21;
    public static final int LONG_OBJ = 22;
    public static final int SHORT_OBJ = 23;
    public static final int BIGDECIMAL = 24;
    public static final int BIGINTEGER = 25;
    public static final int LOCALE = 26;
    public static final int PC_UNTYPED = 27;
    public static final int CALENDAR = 28;
    public static final int OID = 29;
    public static final int INPUT_STREAM = 30;
    public static final int INPUT_READER = 31;
    public static final int ENUM = 32;

    // Java8 time API
    public static final int LOCAL_DATE = 33;
    public static final int LOCAL_TIME = 34;
    public static final int LOCAL_DATETIME = 35;
    public static final int OFFSET_TIME = 36;
    public static final int OFFSET_DATETIME = 37;

    public static final int UUID_OBJ = 38;


    private static final Localizer _loc = Localizer.forPackage(JavaTypes.class);

    private static final Map<Class<?>, Integer> _typeCodes = new HashMap<>();

    static {
        _typeCodes.put(String.class, STRING);
        _typeCodes.put(Boolean.class, BOOLEAN_OBJ);
        _typeCodes.put(Byte.class, BYTE_OBJ);
        _typeCodes.put(Character.class, CHAR_OBJ);
        _typeCodes.put(Double.class, DOUBLE_OBJ);
        _typeCodes.put(Float.class, FLOAT_OBJ);
        _typeCodes.put(Integer.class, INT_OBJ);
        _typeCodes.put(Long.class, LONG_OBJ);
        _typeCodes.put(Short.class, SHORT_OBJ);
        _typeCodes.put(Date.class, DATE);
        _typeCodes.put(java.sql.Date.class, DATE);
        _typeCodes.put(java.sql.Timestamp.class, DATE);
        _typeCodes.put(java.sql.Time.class, DATE);
        _typeCodes.put(BigInteger.class, BIGINTEGER);
        _typeCodes.put(BigDecimal.class, BIGDECIMAL);
        _typeCodes.put(Number.class, NUMBER);
        _typeCodes.put(Locale.class, LOCALE);
        _typeCodes.put(Object.class, OBJECT);
        _typeCodes.put(PersistenceCapable.class, PC_UNTYPED);
        _typeCodes.put(Properties.class, MAP);
        _typeCodes.put(Calendar.class, CALENDAR);

        // Java8 time API
        _typeCodes.put(LocalDate.class, LOCAL_DATE);
        _typeCodes.put(LocalTime.class, LOCAL_TIME);
        _typeCodes.put(LocalDateTime.class, LOCAL_DATETIME);
        _typeCodes.put(OffsetTime.class, OFFSET_TIME);
        _typeCodes.put(OffsetDateTime.class, OFFSET_DATETIME);

        _typeCodes.put(UUID.class, UUID_OBJ);
    }

    /**
     * Return the field metadata type code for the given class. First class
     * objects are not recognized in this method.
     */
    public static int getTypeCode(Class<?> type) {
        if (type == null)
            return OBJECT;

        if (type.isPrimitive()) {
            switch (type.getName().charAt(0)) {
                case 'b':
                    return (type == boolean.class) ? BOOLEAN : BYTE;
                case 'c':
                    return CHAR;
                case 'd':
                    return DOUBLE;
                case 'f':
                    return FLOAT;
                case 'i':
                    return INT;
                case 'l':
                    return LONG;
                case 's':
                    return SHORT;
            }
        }

        Integer code = _typeCodes.get(type);
        if (code != null) {
            return code;
        }

        // have to do this first to catch custom collection and map types;
        // on resolve we figure out if these custom types are
        // persistence-capable
        if (Collection.class.isAssignableFrom(type))
            return COLLECTION;
        if (Map.class.isAssignableFrom(type))
            return MAP;
        if (type.isArray())
            return ARRAY;
        if (Calendar.class.isAssignableFrom(type))
            return CALENDAR;

        if (type.isInterface()) {
            if (Serializable.class.isAssignableFrom(type))
                return OBJECT;
            return PC_UNTYPED;
        }
        if (type.isAssignableFrom(Reader.class))
            return INPUT_READER;
        if (type.isAssignableFrom (InputStream.class))
            return INPUT_STREAM;
        if (Enum.class.isAssignableFrom(type))
            return ENUM;

        return OBJECT;
    }

    /**
     * Check the given name against the same set of standard packages used
     * when parsing metadata.
     */
    public static Class<?> classForName(String name, ClassMetaData context) {
        return classForName(name, context, null);
    }

    /**
     * Check the given name against the same set of standard packages used
     * when parsing metadata.
     */
    public static Class<?> classForName(String name, ClassMetaData context, ClassLoader loader) {
        return classForName(name, context, context.getDescribedType(), null,
            loader);
    }

    /**
     * Check the given name against the same set of standard packages used
     * when parsing metadata.
     *
     * @param mustExist Whether the supplied loader <b>must</b> be able to load the class. If true no attempt to use a
     * different classloader will be made. If false the ClassResolver from the configuration will be used.
     */
    public static Class<?> classForName(String name, ClassMetaData context, ClassLoader loader, boolean mustExist) {
        return classForName(name, context, context.getDescribedType(), null, loader, mustExist);
    }

    /**
     * Check the given name against the same set of standard packages used
     * when parsing metadata.
     */
    public static Class<?> classForName(String name, ValueMetaData context) {
        return classForName(name, context, null);
    }

    /**
     * Check the given name against the same set of standard packages used
     * when parsing metadata.
     */
    public static Class<?> classForName(String name, ValueMetaData context, ClassLoader loader) {
        return classForName(name,
            context.getFieldMetaData().getDefiningMetaData(),
            context.getFieldMetaData().getDeclaringType(), context, loader);
    }

     /**
     * Try to load a class using the provided loader. Optionally tries the
     * configuration's ClassResolver if the supplied loader cannot find the class.
     *
     * @param name Name of the class to load.
     * @param loader ClassLoader to use. If null, the configuration's ClassResolver will be used.
     * @param mustExist Whether the supplied loader <b>must</b> be able to load the class. If true no attempt to use a
     *        different classloader will be made. If false the ClassResolver from the configuration will be used.
     */
    public static Class<?> classForName(String name, ValueMetaData context,
            ClassLoader loader, boolean mustExist) {
            return classForName(name,
                context.getFieldMetaData().getDefiningMetaData(),
                context.getFieldMetaData().getDeclaringType(), context, loader, mustExist);
        }

    /**
     * OJ-758: Delegates to the final classForName.  This is needed
     * to maintain the existing code path prior to OJ-758.
     */
    private static Class<?> classForName(String name, ClassMetaData meta,
            Class<?> dec, ValueMetaData vmd, ClassLoader loader) {
        return classForName(name, meta, dec, vmd,  loader, true);
    }

    /**
     * Check the given name against the same set of standard packages used
     * when parsing metadata.
     */
    private static Class<?> classForName(String name, ClassMetaData meta, Class<?> dec, ValueMetaData vmd,
        ClassLoader loader, boolean mustExist) {
        // special case for PersistenceCapable and Object
        if ("PersistenceCapable".equals(name)
            || "javax.jdo.PersistenceCapable".equals(name)) // backwards compatibility
            return PersistenceCapable.class;
        if ("Object".equals(name))
            return Object.class;

        MetaDataRepository rep = meta.getRepository();
        boolean runtime = (rep.getValidate() & MetaDataRepository.VALIDATE_RUNTIME) != 0;
        if (loader == null)
            loader = rep.getConfiguration().getClassResolverInstance().
                getClassLoader(dec, meta.getEnvClassLoader());

        // try the owner's package
        String pkg = ClassUtil.getPackageName(dec);
        Class<?> cls = CFMetaDataParser.classForName(name, pkg, runtime, loader);
        if (cls == null && vmd != null) {
            // try against this value type's package too
            pkg = ClassUtil.getPackageName(vmd.getDeclaredType());
            cls = CFMetaDataParser.classForName(name, pkg, runtime, loader);
        }

        //OJ-758 start: If the class is still null, as a last/final attempt to
        //load the class, check with the ClassResolver to get a loader
        //and use it to attempt to load the class.
        if (cls == null  && !mustExist){
            loader = rep.getConfiguration().getClassResolverInstance().
            getClassLoader(dec, meta.getEnvClassLoader());
            cls = CFMetaDataParser.classForName(name, pkg, runtime, loader);
        }
        //OJ-758 end

        if (cls == null)
            throw new MetaDataException(_loc.get("bad-class", name,
                (vmd == null) ? (Object) meta : (Object) vmd));

        return cls;
    }

    /**
     * Convert the given object to the given type if possible. If the type is
     * a numeric primitive, this method only guarantees that the return value
     * is a {@link Number}. If no known conversion or the value is null,
     * returns the original value.
     */
    public static Object convert(Object val, int typeCode) {
        if (val == null)
            return null;

        switch (typeCode) {
            case BIGDECIMAL:
                if (val instanceof BigDecimal)
                    return val;
                if (val instanceof Number)
                    return new BigDecimal(((Number) val).doubleValue());
                if (val instanceof String)
                    return new BigDecimal(val.toString());
                return val;
            case BIGINTEGER:
                if (val instanceof BigInteger)
                    return val;
                if (val instanceof Number || val instanceof String)
                    return new BigInteger(val.toString());
                return val;
            case BOOLEAN:
            case BOOLEAN_OBJ:
                if (val instanceof String)
                    return Boolean.valueOf(val.toString());
                return val;
            case BYTE_OBJ:
                if (val instanceof Byte)
                    return val;
                if (val instanceof Number)
                    return ((Number) val).byteValue();
                // no break
            case BYTE:
                if (val instanceof String)
                    return Byte.valueOf((String)val);
                return val;
            case CHAR:
            case CHAR_OBJ:
                if (val instanceof Character)
                    return val;
                if (val instanceof String)
                    return val.toString().charAt(0);
                if (val instanceof Number)
                    return (char) ((Number) val).intValue();
                return val;
            case DATE:
                if (val instanceof String)
                    return new Date(val.toString());
                return val;
            case DOUBLE_OBJ:
                if (val instanceof Double)
                    return val;
                if (val instanceof Number)
                    return ((Number) val).doubleValue();
                // no break
            case DOUBLE:
                if (val instanceof String)
                    return Double.valueOf(val.toString());
                return val;
            case FLOAT_OBJ:
                if (val instanceof Float)
                    return val;
                if (val instanceof Number)
                    return ((Number) val).floatValue();
                // no break
            case FLOAT:
                if (val instanceof String)
                    return Float.valueOf(val.toString());
                return val;
            case INT_OBJ:
                if (val instanceof Integer)
                    return val;
                if (val instanceof Number)
                    return ((Number) val).intValue();
                // no break
            case INT:
                if (val instanceof String)
                    return Integer.valueOf(val.toString());
                return val;
            case LONG_OBJ:
                if (val instanceof Long)
                    return val;
                if (val instanceof Number)
                    return ((Number) val).longValue();
                // no break
            case LONG:
                if (val instanceof String)
                    return Long.valueOf(val.toString());
                return val;
            case NUMBER:
                if (val instanceof Number)
                    return val;
                if (val instanceof String)
                    return new BigDecimal(val.toString());
                return val;
            case SHORT_OBJ:
                if (val instanceof Short)
                    return val;
                if (val instanceof Number)
                    return ((Number) val).shortValue();
                // no break
            case SHORT:
                if (val instanceof String)
                    return Short.valueOf(val.toString());
                return val;
            case UUID_OBJ:
                if (val instanceof String) {
                    return UUID.fromString((String) val);
                }
                return (UUID) val;
            case STRING:
                return val.toString();
            default:
                return val;
        }
    }

    /**
     * Return true if the (possibly unresolved) field or its elements might be
     * persistence capable objects.
     */
    public static boolean maybePC(FieldMetaData field) {
        switch (field.getDeclaredTypeCode()) {
            case JavaTypes.ARRAY:
            case JavaTypes.COLLECTION:
                return maybePC(field.getElement());
            case JavaTypes.MAP:
                return maybePC(field.getKey()) || maybePC(field.getElement());
            default:
                return maybePC((ValueMetaData) field);
        }
    }

    /**
     * Return true if the (possibly unresolved) value might be a first class
     * object.
     */
    public static boolean maybePC(ValueMetaData val) {
        return maybePC(val.getDeclaredTypeCode(), val.getDeclaredType());
    }

    /**
     * Return true if the given unresolved typecode/type pair may represent a
     * persistent object.
     */
    static boolean maybePC(int typeCode, Class<?> type) {
        if (type == null)
            return false;
        switch (typeCode) {
            case JavaTypes.OBJECT:
            case JavaTypes.PC:
            case JavaTypes.PC_UNTYPED:
                return true;
            case JavaTypes.COLLECTION:
            case JavaTypes.MAP:
                return !type.getName().startsWith("java.util.");
            default:
                return false;
        }
    }

    /**
     * Helper method to return the given array value as a collection.
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> toList(Object val, Class<T> elem, boolean mutable) {
        if (val == null)
            return null;

        List<T> l;
        if (!elem.isPrimitive()) {
            // if an object array, use built-in list function
            l = Arrays.asList((T[]) val);
            if (mutable)
                l = new ArrayList<>(l);
        } else {
            // convert to list of wrapper objects
            int length = Array.getLength(val);
            l = new ArrayList<>(length);
            for (int i = 0; i < length; i++)
                l.add((T)Array.get(val, i));
        }
        return l;
    }

    /**
     * Helper method to return the given collection as an array.
     */
    public static Object toArray(Collection<?> coll, Class<?> elem) {
        if (coll == null)
            return null;

        Object array = Array.newInstance(elem, coll.size());
        int idx = 0;
        for (Iterator<?> itr = coll.iterator(); itr.hasNext(); idx++)
            Array.set(array, idx, itr.next ());
		return array;
	}

    /**
     * Determine whether or not the provided Object value is the default for the provided typeCode.
     *
     * For example: If o = Integer(0) and typeCode = JavaTypes.INT, this method will return true.
     */
    public static boolean isPrimitiveDefault(Object o, int typeCode) {
        switch (typeCode) {
            case BOOLEAN:
                return ((Boolean) o).equals(Boolean.FALSE) ? true : false;
            case BYTE:
                return ((Byte) o) == 0 ? true : false;
            case SHORT:
                return ((Short) o) == 0 ? true : false;
            case INT:
                return ((Integer) o) == 0 ? true : false;
            case LONG:
                return ((Long) o) == 0L ? true : false;
            case FLOAT:
                return ((Float) o) == 0.0F ? true : false;
            case CHAR:
                return ((Character) o) == '\u0000' ? true : false;
            case DOUBLE:
                return ((Double) o) == 0.0d ? true : false;
        }
        return false;
    }
}
