/*
 * Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.value;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;

import org.h2.api.ErrorCode;
import org.h2.engine.CastDataProvider;
import org.h2.message.DbException;

/**
 * Implementation of the NUMERIC data type.
 */
public final class ValueNumeric extends ValueBigDecimalBase {

    /**
     * The value 'zero'.
     */
    public static final ValueNumeric ZERO = new ValueNumeric(BigDecimal.ZERO);

    /**
     * The value 'one'.
     */
    public static final ValueNumeric ONE = new ValueNumeric(BigDecimal.ONE);

    /**
     * The default scale for a NUMERIC value.
     */
    public static final int DEFAULT_SCALE = 0;

    /**
     * The maximum scale.
     */
    public static final int MAXIMUM_SCALE = 100_000;

    private ValueNumeric(BigDecimal value) {
        super(value);
        if (value == null) {
            throw new IllegalArgumentException("null");
        }
        int scale = value.scale();
        if (scale < 0 || scale > MAXIMUM_SCALE) {
            throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale), "0", "" + MAXIMUM_SCALE);
        }
    }

    @Override
    public String getString() {
        return value.toPlainString();
    }

    @Override
    public StringBuilder getSQL(StringBuilder builder, int sqlFlags) {
        String s = getString();
        if ((sqlFlags & NO_CASTS) == 0 && s.indexOf('.') < 0 && value.compareTo(MAX_LONG_DECIMAL) <= 0
                && value.compareTo(MIN_LONG_DECIMAL) >= 0) {
            return builder.append("CAST(").append(value).append(" AS NUMERIC(").append(value.precision()).append("))");
        }
        return builder.append(s);
    }

    @Override
    public TypeInfo getType() {
        TypeInfo type = this.type;
        if (type == null) {
            this.type = type = new TypeInfo(NUMERIC, value.precision(), value.scale(), null);
        }
        return type;
    }

    @Override
    public int getValueType() {
        return NUMERIC;
    }

    @Override
    public Value add(Value v) {
        return get(value.add(((ValueNumeric) v).value));
    }

    @Override
    public Value subtract(Value v) {
        return get(value.subtract(((ValueNumeric) v).value));
    }

    @Override
    public Value negate() {
        return get(value.negate());
    }

    @Override
    public Value multiply(Value v) {
        return get(value.multiply(((ValueNumeric) v).value));
    }

    @Override
    public Value divide(Value v, TypeInfo quotientType) {
        BigDecimal divisor = ((ValueNumeric) v).value;
        if (divisor.signum() == 0) {
            throw DbException.get(ErrorCode.DIVISION_BY_ZERO_1, getTraceSQL());
        }
        return get(value.divide(divisor, quotientType.getScale(), RoundingMode.HALF_DOWN));
    }

    @Override
    public Value modulus(Value v) {
        ValueBigDecimalBase dec = (ValueNumeric) v;
        if (dec.value.signum() == 0) {
            throw DbException.get(ErrorCode.DIVISION_BY_ZERO_1, getTraceSQL());
        }
        return get(value.remainder(dec.value));
    }

    @Override
    public int compareTypeSafe(Value o, CompareMode mode, CastDataProvider provider) {
        return value.compareTo(((ValueNumeric) o).value);
    }

    @Override
    public int getSignum() {
        return value.signum();
    }

    @Override
    public BigDecimal getBigDecimal() {
        return value;
    }

    @Override
    public float getFloat() {
        return value.floatValue();
    }

    @Override
    public double getDouble() {
        return value.doubleValue();
    }

    @Override
    public int hashCode() {
        return getClass().hashCode() * 31 + value.hashCode();
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof ValueNumeric && value.equals(((ValueNumeric) other).value);
    }

    @Override
    public int getMemory() {
        return value.precision() + 120;
    }

    /**
     * Get or create a NUMERIC value for the given big decimal.
     *
     * @param dec the big decimal
     * @return the value
     */
    public static ValueNumeric get(BigDecimal dec) {
        if (BigDecimal.ZERO.equals(dec)) {
            return ZERO;
        } else if (BigDecimal.ONE.equals(dec)) {
            return ONE;
        }
        return (ValueNumeric) Value.cache(new ValueNumeric(dec));
    }

    /**
     * Get or create a NUMERIC value for the given big decimal with possibly
     * negative scale. If scale is negative, it is normalized to 0.
     *
     * @param dec
     *            the big decimal
     * @return the value
     */
    public static ValueNumeric getAnyScale(BigDecimal dec) {
        if (dec.scale() < 0) {
            dec = dec.setScale(0, RoundingMode.UNNECESSARY);
        }
        return get(dec);
    }

    /**
     * Get or create a NUMERIC value for the given big integer.
     *
     * @param bigInteger the big integer
     * @return the value
     */
    public static ValueNumeric get(BigInteger bigInteger) {
        if (bigInteger.signum() == 0) {
            return ZERO;
        } else if (BigInteger.ONE.equals(bigInteger)) {
            return ONE;
        }
        return (ValueNumeric) Value.cache(new ValueNumeric(new BigDecimal(bigInteger)));
    }

    /**
     * Set the scale of a BigDecimal value.
     *
     * @param bd the BigDecimal value
     * @param scale the new scale
     * @return the scaled value
     */
    public static BigDecimal setScale(BigDecimal bd, int scale) {
        if (scale < 0 || scale > MAXIMUM_SCALE) {
            throw DbException.getInvalidValueException("scale", scale);
        }
        return bd.setScale(scale, RoundingMode.HALF_UP);
    }

}
