//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/Particle/FFMemo.cpp
//! @brief     Implements class FFMemo.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2026
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Sample/Particle/FFMemo.h"
#include "Base/Vector/WavevectorInfo.h"
#include "Sample/Particle/Formfactor.h"
#include <algorithm>
#include <atomic>
#include <bit>
#include <cstdint>
#include <cstring>
#include <unordered_map>

namespace {

//! Mints process-unique ids for FFMemo instances, which key the per-thread tables.
//! The address would be ambiguous: in a fit loop, a new memo may be allocated at the
//! address of a deleted one and would inherit its table — with the same q grid, that
//! would silently yield form factors of the previous iteration. Ids are never reused.
//! Atomic, since memo construction is not bound to one thread by contract.
std::atomic<size_t> id_counter{0};

//! Spreads any single-bit difference of the input over all 64 output bits ("avalanche").
//! This is the finalizer fmix64 of MurmurHash3, copied verbatim from the public-domain
//! reference (github.com/aappleby/smhasher): verify against it, do not modify.
uint64_t fmix64(double d)
{
    uint64_t u;
    std::memcpy(&u, &d, sizeof u);
    u ^= u >> 33;
    u *= 0xff51afd7ed558ccdULL;
    u ^= u >> 33;
    u *= 0xc4ceb9fe1a85ec53ULL;
    u ^= u >> 33;
    return u;
}

//! Hash of q, indexing the memo tables. Each component is avalanched before entering the
//! combining chain, because the q vectors of DWBA channel pairs differ only in the sign
//! of qz, and sign flips of two components cancel exactly in a plain xor-multiply chain.
//! No standard hash is usable here: the mixing quality of std::hash is unspecified, and
//! some implementations exhibit exactly this cancellation. Once the project requires
//! Boost >= 1.81, fmix64 and this chain can be replaced by boost::hash_combine.
size_t hashQ(const C3& q)
{
    uint64_t h = 0xcbf29ce484222325ULL;
    for (double d :
         {q.x().real(), q.x().imag(), q.y().real(), q.y().imag(), q.z().real(), q.z().imag()})
        h = (h ^ ::fmix64(d)) * 0x100000001b3ULL;
    return static_cast<size_t>(h ^ (h >> 33));
}

} // namespace


//! @param working_set Number of wavevector keys the table must hold at a time (estimated
//! by the callers). Factor 8 is collision headroom: losses scale as 1/capacity, so more
//! headroom buys ever less, while memory and per-resampling initialization grow linearly.
//! The cap bounds memory in many-thread fits; beyond it the cache degrades gracefully.
FFMemo::FFMemo(size_t working_set)
    : m_capacity(std::bit_ceil(std::clamp<size_t>(8 * working_set, 128, 4096)))
    , m_id(++::id_counter)
{
}

FFMemo::~FFMemo() = default;

complex_t FFMemo::theFF(const WavevectorInfo& wv2, const Formfactor& ff) const
{
    const C3 q = wv2.getQ();
    Table::Entry& entry = myTable().slots[::hashQ(q) & (m_capacity - 1)];
    if (entry.used && entry.q == q)
        return entry.value;
    entry.value = ff.theFF(wv2); // entry is updated only if the evaluation does not throw
    entry.q = q;
    entry.used = true;
    return entry.value;
}

FFMemo::Table& FFMemo::myTable() const
{
    static thread_local std::unordered_map<size_t, Table> registry;
    // Bound the registry of long-lived threads (e.g. the main thread across fit iterations).
    // Design limit: with more than 64 memos alive at once, this purge would thrash.
    if (registry.size() > 64 && !registry.contains(m_id))
        registry.clear();
    Table& table = registry[m_id];
    if (table.slots.empty()) // first use of this memo by this thread
        table.slots.resize(m_capacity);
    return table;
}
