/*
 * 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.struts2.conversion.impl;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.conversion.TypeConversionException;

import java.lang.reflect.Member;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.Map;

public class NumberConverter extends DefaultTypeConverter {

    private static final Logger LOG = LogManager.getLogger(NumberConverter.class);

    public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName, Object value, Class toType) {
        if (value instanceof String) {
            String stringValue = String.valueOf(value);

            if (toType == BigDecimal.class) {
                return convertToBigDecimal(context, stringValue);
            } else if (toType == BigInteger.class) {
                return new BigInteger(stringValue);
            } else if (toType == Double.class || toType == double.class) {
                return convertToDouble(context, stringValue);
            } else if (toType == Float.class || toType == float.class) {
                return convertToFloat(context, stringValue);
            } else if (toType.isPrimitive()) {
                Object convertedValue = super.convertValue(context, value, toType);

                if (!isInRange((Number) convertedValue, stringValue, toType))
                    throw new TypeConversionException("Overflow or underflow casting: \"" + stringValue + "\" into class " + convertedValue.getClass().getName());

                return convertedValue;
            } else {
                if (stringValue.isEmpty()) {
                    return null;
                }
                NumberFormat numFormat = NumberFormat.getInstance(getLocale(context));
                ParsePosition parsePos = new ParsePosition(0);
                if (isIntegerType(toType)) {
                    numFormat.setParseIntegerOnly(true);
                }
                numFormat.setGroupingUsed(true);
                Number number = numFormat.parse(stringValue, parsePos);

                if (parsePos.getIndex() != stringValue.length()) {
                    throw new TypeConversionException("Unparseable number: \"" + stringValue + "\" at position "
                        + parsePos.getIndex());
                } else {
                    if (!isInRange(number, stringValue, toType))
                        throw new TypeConversionException("Overflow or underflow casting: \"" + stringValue + "\" into class " + number.getClass().getName());

                    value = super.convertValue(context, number, toType);
                }
            }
        } else if (value instanceof Object[] objArray) {

            if (objArray.length == 1) {
                return convertValue(context, null, null, null, objArray[0], toType);
            }
        }

        // pass it through DefaultTypeConverter
        return super.convertValue(context, value, toType);
    }

    protected Object convertToBigDecimal(Map<String, Object> context, String stringValue) {
        Locale locale = getLocale(context);

        NumberFormat format = getNumberFormat(locale);
        if (format instanceof DecimalFormat) {
            ((DecimalFormat) format).setParseBigDecimal(true);
            char separator = ((DecimalFormat) format).getDecimalFormatSymbols().getGroupingSeparator();
            stringValue = normalize(stringValue, separator);
        }

        LOG.debug("Trying to convert a value {} with locale {} to BigDecimal", stringValue, locale);
        ParsePosition parsePosition = new ParsePosition(0);
        Number number = format.parse(stringValue, parsePosition);

        if (parsePosition.getIndex() != stringValue.length()) {
            throw new TypeConversionException("Unparseable number: \"" + stringValue + "\" at position " + parsePosition.getIndex());
        }

        return number;
    }

    protected Object convertToDouble(Map<String, Object> context, String stringValue) {
        Locale locale = getLocale(context);

        NumberFormat format = getNumberFormat(locale);
        if (format instanceof DecimalFormat) {
            char separator = ((DecimalFormat) format).getDecimalFormatSymbols().getGroupingSeparator();
            stringValue = normalize(stringValue, separator);
        }

        LOG.debug("Trying to convert a value {} with locale {} to Double", stringValue, locale);
        ParsePosition parsePosition = new ParsePosition(0);
        Number number = format.parse(stringValue, parsePosition);

        if (parsePosition.getIndex() != stringValue.length()) {
            throw new TypeConversionException("Unparseable number: \"" + stringValue + "\" at position " + parsePosition.getIndex());
        }

        if (!isInRange(number, stringValue, Double.class)) {
            throw new TypeConversionException("Overflow or underflow converting: \"" + stringValue + "\" into class " + number.getClass().getName());
        }

        if (number != null) {
            return number.doubleValue();
        }

        return null;
    }

    protected Object convertToFloat(Map<String, Object> context, String stringValue) {
        Locale locale = getLocale(context);

        NumberFormat format = getNumberFormat(locale);
        if (format instanceof DecimalFormat) {
            char separator = ((DecimalFormat) format).getDecimalFormatSymbols().getGroupingSeparator();
            stringValue = normalize(stringValue, separator);
        }

        LOG.debug("Trying to convert a value {} with locale {} to Float", stringValue, locale);
        ParsePosition parsePosition = new ParsePosition(0);
        Number number = format.parse(stringValue, parsePosition);

        if (parsePosition.getIndex() != stringValue.length()) {
            throw new TypeConversionException("Unparseable number: \"" + stringValue + "\" at position " + parsePosition.getIndex());
        }

        if (!isInRange(number, stringValue, Float.class)) {
            throw new TypeConversionException("Overflow or underflow converting: \"" + stringValue + "\" into class " + number.getClass().getName());
        }

        if (number != null) {
            return number.floatValue();
        }

        return null;
    }

    protected NumberFormat getNumberFormat(Locale locale) {
        NumberFormat format = NumberFormat.getNumberInstance(locale);
        format.setGroupingUsed(true);
        return format;
    }

    protected String normalize(String strValue, char separator) {
        // this is a hack as \160 isn't the same as " " (an empty space)
        if (separator == 160) {
            strValue = strValue.replaceAll(" ", String.valueOf(separator));
        }
        return strValue;
    }

    protected boolean isInRange(Number value, String stringValue, Class toType) {
        Number bigValue;
        Number lowerBound;
        Number upperBound;

        try {
            if (double.class == toType || Double.class == toType) {
                bigValue = new BigDecimal(stringValue);
                // Double.MIN_VALUE is the smallest positive non-zero number
                lowerBound = BigDecimal.valueOf(Double.MAX_VALUE).negate();
                upperBound = BigDecimal.valueOf(Double.MAX_VALUE);
            } else if (float.class == toType || Float.class == toType) {
                bigValue = new BigDecimal(stringValue);
                // Float.MIN_VALUE is the smallest positive non-zero number
                lowerBound = BigDecimal.valueOf(Float.MAX_VALUE).negate();
                upperBound = BigDecimal.valueOf(Float.MAX_VALUE);
            } else if (byte.class == toType || Byte.class == toType) {
                bigValue = new BigInteger(stringValue);
                lowerBound = BigInteger.valueOf(Byte.MIN_VALUE);
                upperBound = BigInteger.valueOf(Byte.MAX_VALUE);
            } else if (char.class == toType || Character.class == toType) {
                bigValue = new BigInteger(stringValue);
                lowerBound = BigInteger.valueOf(Character.MIN_VALUE);
                upperBound = BigInteger.valueOf(Character.MAX_VALUE);
            } else if (short.class == toType || Short.class == toType) {
                bigValue = new BigInteger(stringValue);
                lowerBound = BigInteger.valueOf(Short.MIN_VALUE);
                upperBound = BigInteger.valueOf(Short.MAX_VALUE);
            } else if (int.class == toType || Integer.class == toType) {
                bigValue = new BigInteger(stringValue);
                lowerBound = BigInteger.valueOf(Integer.MIN_VALUE);
                upperBound = BigInteger.valueOf(Integer.MAX_VALUE);
            } else if (long.class == toType || Long.class == toType) {
                bigValue = new BigInteger(stringValue);
                lowerBound = BigInteger.valueOf(Long.MIN_VALUE);
                upperBound = BigInteger.valueOf(Long.MAX_VALUE);
            } else {
                throw new IllegalArgumentException("Unexpected numeric type: " + toType.getName());
            }
        } catch (NumberFormatException e) {
            //shoult it fail here? BigInteger doesnt seem to be so nice parsing numbers as NumberFormat
            return true;
        }

        return ((Comparable) bigValue).compareTo(lowerBound) >= 0 && ((Comparable) bigValue).compareTo(upperBound) <= 0;
    }

    private boolean isIntegerType(Class type) {
        return double.class != type && float.class != type && Double.class != type && Float.class != type
                && char.class != type && Character.class != type;
    }

}
