/*
 * 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.arrow.gandiva.evaluator;

import org.apache.arrow.vector.types.pojo.ArrowType.Decimal;

/** Utility methods for working with {@link Decimal} values. */
public class DecimalTypeUtil {
  private DecimalTypeUtil() {}

  /** Enum for supported mathematical operations. */
  public enum OperationType {
    ADD,
    SUBTRACT,
    MULTIPLY,
    DIVIDE,
    MOD
  }

  private static final int MIN_ADJUSTED_SCALE = 6;
  /// The maximum precision representable by a 16-byte decimal
  private static final int MAX_PRECISION = 38;

  /** Determines the scale and precision of applying the given operation to the operands. */
  public static Decimal getResultTypeForOperation(
      OperationType operation, Decimal operand1, Decimal operand2) {
    int s1 = operand1.getScale();
    int s2 = operand2.getScale();
    int p1 = operand1.getPrecision();
    int p2 = operand2.getPrecision();
    int resultScale = 0;
    int resultPrecision = 0;
    switch (operation) {
      case ADD:
      case SUBTRACT:
        resultScale = Math.max(operand1.getScale(), operand2.getScale());
        resultPrecision =
            resultScale
                + Math.max(
                    operand1.getPrecision() - operand1.getScale(),
                    operand2.getPrecision() - operand2.getScale())
                + 1;
        break;
      case MULTIPLY:
        resultScale = s1 + s2;
        resultPrecision = p1 + p2 + 1;
        break;
      case DIVIDE:
        resultScale =
            Math.max(MIN_ADJUSTED_SCALE, operand1.getScale() + operand2.getPrecision() + 1);
        resultPrecision =
            operand1.getPrecision() - operand1.getScale() + operand2.getScale() + resultScale;
        break;
      case MOD:
        resultScale = Math.max(operand1.getScale(), operand2.getScale());
        resultPrecision =
            Math.min(
                    operand1.getPrecision() - operand1.getScale(),
                    operand2.getPrecision() - operand2.getScale())
                + resultScale;
        break;
      default:
        throw new RuntimeException("Needs support");
    }
    return adjustScaleIfNeeded(resultPrecision, resultScale);
  }

  private static Decimal adjustScaleIfNeeded(int precision, int scale) {
    if (precision > MAX_PRECISION) {
      int minScale = Math.min(scale, MIN_ADJUSTED_SCALE);
      int delta = precision - MAX_PRECISION;
      precision = MAX_PRECISION;
      scale = Math.max(scale - delta, minScale);
    }
    return new Decimal(precision, scale, 128);
  }
}
