// Copyright (C) 2026 Kiyotsugu Arai
// SPDX-License-Identifier: LGPL-3.0-or-later

// IntMultiplication.hpp
// Fast multiplication algorithms (Karatsuba, Toom-Cook, FFT)

#pragma once

#include <math/core/mp/Int/IntBase.hpp>
#include <math/core/mp/Int/IntDivision.hpp>
#include <math/core/mp/Int/MpnOps.hpp>
#include <math/core/mp/Int/ScratchArena.hpp>
#include <math/fft/fft.hpp>
#include <algorithm>
#include <vector>
#include <cmath>

namespace sangi {

// Multiplication-algorithm thresholds (in words)
// Tuned according to benchmark results (2026-02):
//   - Basecase → Karatsuba: 44% faster at 128 words
//   - Karatsuba → Toom-Cook: 15% faster at 256 words
//   - FFT (double-based): slower than Toom-Cook at every size (1.5x-2.8x)
//     → Will be re-evaluated once an NTT implementation lands. Currently unused in operator*.
namespace MultiplicationThresholds {
    constexpr size_t KARATSUBA_THRESHOLD = 128;  // 128 words (8K-bit) or larger uses Karatsuba
    constexpr size_t TOOMCOOK_THRESHOLD = 256;   // 256 words (16K-bit) or larger uses Toom-Cook-3
    constexpr size_t TOOMCOOK4_THRESHOLD = 640;  // 640 words (40K-bit) or larger uses Toom-Cook-4
    constexpr size_t FFT_THRESHOLD = 256;        // 256 words — currently unused (FFT is slower than Toom-Cook)
}

class IntMultiplication {
public:
    // Karatsuba multiplication (mpn-based)
    // Complexity: O(n^1.585) where n = max(X.words, Y.words)
    // Operates on raw limb pointers internally; Int is materialized only at entry and exit
    static Int karatsubaMultiply(const Int& X, const Int& Y) {
        int result_sign = X.getSign() * Y.getSign();
        if (result_sign == 0) return Int(0);
        if (X.isSpecialState() || Y.isSpecialState()) {
            return basecaseMultiply(X, Y);
        }

        const uint64_t* a = X.m_words.data();
        size_t an = X.m_words.size();
        const uint64_t* b = Y.m_words.data();
        size_t bn = Y.m_words.size();

        // For small inputs use basecase (mpn-based)
        if (an < MultiplicationThresholds::KARATSUBA_THRESHOLD ||
            bn < MultiplicationThresholds::KARATSUBA_THRESHOLD) {
            return basecaseMultiply(X, Y);
        }

        // Allocate the result buffer and scratch from the arena
        ScratchScope scope;
        size_t rn = an + bn;
        uint64_t* r = getThreadArena().alloc_limbs(rn);
        size_t scratch_size = mpn::mul_karatsuba_scratch_size(std::max(an, bn));
        uint64_t* scratch = getThreadArena().alloc_limbs(scratch_size);

        // Run mpn Karatsuba
        mpn::mul_karatsuba(r, a, an, b, bn, scratch);

        // Convert the result to Int (only one copy happens here)
        size_t actual_rn = mpn::normalized_size(r, rn);
        if (actual_rn == 0) return Int(0);

        return Int::fromRawWords(std::span<const uint64_t>(r, actual_rn), result_sign);
    }

    // Toom-Cook-3 multiplication (mpn-based)
    // Complexity: O(n^1.465) where n = max(X.words, Y.words)
    // Evaluation points {0, 1, -1, 2, ∞} (GMP convention)
    // Operates on raw limb pointers internally; Int is materialized only at entry and exit
    static Int toomCook3Multiply(const Int& X, const Int& Y) {
        int result_sign = X.getSign() * Y.getSign();
        if (result_sign == 0) return Int(0);
        if (X.isSpecialState() || Y.isSpecialState()) {
            return basecaseMultiply(X, Y);
        }

        const uint64_t* a = X.m_words.data();
        size_t an = X.m_words.size();
        const uint64_t* b = Y.m_words.data();
        size_t bn = Y.m_words.size();

        if (an < MultiplicationThresholds::TOOMCOOK_THRESHOLD ||
            bn < MultiplicationThresholds::TOOMCOOK_THRESHOLD) {
            return karatsubaMultiply(X, Y);
        }

        // Allocate the result buffer and scratch from the arena
        ScratchScope scope;
        size_t rn = an + bn;
        uint64_t* r = getThreadArena().alloc_limbs(rn);
        size_t scratch_size = mpn::mul_toomcook3_scratch_size(std::max(an, bn));
        uint64_t* scratch = getThreadArena().alloc_limbs(scratch_size);

        // Run mpn Toom-Cook-3
        mpn::mul_toomcook3(r, a, an, b, bn, scratch);

        // Convert the result to Int
        size_t actual_rn = mpn::normalized_size(r, rn);
        if (actual_rn == 0) return Int(0);

        return Int::fromRawWords(std::span<const uint64_t>(r, actual_rn), result_sign);
    }

    // Toom-Cook-4 multiplication (mpn-based)
    // Complexity: O(n^1.404) where n = max(X.words, Y.words)
    // Evaluation points {0, 1, -1, 2, -2, 3, ∞}
    // Operates on raw limb pointers internally; Int is materialized only at entry and exit
    static Int toomCook4Multiply(const Int& X, const Int& Y) {
        int result_sign = X.getSign() * Y.getSign();
        if (result_sign == 0) return Int(0);
        if (X.isSpecialState() || Y.isSpecialState()) {
            return basecaseMultiply(X, Y);
        }

        const uint64_t* a = X.m_words.data();
        size_t an = X.m_words.size();
        const uint64_t* b = Y.m_words.data();
        size_t bn = Y.m_words.size();

        if (an < MultiplicationThresholds::TOOMCOOK4_THRESHOLD ||
            bn < MultiplicationThresholds::TOOMCOOK4_THRESHOLD) {
            return toomCook3Multiply(X, Y);
        }

        // Allocate the result buffer and scratch from the arena
        ScratchScope scope;
        size_t rn = an + bn;
        uint64_t* r = getThreadArena().alloc_limbs(rn);
        size_t scratch_size = mpn::mul_toomcook4_scratch_size(std::max(an, bn));
        uint64_t* scratch = getThreadArena().alloc_limbs(scratch_size);

        // Run mpn Toom-Cook-4
        mpn::mul_toomcook4(r, a, an, b, bn, scratch);

        // Convert the result to Int
        size_t actual_rn = mpn::normalized_size(r, rn);
        if (actual_rn == 0) return Int(0);

        return Int::fromRawWords(std::span<const uint64_t>(r, actual_rn), result_sign);
    }

    // General-purpose multiplication (handles unbalanced inputs)
    // Selects the algorithm automatically based on size, and uses a chunked
    // strategy when the size ratio is large.
    static Int multiply(const Int& X, const Int& Y) {
        int result_sign = X.getSign() * Y.getSign();
        if (result_sign == 0) return Int(0);
        if (X.isSpecialState() || Y.isSpecialState()) {
            return basecaseMultiply(X, Y);
        }

        const uint64_t* a = X.m_words.data();
        size_t an = X.m_words.size();
        const uint64_t* b = Y.m_words.data();
        size_t bn = Y.m_words.size();

        ScratchScope scope;
        size_t rn = an + bn;
        uint64_t* r = getThreadArena().alloc_limbs(rn);
        size_t scratch_size = mpn::multiply_scratch_size(an, bn);
        uint64_t* scratch = (scratch_size > 0)
            ? getThreadArena().alloc_limbs(scratch_size) : nullptr;

        mpn::multiply(r, a, an, b, bn, scratch);

        size_t actual_rn = mpn::normalized_size(r, rn);
        if (actual_rn == 0) return Int(0);

        return Int::fromRawWords(std::span<const uint64_t>(r, actual_rn), result_sign);
    }

    // Compute only the top rn words of the product (short multiplication)
    // Uses mulhigh_basecase only when the size is at or below basecase.
    // Otherwise falls back to the full product followed by extracting the top words.
    static Int multiplyHigh(const Int& X, const Int& Y, size_t rn) {
        int result_sign = X.getSign() * Y.getSign();
        if (result_sign == 0) return Int(0);

        const uint64_t* a = X.m_words.data();
        size_t an = X.m_words.size();
        const uint64_t* b = Y.m_words.data();
        size_t bn = Y.m_words.size();

        size_t total = an + bn;
        if (rn >= total) {
            return multiply(X, Y);  // The full product suffices
        }

        ScratchScope scope;
        uint64_t* rp = getThreadArena().alloc_limbs(rn);

        if (std::min(an, bn) < mpn::KARATSUBA_THRESHOLD) {
            // Basecase: skip the low words via mulhigh
            mpn::mulhigh_basecase(rp, a, an, b, bn, rn);
        } else {
            // Karatsuba or larger: compute the full product and extract the top portion
            size_t scratch_size = mpn::multiply_scratch_size(an, bn);
            uint64_t* full = getThreadArena().alloc_limbs(total);
            uint64_t* scratch = (scratch_size > 0)
                ? getThreadArena().alloc_limbs(scratch_size) : nullptr;
            mpn::multiply(full, a, an, b, bn, scratch);
            std::memcpy(rp, full + (total - rn), rn * sizeof(uint64_t));
        }

        size_t actual_rn = mpn::normalized_size(rp, rn);
        if (actual_rn == 0) return Int(0);
        return Int::fromRawWords(std::span<const uint64_t>(rp, actual_rn), result_sign);
    }

    // FFT multiplication
    // Complexity: O(n log n) where n = FFT size
    // Precision limit: N < 2^17 points (about 630k digits) due to the 53-bit double mantissa
    static Int fftMultiply(const Int& X, const Int& Y) {
        // Save the sign and compute on the absolute values
        int result_sign = X.getSign() * Y.getSign();

        // Take the absolute values
        Int absX = (X.getSign() < 0) ? -X : X;
        Int absY = (Y.getSign() < 0) ? -Y : Y;

        size_t Nx = absX.words().size();
        size_t Ny = absY.words().size();

        // Small numbers use Toom-Cook
        if (Nx < MultiplicationThresholds::FFT_THRESHOLD ||
            Ny < MultiplicationThresholds::FFT_THRESHOLD) {
            Int result = toomCook3Multiply(absX, absY);
            if (result_sign < 0) result = -result;
            return result;
        }

        // Decompose the Int's 64-bit words into 16-bit words
        // (Use a smaller word size to preserve FFT precision)
        constexpr size_t WORD_BITS = 16;
        constexpr size_t WORDS_PER_U64 = 64 / WORD_BITS;  // = 4

        size_t x_u16_words = Nx * WORDS_PER_U64;
        size_t y_u16_words = Ny * WORDS_PER_U64;

        // Determine the FFT size (next power of two)
        size_t result_u16_words = x_u16_words + y_u16_words;
        int nfft = 1;
        while (static_cast<size_t>(nfft) < result_u16_words) {
            nfft *= 2;
        }

        // Precision-limit check (N <= 2^16)
        if (nfft > (1 << 16)) {
            // Fall back to Toom-Cook when the FFT precision limit is exceeded
            Int result = toomCook3Multiply(absX, absY);
            if (result_sign < 0) result = -result;
            return result;
        }

        // Initialize the FFT
        FFTEngine<double> fft(nfft, FFTDivMode::Inverse);

        // Convert X to a 16-bit word array
        std::vector<double> x_data(nfft, 0.0);
        auto x_words = absX.words();
        for (size_t i = 0; i < Nx; ++i) {
            uint64_t word = x_words[i];
            for (size_t j = 0; j < WORDS_PER_U64; ++j) {
                x_data[i * WORDS_PER_U64 + j] = static_cast<double>(word & 0xFFFF);
                word >>= WORD_BITS;
            }
        }

        // Convert Y to a 16-bit word array
        std::vector<double> y_data(nfft, 0.0);
        auto y_words = absY.words();
        for (size_t i = 0; i < Ny; ++i) {
            uint64_t word = y_words[i];
            for (size_t j = 0; j < WORDS_PER_U64; ++j) {
                y_data[i * WORDS_PER_U64 + j] = static_cast<double>(word & 0xFFFF);
                word >>= WORD_BITS;
            }
        }

        // Real-FFT forward transform
        auto X_freq = fft.real_transform(x_data);
        auto Y_freq = fft.real_transform(y_data);

        // Pointwise multiplication in the frequency domain
        std::vector<Complex<double>> Z_freq(nfft / 2 + 1);
        for (int i = 0; i <= nfft / 2; ++i) {
            Z_freq[i] = X_freq[i] * Y_freq[i];
        }

        // Real-FFT inverse transform
        auto z_data = fft.real_inverse(Z_freq);

        // Convert from 16-bit words to 64-bit words (with carry handling)
        std::vector<uint64_t> result_words;
        result_words.reserve((nfft / WORDS_PER_U64) + 1);

        uint64_t carry = 0;
        uint64_t current_word = 0;
        size_t shift = 0;

        for (int i = 0; i < nfft; ++i) {
            // Round to the nearest integer
            uint64_t val = static_cast<uint64_t>(z_data[i] + 0.5) + carry;

            // Process as a 16-bit word
            uint64_t low16 = val & 0xFFFF;
            carry = val >> WORD_BITS;

            // Pack into the 64-bit word
            current_word |= (low16 << shift);
            shift += WORD_BITS;

            if (shift == 64) {
                result_words.push_back(current_word);
                current_word = 0;
                shift = 0;
            }
        }

        // Handle the final carry
        if (shift > 0) {
            current_word |= (carry << shift);
            result_words.push_back(current_word);
            // Handle the case where the high bits of carry overflow by shift
            if (shift < 64) {
                uint64_t remaining = carry >> (64 - shift);
                if (remaining > 0) {
                    result_words.push_back(remaining);
                }
            }
        } else if (carry > 0) {
            result_words.push_back(carry);
        }

        // Strip leading zeros
        while (!result_words.empty() && result_words.back() == 0) {
            result_words.pop_back();
        }

        if (result_words.empty()) {
            return Int(0);
        }

        Int result = Int::fromRawWords(result_words, result_sign);
        return result;
    }

    // Basic multiplication (O(n^2) long multiplication, mpn-based)
    // Used for small inputs or those below the Karatsuba threshold
    static Int basecaseMultiply(const Int& X, const Int& Y) {
        int result_sign = X.getSign() * Y.getSign();
        if (result_sign == 0) return Int(0);

        const uint64_t* a = X.m_words.data();
        size_t an = X.m_words.size();
        const uint64_t* b = Y.m_words.data();
        size_t bn = Y.m_words.size();

        if (an == 0 || bn == 0) return Int(0);

        std::vector<uint64_t> product(an + bn);
        mpn::mul_basecase(product.data(), a, an, b, bn);

        size_t actual = mpn::normalized_size(product.data(), product.size());
        if (actual == 0) return Int(0);
        product.resize(actual);

        return Int::fromRawWords(product, result_sign);
    }

};

} // namespace sangi
