/**
 * Copyright (C) 2023-2026 Linaro Limited (or its affiliates). All rights reserved.
 * Copyright (C) 2016-2023 Arm Limited (or its affiliates).
 * All rights reserved.
 *
 * This library 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 2.1 of the License, or (at your option) any later version.
 */

#include "internstringcache.h"

#include <QCoreApplication>
#include <QMutex>
#include <QDebug>

// If set to '1', print out how many strings were cached and fetched from the cache
// when each InternStringCache instance is destroyed.
#define REPORT_IMPLICIT_SHARING_STATS 0

#if !defined (Q_OS_MACOS) && !defined (Q_OS_WIN)
    // At time of writing, thread-local storage not supported on this platform.
    #define HAVE_THREAD_LOCAL_STORAGE
#endif

namespace
{
#ifndef HAVE_THREAD_LOCAL_STORAGE
    // Use a single global stack protected by a mutex.
    Q_GLOBAL_STATIC(QMutex                    , sInternStringCacheMutex);
    Q_GLOBAL_STATIC(QStack<InternStringCache*>, sInternStringCacheStack);

#else

    //! Returns the stack of all InternStringCache instantiated on this thread
    static QStack<InternStringCache*>& getThreadLocalStack()
    {
        // NB: Use thread_local here as __thread does not support types with non-trivial
        // constructors/destructors (__thread is still preferred with POD types as
        // thread_local does incur a small runtime overhead).
        static thread_local std::unique_ptr<QStack<InternStringCache*>> sHelpers;
        if (!sHelpers)
            sHelpers.reset(new QStack<InternStringCache*>());
        return *sHelpers;
    }

    //! Returns the most recently allocated InternStringCache on this thread
    static InternStringCache* getTopThreadLocalInstance()
    {
        const QStack<InternStringCache*>& helpers = getThreadLocalStack();
        if (helpers.isEmpty())
            return nullptr;
        else
            return helpers.top();
    }
#endif
}

int InternStringCache::sNumNoHelper = 0;

InternStringCache::InternStringCache()
    : mNumAdded(0), mNumFetchedFromCache(0)
{
#ifdef HAVE_THREAD_LOCAL_STORAGE
    getThreadLocalStack().push(this);
    #if REPORT_IMPLICIT_SHARING_STATS
        qDebug() << qApp->applicationFilePath() << ": Using implicitly shared string cache using thread-local storage";
    #endif
#else
    QMutexLocker locker(sInternStringCacheMutex());
    sInternStringCacheStack()->push(this);
    #if REPORT_IMPLICIT_SHARING_STATS
        qDebug() << qApp->applicationFilePath() << ": Using implicitly shared string cache using global storage";
    #endif
#endif
}

InternStringCache::~InternStringCache()
{
#ifdef HAVE_THREAD_LOCAL_STORAGE
    QStack<InternStringCache*>& helpers = getThreadLocalStack();
    Q_ASSERT(helpers.contains(this));
    helpers.remove(helpers.indexOf(this));
    Q_ASSERT(!getThreadLocalStack().contains(this));
#else
    QMutexLocker locker(sInternStringCacheMutex());
    QStack<InternStringCache*>& helpers = *sInternStringCacheStack();
    Q_ASSERT(helpers.contains(this));
    helpers.remove(helpers.indexOf(this));
    Q_ASSERT(!sInternStringCacheStack()->contains(this));
#endif

#if REPORT_IMPLICIT_SHARING_STATS
    qDebug() << qApp->applicationFilePath() << ": Implicitly shared cache results (numeric overflow possible):"
             << "\n    Num cached strings                          :" << mNumAdded
             << "\n    Num cache fetches                           :" << mNumFetchedFromCache
             << "\n    Num calls with no InternStringCache instance:" << sNumNoHelper;
#endif
}

//! Interns the string \c str
/*! Returns a canonical representation of the \c str object. If the string
    \c str already exists in the cache the returned string
    will implicitly share the cached string's value... at least until
    it is altered. */
QString InternStringCache::intern(const QString& str)
{
#ifdef HAVE_THREAD_LOCAL_STORAGE
    InternStringCache* helper = getTopThreadLocalInstance();
#else
    QMutexLocker locker(sInternStringCacheMutex());
    InternStringCache* helper = sInternStringCacheStack()->isEmpty()? nullptr : sInternStringCacheStack()->top();
#endif

    if (helper)
    {
        QSet<QString>::const_iterator iter = helper->mStrings.constFind(str);
        if (iter == helper->mStrings.constEnd())
        {
            helper->mStrings << str;
            helper->mNumAdded++;
            return str;
        }
        else
        {
            helper->mNumFetchedFromCache++;
            return *iter;
        }
    }
    sNumNoHelper++;
    return str;
}

//! Returns \c true if an InternStringCache already exists on this thread
/*! This can be used to avoid creating additional cache instances when one
 *  already exists. The lifetime of caches should not be a critical concern --
 *  interned strings remain valid when the originating InternStringCache
 *  is destroyed as each QString does its own internal reference counting.
 *
 *  In general it will preferable to re-use an existing cache than starting
 *  a new one, unless you have context-specific knowledge about the pattern
 *  of duplicate string requests likely to be received. */
bool InternStringCache::hasCache()
{
#ifdef HAVE_THREAD_LOCAL_STORAGE
    InternStringCache* helper = getTopThreadLocalInstance();
#else
    QMutexLocker locker(sInternStringCacheMutex());
    InternStringCache* helper = sInternStringCacheStack()->isEmpty()? nullptr : sInternStringCacheStack()->top();
#endif
    return helper != nullptr;
}

//! Returns \c true if this string is in this cache
/*! If \c true then requests for this string via intern() will return a
 *  reference-counted copy of that string. */
bool InternStringCache::isCached(const QString& str) const
{
    return mStrings.contains(str);
}
