/* 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.
 */

#define MP_APR_POOL_NEW "APR::Pool::new"

typedef struct {
    SV *sv;
#ifdef USE_ITHREADS
    PerlInterpreter *perl;
    modperl_interp_t *interp;
#endif
} mpxs_pool_account_t;

/* XXX: this implementation has a problem with perl ithreads. if a
 * custom pool is allocated, and then a thread is spawned we now have
 * two copies of the pool object, each living in a different perl
 * interpreter, both pointing to the same memory address of the apr
 * pool.
 *
 * need to write a CLONE class method could properly clone the
 * thread's copied object, but it's tricky:
 * - it needs to call parent_get() on the copied object and allocate a
 *   new pool from that parent's pool
 * - it needs to reinstall any registered cleanup callbacks (can we do
 *   that?) may be we can skip those?
 */

#ifndef MP_SOURCE_SCAN
#ifdef USE_ITHREADS
#include "apr_optional.h"
APR_OPTIONAL_FN_TYPE(modperl_interp_unselect) *modperl_opt_interp_unselect;
APR_OPTIONAL_FN_TYPE(modperl_thx_interp_get) *modperl_opt_thx_interp_get;
#endif
#endif

#define MP_APR_POOL_SV_HAS_OWNERSHIP(sv) mpxs_pool_is_custom(sv)

/* before the magic is freed, one needs to carefully detach the
 * dependant pool magic added by mpxs_add_pool_magic (most of the time
 * it'd be a parent pool), and postpone its destruction, until after
 * the child pool is destroyed. Since if we don't do that the
 * destruction of the parent pool will destroy the child pool C guts
 * and when perl unware of that the rug was pulled under the feet will
 * continue destructing the child pool, things will crash
 */
#define MP_APR_POOL_SV_DROPS_OWNERSHIP_RUN(acct) STMT_START {       \
    MAGIC *mg = mg_find(acct->sv, PERL_MAGIC_ext);                  \
    if (mg && mg->mg_obj) {                                         \
        sv_2mortal(mg->mg_obj);                                     \
        mg->mg_obj = Nullsv;                                        \
        mg->mg_flags &= ~MGf_REFCOUNTED;                            \
    }                                                               \
    mg_free(acct->sv);                                              \
    SvIVX(acct->sv) = 0;                                            \
} STMT_END

#ifdef USE_ITHREADS

#define MP_APR_POOL_SV_DROPS_OWNERSHIP(acct) STMT_START {               \
    dTHXa(acct->perl);                                                  \
    MP_APR_POOL_SV_DROPS_OWNERSHIP_RUN(acct);                           \
    if (modperl_opt_interp_unselect && acct->interp) {                  \
        /* this will decrement the interp refcnt until                  \
         * there are no more references, in which case                  \
         * the interpreter will be putback into the mip                 \
         */                                                             \
        (void)modperl_opt_interp_unselect(acct->interp);                \
    }                                                                   \
} STMT_END

#define MP_APR_POOL_SV_TAKES_OWNERSHIP(acct_sv, pool) STMT_START {      \
    mpxs_pool_account_t *acct = apr_palloc(pool, sizeof *acct);         \
    acct->sv = acct_sv;                                                 \
    acct->perl = aTHX;                                                  \
    SvIVX(acct_sv) = PTR2IV(pool);                                      \
                                                                        \
    sv_magic(acct_sv, Nullsv, PERL_MAGIC_ext,                           \
             MP_APR_POOL_NEW, sizeof(MP_APR_POOL_NEW));                 \
                                                                        \
    apr_pool_cleanup_register(pool, (void *)acct,                       \
                              mpxs_apr_pool_cleanup,                    \
                              apr_pool_cleanup_null);                   \
                                                                        \
    /* make sure interpreter is not putback into the mip                \
     * until this cleanup has run.                                      \
     */                                                                 \
    if (modperl_opt_thx_interp_get) {                                   \
        if ((acct->interp = modperl_opt_thx_interp_get(aTHX))) {        \
            acct->interp->refcnt++;                                     \
        }                                                               \
    }                                                                   \
} STMT_END

#else /* !USE_ITHREADS */

#define MP_APR_POOL_SV_DROPS_OWNERSHIP MP_APR_POOL_SV_DROPS_OWNERSHIP_RUN

#define MP_APR_POOL_SV_TAKES_OWNERSHIP(acct_sv, pool) STMT_START {      \
    mpxs_pool_account_t *acct = apr_palloc(pool, sizeof *acct);         \
    acct->sv = acct_sv;                                                 \
    SvIVX(acct_sv) = PTR2IV(pool);                                      \
                                                                        \
    sv_magic(acct_sv, Nullsv, PERL_MAGIC_ext,                           \
              MP_APR_POOL_NEW, sizeof(MP_APR_POOL_NEW));                \
                                                                        \
    apr_pool_cleanup_register(pool, (void *)acct,                       \
                              mpxs_apr_pool_cleanup,                    \
                              apr_pool_cleanup_null);                   \
} STMT_END

#endif /* USE_ITHREADS */


/* XXX: should we make it a new global tracing category
 * MOD_PERL_TRACE=p for tracing pool management? */
#define MP_POOL_TRACE_DO 0

#if MP_POOL_TRACE_DO && defined(MP_TRACE)
#define MP_POOL_TRACE modperl_trace
#else
#define MP_POOL_TRACE if (0) modperl_trace
#endif

/* invalidate all Perl objects referencing the data sv stored in the
 * pool and the sv itself. this is needed when a parent pool triggers
 * apr_pool_destroy on its child pools
 */
static MP_INLINE apr_status_t
mpxs_apr_pool_cleanup(void *cleanup_data)
{
    mpxs_pool_account_t *acct = cleanup_data;
    MP_APR_POOL_SV_DROPS_OWNERSHIP(acct);
    return APR_SUCCESS;
}

/**
 * Create a new pool or subpool.
 * @param  parent_pool_obj   an APR::Pool object or an "APR::Pool" class
 * @return                   a new pool or subpool
 */
static MP_INLINE SV *mpxs_apr_pool_create(pTHX_ SV *parent_pool_obj)
{
    apr_pool_t *parent_pool = mpxs_sv_object_deref(parent_pool_obj, apr_pool_t);
    apr_pool_t *child_pool  = NULL;

    MP_POOL_TRACE(MP_FUNC, "parent pool 0x%l", (unsigned long)parent_pool);
    (void)apr_pool_create(&child_pool, parent_pool);

#if APR_POOL_DEBUG
    /* useful for pools debugging, can grep for APR::Pool::new */
    apr_pool_tag(child_pool, MP_APR_POOL_NEW);
#endif

    /* allocation corruption validation: I saw this happening when the
     * same pool was destroyed more than once, should be fixed now,
     * but still the check is not redundant */
    if (child_pool == parent_pool) {
        Perl_croak(aTHX_ "a newly allocated sub-pool 0x%lx "
                   "is the same as its parent 0x%lx, aborting",
                   (unsigned long)child_pool, (unsigned long)parent_pool);
    }

#if APR_POOL_DEBUG
    /* child <-> parent <-> ... <-> top ancestry traversal */
    {
        apr_pool_t *p = child_pool;
        apr_pool_t *pp;

        while ((pp = apr_pool_parent_get(p))) {
            MP_POOL_TRACE(MP_FUNC, "parent 0x%lx, child 0x%lx",
                    (unsigned long)pp, (unsigned long)p);

            if (apr_pool_is_ancestor(pp, p)) {
                MP_POOL_TRACE(MP_FUNC, "0x%lx is a subpool of 0x%lx",
                        (unsigned long)p, (unsigned long)pp);
            }
            p = pp;
        }
    }
#endif

    {
        SV *rv = sv_setref_pv(NEWSV(0, 0), "APR::Pool", (void*)child_pool);
        SV *sv = SvRV(rv);

        /* Each newly created pool must be destroyed only once. Calling
         * apr_pool_destroy will destroy the pool and its children pools,
         * however a perl object for a sub-pool will still keep a pointer
         * to the pool which was already destroyed. When this object is
         * DESTROYed, apr_pool_destroy will be called again. In the best
         * case it'll try to destroy a non-existing pool, but in the worst
         * case it'll destroy a different valid pool which has been given
         * the same memory allocation wrecking havoc. Therefore we must
         * ensure that when sub-pools are destroyed via the parent pool,
         * their cleanup callbacks will destroy the guts of their perl
         * objects, so when those perl objects, pointing to memory
         * previously allocated by destroyed sub-pools or re-used already
         * by new pools, will get their time to DESTROY, they won't make a
         * mess, trying to destroy an already destroyed pool or even worse
         * a pool allocate in the place of the old one.
         */

        MP_APR_POOL_SV_TAKES_OWNERSHIP(sv, child_pool);

        MP_POOL_TRACE(MP_FUNC, "sub-pool p: 0x%lx, sv: 0x%lx, rv: 0x%lx",
                      (unsigned long)child_pool, sv, rv);

        if (parent_pool) {
            mpxs_add_pool_magic(rv, parent_pool_obj);
        }

        return rv;
    }
}

static MP_INLINE void mpxs_APR__Pool_clear(pTHX_ SV *obj)
{
    apr_pool_t *p = mp_xs_sv2_APR__Pool(obj);
    SV *sv = SvRV(obj);

    if (!MP_APR_POOL_SV_HAS_OWNERSHIP(sv)) {
        MP_POOL_TRACE(MP_FUNC, "parent pool (0x%lx) is a core pool",
                      (unsigned long)p);
        apr_pool_clear(p);
        return;
    }

    MP_POOL_TRACE(MP_FUNC,
                  "parent pool (0x%lx) is a custom pool, sv 0x%lx",
                  (unsigned long)p,
                  (unsigned long)sv);

    apr_pool_clear(p);

    /* apr_pool_clear runs & removes the cleanup, so we need to restore
     * it. Since clear triggers mpxs_apr_pool_cleanup call, our
     * object's guts get nuked too, so we need to restore them too */

    MP_APR_POOL_SV_TAKES_OWNERSHIP(sv, p);
}


typedef struct {
    SV *cv;
    SV *arg;
    apr_pool_t *p;
#ifdef USE_ITHREADS
    PerlInterpreter *perl;
    modperl_interp_t *interp;
#endif
} mpxs_cleanup_t;

/**
 * callback wrapper for Perl cleanup subroutines
 * @param data   internal storage
 */


static apr_status_t mpxs_cleanup_run(void *data)
{
    int count;
    mpxs_cleanup_t *cdata = (mpxs_cleanup_t *)data;
#ifdef USE_ITHREADS
    dTHXa(cdata->perl);
#endif
    dSP;

    ENTER;SAVETMPS;
    PUSHMARK(SP);
    if (cdata->arg) {
        XPUSHs(cdata->arg);
    }
    PUTBACK;

    count = call_sv(cdata->cv, G_SCALAR|G_EVAL);

    SPAGAIN;

    if (count == 1) {
        (void)POPs; /* the return value is ignored */
    }

    PUTBACK;
    FREETMPS;LEAVE;

    SvREFCNT_dec(cdata->cv);
    if (cdata->arg) {
        SvREFCNT_dec(cdata->arg);
    }

#ifdef USE_ITHREADS
    if (cdata->interp && modperl_opt_interp_unselect) {
        /* this will decrement the interp refcnt until
         * there are no more references, in which case
         * the interpreter will be putback into the mip
         */
        (void)modperl_opt_interp_unselect(cdata->interp);
    }
#endif

    if (SvTRUE(ERRSV)) {
        Perl_croak(aTHX_ SvPV_nolen(ERRSV));
    }

    /* the return value is ignored by apr_pool_destroy anyway */
    return APR_SUCCESS;
}

/**
 * register cleanups to run
 * @param p      pool with which to associate the cleanup
 * @param cv     subroutine reference to run
 * @param arg    optional argument to pass to the subroutine
 */
static MP_INLINE void mpxs_apr_pool_cleanup_register(pTHX_ apr_pool_t *p,
                                                     SV *cv, SV *arg)
{
    mpxs_cleanup_t *data =
        (mpxs_cleanup_t *)apr_pcalloc(p, sizeof(*data));

    data->cv = SvREFCNT_inc(cv);
    data->arg = arg ? SvREFCNT_inc(arg) : Nullsv;
    data->p = p;
#ifdef USE_ITHREADS
    data->perl = aTHX;
    /* make sure interpreter is not putback into the mip
     * until this cleanup has run.
     */
    if (modperl_opt_thx_interp_get) {
        if ((data->interp = modperl_opt_thx_interp_get(data->perl))) {
            data->interp->refcnt++;
        }
    }
#endif

    apr_pool_cleanup_register(p, data,
                              mpxs_cleanup_run,
                              apr_pool_cleanup_null);
}


static MP_INLINE SV *
mpxs_apr_pool_parent_get(pTHX_ apr_pool_t *child_pool)
{
    apr_pool_t *parent_pool = apr_pool_parent_get(child_pool);

    if (parent_pool) {
        return SvREFCNT_inc(mp_xs_APR__Pool_2obj(parent_pool));
    }
    else {
        MP_POOL_TRACE(MP_FUNC, "pool (0x%lx) has no parents",
                      (unsigned long)child_pool);
        return &PL_sv_undef;
    }
}

/**
 * destroy a pool
 * @param obj    an APR::Pool object
 */
static MP_INLINE void mpxs_apr_pool_DESTROY(pTHX_ SV *obj)
{
    SV *sv = SvRV(obj);

    if (MP_APR_POOL_SV_HAS_OWNERSHIP(sv)) {
        apr_pool_t *p = mpxs_sv_object_deref(obj, apr_pool_t);
        apr_pool_destroy(p);
    }
}
