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

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import org.apache.openjpa.enhance.Reflection;
import org.apache.openjpa.kernel.exps.AggregateListener;
import org.apache.openjpa.kernel.exps.FilterListener;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.UserException;

/**
 * Helper methods for dealing with query filters.
 *
 * @author Abe White
 */
public class Filters {

    private static final BigDecimal ZERO_BIGDECIMAL = new BigDecimal(0D);
    private static final BigInteger ZERO_BIGINTEGER = new BigInteger("0");

    private static final int OP_ADD = 0;
    private static final int OP_SUBTRACT = 1;
    private static final int OP_MULTIPLY = 2;
    private static final int OP_DIVIDE = 3;
    private static final int OP_MOD = 4;
    private static final int OP_POWER = 5;
    private static final int OP_ROUND = 6;

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

    /**
     * Return the correct wrapper type for the given class.
     */
    public static Class<?> wrap(Class<?> c) {
        if (!c.isPrimitive())
            return c;
        if (c == int.class)
            return Integer.class;
        if (c == float.class)
            return Float.class;
        if (c == double.class)
            return Double.class;
        if (c == long.class)
            return Long.class;
        if (c == boolean.class)
            return Boolean.class;
        if (c == short.class)
            return Short.class;
        if (c == byte.class)
            return Byte.class;
        if (c == char.class)
            return Character.class;
        return c;
    }

    /**
     * Return the correct primitive type for the given class, if it is a
     * wrapper.
     */
    public static Class<?> unwrap(Class<?> c) {
        if (c.isPrimitive() || c == String.class)
            return c;
        if (c == Integer.class)
            return int.class;
        if (c == Float.class)
            return float.class;
        if (c == Double.class)
            return double.class;
        if (c == Long.class)
            return long.class;
        if (c == Boolean.class)
            return boolean.class;
        if (c == Short.class)
            return short.class;
        if (c == Byte.class)
            return byte.class;
        if (c == Character.class)
            return char.class;
        return c;
    }

    /**
     * Given two types, return type they should both be converted
     * to before performing any operations between them.
     */
    public static Class<?> promote(Class<?> c1, Class<?> c2) {
        if (c1 == c2)
            return unwrap(c1);
        Class<?> w1 = wrap(c1);
        Class<?> w2 = wrap(c2);
        if (w1 == w2)
            return unwrap(c1);

        // not numbers?
        boolean w1Number = Number.class.isAssignableFrom(w1);
        boolean w2Number = Number.class.isAssignableFrom(w2);
        if (!w1Number || !w2Number) {
            // the only non-numeric promotion we do is string to char,
            // or from char/string to number
            if (!w1Number) {
                if (w2Number && (w1 == Character.class || w1 == String.class))
                    return (w2 == Byte.class || w2 == Short.class)
                        ? Integer.class : unwrap(c2);
                if (!w2Number && w1 == Character.class && w2 == String.class)
                    return String.class;
                if (w2Number)
                    return unwrap(c2);
            }
            if (!w2Number) {
                if (w1Number && (w2 == Character.class || w2 == String.class))
                    return (w1 == Byte.class || w1 == Short.class)
                        ? Integer.class : unwrap(c1);
                if (!w1Number && w2 == Character.class && w1 == String.class)
                    return String.class;
                if (w1Number)
                    return unwrap(c1);
            }

            // if neither are numbers, use least-derived of the two.  if neither
            // is assignable from the other but one is a standard type, assume
            // the other can be converted to that standard type
            if (!w1Number && !w2Number) {
                if (w1 == Object.class)
                    return unwrap(c2);
                if (w2 == Object.class)
                    return unwrap(c1);
                if (w1.isAssignableFrom(w2))
                    return unwrap(c1);
                if (w2.isAssignableFrom(w1))
                    return unwrap(c2);
                if (isNonstandardType(w1))
                    return (isNonstandardType(w2)) ? Object.class : unwrap(c2);
                if (isNonstandardType(w2))
                    return (isNonstandardType(w1)) ? Object.class : unwrap(c1);
            }
            return Object.class;
        }

        if (w1 == BigDecimal.class || w2 == BigDecimal.class)
            return BigDecimal.class;
        if (w1 == BigInteger.class) {
            if (w2 == Float.class || w2 == Double.class)
                return BigDecimal.class;
            return BigInteger.class;
        }
        if (w2 == BigInteger.class) {
            if (w1 == Float.class || w1 == Double.class)
                return BigDecimal.class;
            return BigInteger.class;
        }
        if (w1 == Double.class || w2 == Double.class)
            return double.class;
        if (w1 == Float.class || w2 == Float.class)
            return float.class;
        if (w1 == Long.class || w2 == Long.class)
            return long.class;
        return int.class;
    }

    /**
     * Return whether the given type is not a standard persistent type.
     */
    private static boolean isNonstandardType(Class<?> c) {
        switch (JavaTypes.getTypeCode(c))
        {
        case JavaTypes.ARRAY:
        case JavaTypes.COLLECTION:
        case JavaTypes.MAP:
        case JavaTypes.PC:
        case JavaTypes.PC_UNTYPED:
        case JavaTypes.OID:
        case JavaTypes.OBJECT:
            return true;
        default:
            return false;
        }
    }

    /**
     * Return whether an instance of the first class can be converted to
     * an instance of the second.
     */
    public static boolean canConvert(Class<?> c1, Class<?> c2, boolean strict) {
        if (c1 == c2)
            return true;
        c1 = wrap(c1);
        c2 = wrap(c2);
        if (c2.isAssignableFrom(c1))
            return true;

        boolean c1Number = Number.class.isAssignableFrom(c1);
        boolean c2Number = Number.class.isAssignableFrom(c2);
        if (c1Number && c2Number)
            return true;
        if ((c1Number && (c2 == Character.class
            || (!strict && c2 == String.class)))
            || (c2Number && (c1 == Character.class
            || (!strict && c1 == String.class))))
            return true;
        if (c1 == String.class && c2 == Character.class)
            return true;
        if (c2 == String.class)
            return !strict;
        if (c1 == String.class && isTemporalType(c2))
            return true;
        if ((c1 == java.util.Date.class ||c1 == java.sql.Time.class) && c2 == java.sql.Timestamp.class)
            return false;
        if ((c1 == java.util.Date.class ||c1 == java.sql.Timestamp.class) && c2 == java.sql.Time.class)
            return false;
        if (isTemporalType(c1) && isTemporalType(c2))
            return true;
        return false;
    }

    /**
     * Convert the given value to match the given (presumably a setter) method argument type.
     *
     * @param o given value
     * @param method a presumably setter method
     *
     * @return the same value if the method does not have one and only one input argument.
     */
    public static Object convertToMatchMethodArgument(Object o, Method method) {
        if (method == null || method.getParameterTypes().length != 1) {
            return o;
        }
        return convert(o, method.getParameterTypes()[0], true);
    }

    public static Object convert(Object o, Class<?> type) {
        return convert(o, type, false);
    }

    /**
     * Convert the given value to the given type.
     * @param o the given value
     * @param type the target type
     */
    public static Object convert(Object o, Class<?> type, boolean strictNumericConversion) {
        if (o == null)
            return null;
        if (o.getClass() == type)
            return o;

        type = wrap(type);
        if (type.isAssignableFrom(o.getClass()))
            return o;

        // the non-numeric conversions we do are to string, or from
        // string/char to number, or calendar/date
        // String to Boolean
        // String to Integer
        boolean num = o instanceof Number;
        if (!num) {
            if (type == String.class) {
                return o.toString();
            }
            else if (type == Boolean.class && o instanceof String) {
                return Boolean.valueOf(o.toString());
            }
            else if (type == Integer.class && o instanceof String) {
                try {
                    return new Integer(o.toString());
                }
                catch (NumberFormatException e) {
                    throw new ClassCastException(_loc.get("cant-convert", o, o.getClass(), type).getMessage());
                }
            }
            else if (type == Character.class) {
                String str = o.toString();
                if (str != null && str.length() == 1) {
                    return str.charAt(0);
                }
            }
            else if (Calendar.class.isAssignableFrom(type) && o instanceof Date) {
                Calendar cal = Calendar.getInstance();
                cal.setTime((Date) o);
                return cal;
            }
            else if (Date.class.isAssignableFrom(type) && o instanceof Calendar) {
                return ((Calendar) o).getTime();
            }
            else if (Number.class.isAssignableFrom(type)) {
                Integer i = null;
                if (o instanceof Character) {
                    i = Integer.valueOf((Character) o);
                }
                else if (o instanceof String && ((String) o).length() == 1) {
                    i = Integer.valueOf(((String) o));
                }

                if (i != null) {
                    if (type == Integer.class) {
                        return i;
                    }
                    num = true;
                }
            } else if (Temporal.class.isAssignableFrom(type)) {
                // handling of Java8 time API.
                if (LocalDate.class.equals(type)) {
                    if (o instanceof java.sql.Date) {
                        return ((java.sql.Date) o).toLocalDate();
                    } else if (o instanceof java.util.Date) {
                        return new java.sql.Date(((java.util.Date)o).getTime()).toLocalDate();
                    } else if (o instanceof CharSequence) {
                        return LocalDate.parse((CharSequence) o);
                    }
                } else if (LocalDateTime.class.equals(type)) {
                    if (o instanceof java.sql.Timestamp) {
                        return ((java.sql.Timestamp) o).toLocalDateTime();
                    } else if (o instanceof java.util.Date) {
                        return ((java.util.Date)o).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
                    } else if (o instanceof CharSequence) {
                        return LocalDateTime.parse((CharSequence) o);
                    }
                } else if (LocalTime.class.equals(type)) {
                    if (o instanceof java.sql.Time) {
                        return ((java.sql.Time) o).toLocalTime();
                    } else if (o instanceof java.util.Date) {
                        return ((java.util.Date)o).toInstant().atZone(ZoneId.systemDefault()).toLocalTime();
                    } else if (o instanceof CharSequence) {
                        return LocalTime.parse((CharSequence) o);
                    }
                } else if (OffsetTime.class.equals(type)) {
                    if (o instanceof java.sql.Time) {
                        return ((java.sql.Time) o).toLocalTime().atOffset(OffsetDateTime.now().getOffset());
                    } else if (o instanceof java.util.Date) {
                        return ((java.util.Date)o).toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime().toOffsetTime();
                    } else if (o instanceof CharSequence) {
                        return OffsetTime.parse((CharSequence) o);
                    }
                } else if (OffsetDateTime.class.equals(type)) {
                    if (o instanceof java.sql.Timestamp) {
                        return ((java.sql.Timestamp) o).toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime();
                    } else if (o instanceof java.util.Date) {
                        return ((java.util.Date)o).toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime();
                    } else if (o instanceof CharSequence) {
                        return OffsetDateTime.parse((CharSequence) o);
                    }
                }

            } else if (o instanceof String && isJDBCTemporalSyntax(o.toString())) {
                try {
                    Object temporal = parseJDBCTemporalSyntax(o.toString());
                    if (temporal != null && type.isAssignableFrom(temporal.getClass()))
                        return temporal;
                } catch (IllegalArgumentException e) {

                }
            } else if (o instanceof String && type.isEnum()) {
                return Enum.valueOf((Class<Enum>)type, o.toString());
            }
        }
        if (!num) {
            throw new ClassCastException(_loc.get("cant-convert", o, o.getClass(), type).getMessage());
        }

        if (type == Integer.class && allowNumericConversion(o.getClass(), type, strictNumericConversion)) {
            return ((Number) o).intValue();
        } else if (type == Float.class && allowNumericConversion(o.getClass(), type, strictNumericConversion)) {
            return ((Number) o).floatValue();
        } else if (type == Double.class) {
            return ((Number) o).doubleValue();
        } else if (type == Long.class && allowNumericConversion(o.getClass(), type, strictNumericConversion)) {
            return ((Number) o).longValue();
        } else if (type == BigDecimal.class) {
            // the BigDecimal constructor doesn't handle the
            // "NaN" string version of Double.NaN and Float.NaN, nor
            // does it handle infinity; we need to instead use the Double
            // and Float versions, despite wanting to cast it to BigDecimal
            double dval = ((Number) o).doubleValue();
            if (Double.isNaN(dval) || Double.isInfinite(dval)) {
                return dval;
            }
            float fval = ((Number) o).floatValue();
            if (Float.isNaN(fval) || Float.isInfinite(fval)) {
                return fval;
            }

            return new BigDecimal(o.toString());
        } else if (type == BigInteger.class) {
            return new BigInteger(o.toString());
        } else if (type == Short.class && allowNumericConversion(o.getClass(), type, strictNumericConversion)) {
            return ((Number) o).shortValue();
        } else if (type == Byte.class && allowNumericConversion(o.getClass(), type, strictNumericConversion)) {
            return ((Number) o).byteValue();
        } else if (type == Character.class) {
            return (char) ((Number) o).intValue();
        } else if (!strictNumericConversion) {
            return ((Number) o).intValue();
        } else {
            throw new ClassCastException(_loc.get("cant-convert", o, o.getClass(), type).getMessage());
        }
    }

    private static boolean allowNumericConversion(Class<?> actual, Class<?> target, boolean strict) {
        if (!strict || actual == target)
            return true;
        if (actual == Byte.class)    return false;
        if (actual == Double.class)  return target == Float.class;
        if (actual == Float.class)   return target == Double.class;
        if (actual == Integer.class) return target == Long.class || target == Short.class;
        if (actual == Long.class)    return target == Integer.class || target == Short.class;
        if (actual == Short.class)   return target == Long.class || target == Integer.class;
        return false;
    }

    /**
     * Add the given values.
     */
    public static Object add(Object o1, Class<?> c1, Object o2, Class<?> c2) {
        return op(o1, c1, o2, c2, OP_ADD);
    }

    /**
     * Subtract the given values.
     */
    public static Object subtract(Object o1, Class<?> c1, Object o2, Class<?> c2) {
        return op(o1, c1, o2, c2, OP_SUBTRACT);
    }

    /**
     * Multiply the given values.
     */
    public static Object multiply(Object o1, Class<?> c1, Object o2, Class<?> c2) {
        return op(o1, c1, o2, c2, OP_MULTIPLY);
    }

    /**
     * Divide the given values.
     */
    public static Object divide(Object o1, Class<?> c1, Object o2, Class<?> c2) {
        return op(o1, c1, o2, c2, OP_DIVIDE);
    }

    /**
     * Mod the given values.
     */
    public static Object mod(Object o1, Class<?> c1, Object o2, Class<?> c2) {
        return op(o1, c1, o2, c2, OP_MOD);
    }

    /**
     * Power the base to the exponent
     */
    public static Object power(Object o1, Class<?> c1, Object o2, Class<?> c2) {
        return op(o1, c1, o2, c2, OP_POWER);
    }

    public static Object round(Object o1, Class<?> c1, Object o2, Class<?> c2) {
        return op(o1, c1, o2, c2, OP_ROUND);
    }

    /**
     * Perform the given operation on two numbers.
     */
    private static Object op(Object o1, Class<?> c1, Object o2, Class<?> c2, int op) {
        Class<?> promote = promote(c1, c2);
        if (promote == int.class) {
            int n1 = (o1 == null) ? 0 : ((Number) o1).intValue();
            int n2 = (o2 == null) ? 0 : ((Number) o2).intValue();
            return op(n1, n2, op);
        }
        if (promote == float.class) {
            float n1 = (o1 == null) ? 0F : ((Number) o1).floatValue();
            float n2 = (o2 == null) ? 0F : ((Number) o2).floatValue();
            return op(n1, n2, op);
        }
        if (promote == double.class) {
            double n1 = (o1 == null) ? 0D : ((Number) o1).doubleValue();
            double n2 = (o2 == null) ? 0D : ((Number) o2).doubleValue();
            return op(n1, n2, op);
        }
        if (promote == long.class) {
            long n1 = (o1 == null) ? 0L : ((Number) o1).longValue();
            long n2 = (o2 == null) ? 0L : ((Number) o2).longValue();
            return op(n1, n2, op);
        }
        if (promote == BigDecimal.class) {
            BigDecimal n1 = (o1 == null) ? ZERO_BIGDECIMAL
                : (BigDecimal) convert(o1, promote);
            BigDecimal n2 = (o2 == null) ? ZERO_BIGDECIMAL
                : (BigDecimal) convert(o2, promote);
            return op(n1, n2, op);
        }
        if (promote == BigInteger.class) {
            BigInteger n1 = (o1 == null) ? ZERO_BIGINTEGER
                : (BigInteger) convert(o1, promote);
            BigInteger n2 = (o2 == null) ? ZERO_BIGINTEGER
                : (BigInteger) convert(o2, promote);
            return op(n1, n2, op);
        }
        // default to int
        int n1 = (o1 == null) ? 0 : ((Number) o1).intValue();
        int n2 = (o2 == null) ? 0 : ((Number) o2).intValue();
        return op(n1, n2, op);
    }

    /**
     * Return the result of a mathematical operation.
     */
    private static Object op(int n1, int n2, int op) {
        int tot;
        switch (op) {
            case OP_ADD:
                tot = n1 + n2;
                break;
            case OP_SUBTRACT:
                tot = n1 - n2;
                break;
            case OP_MULTIPLY:
                tot = n1 * n2;
                break;
            case OP_DIVIDE:
                tot = n1 / n2;
                break;
            case OP_MOD:
                tot = n1 % n2;
                break;
            case OP_POWER:
                return Math.pow(n1, n2);
            case OP_ROUND:
                tot = n1;
            default:
                throw new InternalException();
        }
        return tot;
    }

    /**
     * Return the result of a mathematical operation.
     */
    private static Object op(float n1, float n2, int op) {
        float tot;
        switch (op) {
            case OP_ADD:
                tot = n1 + n2;
                break;
            case OP_SUBTRACT:
                tot = n1 - n2;
                break;
            case OP_MULTIPLY:
                tot = n1 * n2;
                break;
            case OP_DIVIDE:
                tot = n1 / n2;
                break;
            case OP_MOD:
                tot = n1 % n2;
                break;
            case OP_POWER:
                return Math.pow(n1, n2);
            case OP_ROUND:
                BigDecimal bg = new BigDecimal(Float.toString(n1));
                return bg.setScale(Math.toIntExact(Math.round(n2)), RoundingMode.HALF_EVEN).floatValue();
        default:
                throw new InternalException();
        }
        return tot;
    }

    /**
     * Return the result of a mathematical operation.
     */
    private static Object op(double n1, double n2, int op) {
        double tot;
        switch (op) {
            case OP_ADD:
                tot = n1 + n2;
                break;
            case OP_SUBTRACT:
                tot = n1 - n2;
                break;
            case OP_MULTIPLY:
                tot = n1 * n2;
                break;
            case OP_DIVIDE:
                tot = n1 / n2;
                break;
            case OP_MOD:
                tot = n1 % n2;
                break;
            case OP_POWER:
                tot = Math.pow(n1, n2);
                break;
            case OP_ROUND:
                BigDecimal bg = new BigDecimal(Double.toString(n1));
                return bg.setScale(Math.toIntExact(Math.round(n2)), RoundingMode.HALF_EVEN).doubleValue();
            default:
                throw new InternalException();
        }
        return tot;
    }

    /**
     * Return the result of a mathematical operation.
     */
    private static Object op(long n1, long n2, int op) {
        long tot;
        switch (op) {
            case OP_ADD:
                tot = n1 + n2;
                break;
            case OP_SUBTRACT:
                tot = n1 - n2;
                break;
            case OP_MULTIPLY:
                tot = n1 * n2;
                break;
            case OP_DIVIDE:
                tot = n1 / n2;
                break;
            case OP_MOD:
                tot = n1 % n2;
                break;
            case OP_POWER:
                return Math.pow(n1, n2);
            case OP_ROUND:
                return n1;
            default:
                throw new InternalException();
        }
        return tot;
    }

    /**
     * Return the result of a mathematical operation.
     */
    private static Object op(BigDecimal n1, BigDecimal n2, int op) {
        switch (op) {
            case OP_ADD:
                return n1.add(n2);
            case OP_SUBTRACT:
                return n1.subtract(n2);
            case OP_MULTIPLY:
                return n1.multiply(n2);
            case OP_DIVIDE:
                int scale = Math.max(n1.scale(), n2.scale());
                return n1.divide(n2, scale, BigDecimal.ROUND_HALF_UP);
            case OP_MOD:
                throw new UserException(_loc.get("mod-bigdecimal"));
            case OP_POWER:
                return n1.pow(n2.intValue());
            case OP_ROUND:
                return n1.setScale(n2.intValue(), RoundingMode.HALF_EVEN);
            default:
                throw new InternalException();
        }
    }

    /**
     * Return the result of a mathematical operation.
     */
    private static Object op(BigInteger n1, BigInteger n2, int op) {
        switch (op) {
            case OP_ADD:
                return n1.add(n2);
            case OP_SUBTRACT:
                return n1.subtract(n2);
            case OP_MULTIPLY:
                return n1.multiply(n2);
            case OP_DIVIDE:
                return n1.divide(n2);
            case OP_POWER:
                return n1.pow(n2.intValue());
            case OP_ROUND:
                return n1;
            default:
                throw new InternalException();
        }
    }

    /**
     * Parses the given declarations into a list of type, name, type, name...
     * Returns null if no declarations. Assumes declaration is not an empty
     * string and is already trimmed (valid assumptions given the checks made
     * in our setters).
     *
     * @param decType the type of declaration being parsed, for use in
     * error messages
     */
    public static List<String> parseDeclaration(String dec, char split, String decType) {
        if (dec == null)
            return null;

        // watch for common mixups between commas and semis
        char bad = (char) 0;
        if (split == ',')
            bad = ';';
        else if (split == ';')
            bad = ',';

        char sentinal = ' ';
        char cur;
        int start = 0;
        boolean skipSpace = false;
        List<String> results = new ArrayList<>(6);
        for (int i = 0; i < dec.length(); i++) {
            cur = dec.charAt(i);
            if (cur == bad)
                throw new UserException(_loc.get("bad-dec", dec, decType));
            if (cur == ' ' && skipSpace) {
                start++;
                continue;
            }

            skipSpace = false;
            if (cur != sentinal)
                continue;

            // if looking for spaces, look for split char, or vice versa
            sentinal = (sentinal == ' ') ? split : ' ';
            results.add(dec.substring(start, i).trim());
            start = i + 1;
            skipSpace = true;
        }

        // add last token, if any
        if (start < dec.length())
            results.add(dec.substring(start));

        // if not an even number of elements, something is wrong
        if (results.isEmpty() || results.size() % 2 != 0)
            throw new UserException(_loc.get("bad-dec", dec, decType));

        return results;
    }

    /**
     * Split the given expression list into distinct expressions. Assumes the
     * given string is not null or of zero length and is already trimmed
     * (valid assumptions given the checks in our setters and before
     * this method call).
     */
    public static List<String> splitExpressions(String str, char split, int expected) {
        if (str == null)
            return null;

        List<String> exps = null;
        int parenDepth = 0;
        int begin = 0, pos = 0;
        boolean escape = false;
        boolean string = false;
        boolean nonspace = false;
        char quote = 0;
        for (char c; pos < str.length(); pos++) {
            c = str.charAt(pos);
            if (c == '\\') {
                escape = !escape;
                continue;
            }
            if (escape) {
                escape = false;
                continue;
            }

            switch (c) {
                case '\'':
                case '"':
                    if (string && quote == c)
                        string = false;
                    else if (!string) {
                        quote = c;
                        string = true;
                    }
                    nonspace = true;
                    break;
                case '(':
                    if (!string)
                        parenDepth++;
                    nonspace = true;
                    break;
                case ')':
                    if (!string)
                        parenDepth--;
                    nonspace = true;
                    break;
                case ' ':
                case '\t':
                case '\n':
                case '\r':
                    if (c == split && !string && parenDepth == 0 && nonspace) {
                        if (exps == null)
                            exps = new ArrayList<>(expected);
                        exps.add(str.substring(begin, pos).trim());
                        begin = pos + 1;
                        nonspace = false;
                    }
                    break;
                default:
                    if (c == split && !string && parenDepth == 0) {
                        if (exps == null)
                            exps = new ArrayList<>(expected);
                        exps.add(str.substring(begin, pos).trim());
                        begin = pos + 1;
                    }
                    nonspace = true;
            }
            escape = false;
        }

        if (exps == null) {
            exps = Collections.singletonList(str);
            return exps;
        }

        // add last expression and return array
        String last = str.substring(begin).trim();
        if (last.length() > 0)
            exps.add(last);
        return exps;
    }

    /**
     * Add the given access path metadatas to the full path list, making sure
     * to maintain only base metadatas in the list. The given list may be null.
     */
    public static List<ClassMetaData> addAccessPathMetaDatas(List<ClassMetaData> metas, ClassMetaData[] path) {
        if (path == null || path.length == 0)
            return metas;

        // create set of base class metadatas in access path
        if (metas == null)
            metas = new ArrayList<>();
        int last = metas.size();

        // for every element in the path of this executor, compare it
        // to already-gathered elements to see if it should replace
        // a subclass in the list or should be added as a new base;
        // at least it's n^2 of a small n...
        ClassMetaData meta;
        boolean add;
        for (ClassMetaData classMetaData : path) {
            add = true;
            for (int j = 0; add && j < last; j++) {
                meta = metas.get(j);

                if (meta.getDescribedType().isAssignableFrom(classMetaData.getDescribedType())) {
                    // list already contains base class
                    add = false;
                }
                else if (classMetaData.getDescribedType().isAssignableFrom(meta.getDescribedType())) {
                    // this element replaces its subclass
                    add = false;
                    metas.set(j, classMetaData);
                }
            }

            // if no base class of current path element already in
            // list and path element didn't replace a subclass in the
            // list, then add it now as a new base
            if (add)
                metas.add(classMetaData);
        }
        return metas;
    }

    /**
     * Convert the user-given hint value to an aggregate listener.
     * The hint can be an aggregate listener instance or class name.
     */
    public static AggregateListener hintToAggregateListener(Object hint, ClassLoader loader) {
        if (hint == null)
            return null;
        if (hint instanceof AggregateListener)
            return (AggregateListener) hint;

        Exception cause = null;
        if (hint instanceof String) {
            try {
                return (AggregateListener) AccessController.doPrivileged(
                    J2DoPrivHelper.newInstanceAction(Class.forName((String) hint, true, loader)));
            } catch (Exception e) {
                if (e instanceof PrivilegedActionException)
                    e = ((PrivilegedActionException) e).getException();
                cause = e;
            }
        }
        throw new UserException(_loc.get("bad-agg-listener-hint", hint,
            hint.getClass())).setCause(cause);
    }

    /**
     * Convert the user-given hint value to an array of aggregate listeners.
     * The hint can be an aggregate listener, aggregate listener array,
     * collection, or comma-separated class names.
     */
    public static AggregateListener[] hintToAggregateListeners(Object hint, ClassLoader loader) {
        if (hint == null)
            return null;
        if (hint instanceof AggregateListener[])
            return (AggregateListener[]) hint;
        if (hint instanceof AggregateListener)
            return new AggregateListener[]{ (AggregateListener) hint };
        if (hint instanceof Collection) {
            Collection<AggregateListener> c = (Collection<AggregateListener>) hint;
            return c.toArray(new AggregateListener[c.size()]);
        }

        Exception cause = null;
        if (hint instanceof String) {
            String[] clss = StringUtil.split((String) hint, ",", 0);
            AggregateListener[] aggs = new AggregateListener[clss.length];
            try {
                for (int i = 0; i < clss.length; i++)
                    aggs[i] = (AggregateListener) AccessController.doPrivileged(
                        J2DoPrivHelper.newInstanceAction(
                            Class.forName(clss[i], true, loader)));
                return aggs;
            } catch (Exception e) {
                if (e instanceof PrivilegedActionException)
                    e = ((PrivilegedActionException) e).getException();
                cause = e;
            }
        }
        throw new UserException(_loc.get("bad-agg-listener-hint", hint,
            hint.getClass())).setCause(cause);
    }

    /**
     * Convert the user-given hint value to a filter listener.
     * The hint can be a filter listener instance or class name.
     */
    public static FilterListener hintToFilterListener(Object hint, ClassLoader loader) {
        if (hint == null)
            return null;
        if (hint instanceof FilterListener)
            return (FilterListener) hint;

        Exception cause = null;
        if (hint instanceof String) {
            try {
                return (FilterListener) AccessController.doPrivileged(
                    J2DoPrivHelper.newInstanceAction(
                        Class.forName((String) hint, true, loader)));
            } catch (Exception e) {
                if (e instanceof PrivilegedActionException)
                    e = ((PrivilegedActionException) e).getException();
                cause = e;
            }
        }
        throw new UserException(_loc.get("bad-filter-listener-hint", hint,
            hint.getClass())).setCause(cause);
    }

    /**
     * Convert the user-given hint value to an array of filter listeners.
     * The hint can be a filter listener, filter listener array,
     * collection, or comma-separated class names.
     */
    public static FilterListener[] hintToFilterListeners(Object hint, ClassLoader loader) {
        if (hint == null)
            return null;
        if (hint instanceof FilterListener[])
            return (FilterListener[]) hint;
        if (hint instanceof FilterListener)
            return new FilterListener[]{ (FilterListener) hint };
        if (hint instanceof Collection) {
            Collection<FilterListener> c = (Collection<FilterListener>) hint;
            return c.toArray(new FilterListener[c.size()]);
        }

        Exception cause = null;
        if (hint instanceof String) {
            String[] clss = StringUtil.split((String) hint, ",", 0);
            FilterListener[] filts = new FilterListener[clss.length];
            try {
                for (int i = 0; i < clss.length; i++)
                    filts[i] = (FilterListener) AccessController.doPrivileged(
                        J2DoPrivHelper.newInstanceAction(
                            Class.forName(clss[i], true, loader)));
                return filts;
            } catch (Exception e) {
                if (e instanceof PrivilegedActionException)
                    e = ((PrivilegedActionException) e).getException();
                cause = e;
            }
        }
        throw new UserException(_loc.get("bad-filter-listener-hint", hint,
            hint.getClass())).setCause(cause);
    }

    /**
     * Return the value of the property named by the hint key.
     */
    public static Object hintToGetter(Object target, String hintKey) {
        if (target == null || hintKey == null)
            return null;

        Method getter = Reflection.findGetter(target.getClass(), hintKey, true);
        return Reflection.get(target, getter);
    }

    /**
     * Set the value of the property named by the hint key.
     */
    public static void hintToSetter(Object target, String hintKey, Object value) {
        if (target == null || hintKey == null)
            return;

        Method setter = Reflection.findSetter(target.getClass(), hintKey, true);
        if (value instanceof String) {
            if ("null".equals(value))
                value = null;
            else {
                try {
                    value = StringUtil.parse((String) value, setter.getParameterTypes()[0]);
                } catch (Exception e) {
                    throw new UserException(_loc.get("bad-setter-hint-arg",
                        hintKey, value, setter.getParameterTypes()[0])).
                        setCause(e);
                }
            }
        }
        Reflection.set(target, setter, value);
    }

    /**
     * Parses the given string assuming it is a JDBC key expression. Extracts the
     * data portion and based on the key, calls static java.sql.Date/Time/Timestamp.valueOf(String)
     * method to convert to a java.sql.Date/Time/Timestamp instance.
     */
    public static Object parseJDBCTemporalSyntax(String s) {
        s = clip(s.trim(), "{", "}", true);
        if (s.startsWith("ts")) {
            return java.sql.Timestamp.valueOf(clip(s.substring(2).trim(), "'", "'", false));
        } else if (s.startsWith("d")) {
            return java.sql.Date.valueOf(clip(s.substring(1).trim(), "'", "'", false));
        } else if (s.startsWith("t")) {
            return java.sql.Time.valueOf(clip(s.substring(2).trim(), "'", "'", false));
        } else {
            return null;
        }
    }

    /**
     * Affirms if the given String is enclosed in {}.
     *
     */
    public static boolean isJDBCTemporalSyntax(String s) {
        if (s != null) {
            s = s.trim();
        }
        return s != null && s.startsWith("{") && s.endsWith("}");
    }

    /**
     * Removes the first and last string if they are the terminal sequence in the given string.
     *
     * @param s a string to be examined
     * @param first the characters in the beginning of the given string
     * @param last the characters in the end of the given string
     * @param fail if true throws exception if the given string does not have the given terminal sequences.
     * @return the string with terminal sequences removed.
     */
    public static String clip(String s, String first, String last, boolean fail) {
        if (s == null)
            return s;
        if (s.startsWith(first) && s.endsWith(last)) {
            return s.substring(first.length(), s.length()-last.length()).trim();
        }
        if (fail) {
            throw new IllegalArgumentException(s + " is not valid escape syntax for JDBC");
        }
        return s;
    }

    /**
     * Affirms if the given class is Data, Time or Timestamp.
     */
    public static boolean isTemporalType(Class<?> c) {
        return c != null
            && (Date.class.isAssignableFrom(c)
             || Time.class.isAssignableFrom(c)
             || Timestamp.class.isAssignableFrom(c)
             || Calendar.class.isAssignableFrom(c)
             || LocalDate.class == c  // java.time classes are final, so we can compare with ==
             || LocalDateTime.class == c
             || LocalTime.class == c
             || OffsetTime.class ==c
             || OffsetDateTime.class == c);
    }

    public static Object getDefaultForNull(Class<?> nType) {
        if (nType == Long.class)
            return 0L;
        if (nType == Integer.class)
            return 0;
        if (nType == Double.class)
            return 0.0;
        if (nType == Float.class)
            return 0.0F;
        if (nType == Short.class)
            return (short) 0;
        return null;
    }

}
