/*
 *  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.codehaus.groovy.runtime.typehandling;

import org.apache.groovy.util.SystemUtil;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

/**
 * Arbitrary-precision decimal arithmetic operations.
 * <p>
 * Singleton instance providing NumberMath implementations for BigDecimal-based operations.
 * Supports exact decimal arithmetic with configurable precision and rounding.
 * This is the default math provider for non-floating-point division operations.
 */
public final class BigDecimalMath extends NumberMath {

    /**
     * Extra precision added during division when a non-terminating result would otherwise occur.
     * <p>
     * Configurable via system property "groovy.division.extra.precision", defaults to 10.
     */
    public static final int DIVISION_EXTRA_PRECISION = SystemUtil.getIntegerSafe("groovy.division.extra.precision", 10);

    /**
     * Minimum scale (decimal places) for division results.
     * <p>
     * Configurable via system property "groovy.division.min.scale", defaults to 10.
     */
    public static final int DIVISION_MIN_SCALE = SystemUtil.getIntegerSafe("groovy.division.min.scale", 10);

    /**
     * Singleton instance of BigDecimalMath.
     */
    public static final BigDecimalMath INSTANCE = new BigDecimalMath();

    private BigDecimalMath() {}

    @Override
    public Number absImpl(Number number) {
        return toBigDecimal(number).abs();
    }

    @Override
    public Number addImpl(Number left, Number right) {
        return toBigDecimal(left).add(toBigDecimal(right));
    }

    @Override
    public Number subtractImpl(Number left, Number right) {
        return toBigDecimal(left).subtract(toBigDecimal(right));
    }

    @Override
    public Number multiplyImpl(Number left, Number right) {
        return toBigDecimal(left).multiply(toBigDecimal(right));
    }

    @Override
    public Number divideImpl(Number left, Number right) {
        BigDecimal bigLeft = toBigDecimal(left);
        BigDecimal bigRight = toBigDecimal(right);
        try {
            return bigLeft.divide(bigRight);
        } catch (ArithmeticException e) {
            // set a DEFAULT precision if otherwise non-terminating
            int precision = Math.max(bigLeft.precision(), bigRight.precision()) + DIVISION_EXTRA_PRECISION;
            BigDecimal result = bigLeft.divide(bigRight, new MathContext(precision));
            int scale = Math.max(Math.max(bigLeft.scale(), bigRight.scale()), DIVISION_MIN_SCALE);
            if (result.scale() > scale) result = result.setScale(scale, RoundingMode.HALF_UP);
            return result;
        }
    }

    @Override
    public int compareToImpl(Number left, Number right) {
        return toBigDecimal(left).compareTo(toBigDecimal(right));
    }

    @Override
    public Number unaryMinusImpl(Number left) {
        return toBigDecimal(left).negate();
    }

    @Override
    public Number unaryPlusImpl(Number left) {
        return toBigDecimal(left);
    }

    @Override
    public Number remainderImpl(Number left, Number right) {
        return toBigDecimal(left).remainder(toBigDecimal(right));
    }

    @Override
    public Number modImpl(Number self, Number divisor) {
        BigDecimal selfDecimal = toBigDecimal(self);
        BigDecimal divDecimal = toBigDecimal(divisor);
        BigDecimal remainder = selfDecimal.remainder(divDecimal);
        return remainder.signum() < 0 ? remainder.add(divDecimal) : remainder;
    }
}
