/*
  This file is part of TALER
  (C) 2025 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU Lesser General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file amount_quantity.c
 * @brief Parsing quantities and other decimal fractions
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_db_lib.h>
#include <taler/taler_json_lib.h>
#include "taler/taler_merchant_util.h"


/**
 * Multiply two 64-bit values and store result as high/low 64-bit parts.
 */
static void
mul64_overflow (uint64_t a,
                uint64_t b,
                uint64_t *hi,
                uint64_t *lo)
{
  uint64_t a_lo = a & 0xFFFFFFFF;
  uint64_t a_hi = a >> 32;
  uint64_t b_lo = b & 0xFFFFFFFF;
  uint64_t b_hi = b >> 32;

  uint64_t p0 = a_lo * b_lo;
  uint64_t p1 = a_lo * b_hi;
  uint64_t p2 = a_hi * b_lo;
  uint64_t p3 = a_hi * b_hi;

  uint64_t carry = ((p0 >> 32) + (p1 & 0xFFFFFFFF) + (p2 & 0xFFFFFFFF)) >> 32;

  *lo = p0 + (p1 << 32) + (p2 << 32);
  *hi = p3 + (p1 >> 32) + (p2 >> 32) + carry;
}


/**
 * Add two 128-bit numbers represented as hi/lo pairs.
 * Returns 1 on overflow, 0 otherwise.
 */
static int
add128 (uint64_t a_hi,
        uint64_t a_lo,
        uint64_t b_hi,
        uint64_t b_lo,
        uint64_t *r_hi,
        uint64_t *r_lo)
{
  uint64_t carry;

  *r_lo = a_lo + b_lo;
  carry = (*r_lo < a_lo) ? 1 : 0;
  *r_hi = a_hi + b_hi + carry;

  return (*r_hi < a_hi) || ((*r_hi == a_hi) && carry && (b_hi == UINT64_MAX));
}


/**
 * Subtract two 128-bit numbers represented as hi/lo pairs.
 * Returns 1 on underflow, 0 otherwise.
 */
static int
sub128 (uint64_t a_hi,
        uint64_t a_lo,
        uint64_t b_hi,
        uint64_t b_lo,
        uint64_t *r_hi,
        uint64_t *r_lo)
{
  uint64_t carry;

  carry = (a_lo < b_lo) ? 1 : 0;
  *r_lo = a_lo - b_lo;
  *r_hi = a_hi - b_hi - carry;

  return (a_hi < b_hi) || ((a_hi == b_hi) && carry);
}


/**
 * Divide a 128-bit number by a 64-bit number.
 * Returns quotient in q_hi/q_lo and remainder in r.
 */
static void
div128_64 (uint64_t n_hi,
           uint64_t n_lo,
           uint64_t d,
           uint64_t *q_hi,
           uint64_t *q_lo,
           uint64_t *r)
{
  uint64_t remainder;

  if (0 == n_hi)
  {
    *q_hi = 0;
    *q_lo = n_lo / d;
    *r = n_lo % d;
    return;
  }

  /* Note: very slow method, could be done faster, but
     in practice we expect the above short-cut to apply
     in virtually all cases, so we keep it simple here;
     also, if it mattered, we should use __uint128_t on
     systems that support it. */
  remainder = 0;
  *q_hi = 0;
  *q_lo = 0;
  for (int i = 127; i >= 0; i--)
  {
    remainder <<= 1;
    if (i >= 64)
      remainder |= (n_hi >> (i - 64)) & 1;
    else
      remainder |= (n_lo >> i) & 1;

    if (remainder >= d)
    {
      remainder -= d;
      if (i >= 64)
        *q_hi |= (1ULL << (i - 64));
      else
        *q_lo |= (1ULL << i);
    }
  }
  *r = remainder;
}


enum GNUNET_GenericReturnValue
TALER_MERCHANT_amount_multiply_by_quantity (
  struct TALER_Amount *result,
  const struct TALER_Amount *unit_price,
  const struct TALER_MERCHANT_ProductQuantity *factor,
  enum TALER_MERCHANT_RoundMode rm,
  const struct TALER_Amount *atomic_amount)
{
  uint64_t price_hi;
  uint64_t price_lo;
  uint64_t factor_hi;
  uint64_t factor_lo;
  uint64_t prod_hi;
  uint64_t prod_lo;
  uint64_t raw_hi;
  uint64_t raw_lo;
  uint64_t rem;
  uint64_t atomic_hi;
  uint64_t atomic_lo;
  uint64_t rounded_hi;
  uint64_t rounded_lo;
  uint64_t remainder;

  if (GNUNET_OK !=
      TALER_amount_cmp_currency (unit_price,
                                 atomic_amount))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  GNUNET_assert (factor->fractional < TALER_MERCHANT_UNIT_FRAC_BASE);
  GNUNET_assert (unit_price->fraction < TALER_AMOUNT_FRAC_BASE);
  GNUNET_assert (atomic_amount->fraction < TALER_AMOUNT_FRAC_BASE);
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (unit_price->currency,
                                        result));

  if (TALER_amount_is_zero (atomic_amount))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }

  /* Convert unit_price to fractional units */
  mul64_overflow (unit_price->value,
                  TALER_AMOUNT_FRAC_BASE,
                  &price_hi,
                  &price_lo);
  if (add128 (price_hi,
              price_lo,
              0,
              unit_price->fraction,
              &price_hi,
              &price_lo))
    return GNUNET_NO;

  /* Convert factor to fractional units */
  mul64_overflow (factor->integer,
                  TALER_MERCHANT_UNIT_FRAC_BASE,
                  &factor_hi,
                  &factor_lo);
  if (add128 (factor_hi,
              factor_lo,
              0,
              factor->fractional,
              &factor_hi,
              &factor_lo))
    return GNUNET_NO;

  /* Multiply price by factor: (price_hi:price_lo) * (factor_hi:factor_lo) */
  {
    uint64_t p0_hi, p0_lo, p1_hi, p1_lo, p2_hi, p2_lo, p3_hi, p3_lo;

    mul64_overflow (price_lo,
                    factor_lo,
                    &p0_hi,
                    &p0_lo);
    mul64_overflow (price_lo,
                    factor_hi,
                    &p1_hi,
                    &p1_lo);
    mul64_overflow (price_hi,
                    factor_lo,
                    &p2_hi,
                    &p2_lo);
    mul64_overflow (price_hi,
                    factor_hi,
                    &p3_hi,
                    &p3_lo);
    /* Check for overflow in 128-bit result */
    if ( (0 != p3_hi) ||
         (0 != p3_lo) ||
         (0 != p2_hi) ||
         (0 != p1_hi) )
      return GNUNET_NO;

    /* Add all fractions together */
    prod_hi = p0_hi;
    prod_lo = p0_lo;
    if (add128 (prod_hi,
                prod_lo,
                p1_lo,
                0,
                &prod_hi,
                &prod_lo))
      return GNUNET_NO;
    if (add128 (prod_hi,
                prod_lo,
                p2_lo,
                0,
                &prod_hi,
                &prod_lo))
      return GNUNET_NO;
  }

  /* Divide by MERCHANT_UNIT_FRAC_BASE */
  div128_64 (prod_hi,
             prod_lo,
             TALER_MERCHANT_UNIT_FRAC_BASE,
             &raw_hi,
             &raw_lo,
             &rem);

  /* Convert atomic_amount to fractional units */
  mul64_overflow (atomic_amount->value,
                  TALER_AMOUNT_FRAC_BASE,
                  &atomic_hi,
                  &atomic_lo);
  if (add128 (atomic_hi,
              atomic_lo,
              0,
              atomic_amount->fraction,
              &atomic_hi,
              &atomic_lo))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (atomic_hi > 0)
  {
    /* outside of supported range */
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }

  /* Compute remainder when dividing by atomic_amount and round down */
  {
    uint64_t q_hi, q_lo;
    uint64_t half_atomic = atomic_lo >> 1;
    bool round_up = false;

    div128_64 (raw_hi,
               raw_lo,
               atomic_lo,
               &q_hi,
               &q_lo,
               &remainder);
    sub128 (raw_hi,
            raw_lo,
            0,
            remainder,
            &rounded_hi,
            &rounded_lo);
    switch (rm)
    {
    case TALER_MERCHANT_ROUND_NEAREST:
      round_up = (remainder > half_atomic) ||
                 (remainder == half_atomic && (q_lo & 1));
      break;
    case TALER_MERCHANT_ROUND_UP:
      round_up = (remainder > 0);
      break;
    case TALER_MERCHANT_ROUND_DOWN:
      break;
    }
    if ( (round_up) &&
         (add128 (rounded_hi,
                  rounded_lo,
                  atomic_hi,
                  atomic_lo,
                  &rounded_hi,
                  &rounded_lo)) )
      return GNUNET_NO;
  }

  /* Convert back to value and fraction */
  {
    uint64_t final_value;
    uint64_t final_fraction;
    uint64_t q_hi;

    div128_64 (rounded_hi,
               rounded_lo,
               TALER_AMOUNT_FRAC_BASE,
               &q_hi,
               &final_value,
               &final_fraction);

    /* Check for overflow */
    if (0 != q_hi)
      return GNUNET_NO;

    result->value = final_value;
    result->fraction = (uint32_t) final_fraction;
  }
  return GNUNET_OK;
}
