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

#include "mod_perl.h"

/*
 * XXX: this is not the most efficent interpreter pool implementation
 * but it will do for proof-of-concept
 */

#ifdef USE_ITHREADS

void modperl_interp_clone_init(modperl_interp_t *interp)
{
    dTHXa(interp->perl);

    MpInterpCLONED_On(interp);

    MP_ASSERT_CONTEXT(aTHX);

    /* clear @DynaLoader::dl_librefs so we only dlclose() those
     * which are opened by the clone
     */
    modperl_xs_dl_handles_clear(aTHX);
}

modperl_interp_t *modperl_interp_new(modperl_interp_pool_t *mip,
                                     PerlInterpreter *perl)
{
    UV clone_flags = CLONEf_KEEP_PTR_TABLE;
    modperl_interp_t *interp =
        (modperl_interp_t *)malloc(sizeof(*interp));

    memset(interp, '\0', sizeof(*interp));

    interp->mip = mip;
    interp->refcnt = 0;

    if (perl) {
#ifdef MP_USE_GTOP
        MP_dSCFG(mip->server);
        MP_TRACE_m_do(
            modperl_gtop_do_proc_mem_before(MP_FUNC, "perl_clone");
        );
#endif

#if defined(WIN32) && defined(CLONEf_CLONE_HOST)
        clone_flags |= CLONEf_CLONE_HOST;
#endif

        PERL_SET_CONTEXT(perl);

        interp->perl = perl_clone(perl, clone_flags);

        MP_ASSERT_CONTEXT(interp->perl);

        {
            PTR_TBL_t *source = modperl_module_config_table_get(perl, FALSE);
            if (source) {
                PTR_TBL_t *table = modperl_svptr_table_clone(interp->perl,
                                                             perl,
                                                             source);

                modperl_module_config_table_set(interp->perl, table);
            }
        }

        /*
         * we keep the PL_ptr_table past perl_clone so it can be used
         * within modperl_svptr_table_clone. Perl_sv_dup() uses it.
         * Don't confuse our svptr_table with Perl's ptr_table. They
         * are different things, although they use the same type.
         */
        if ((clone_flags & CLONEf_KEEP_PTR_TABLE)) {
            dTHXa(interp->perl);
            ptr_table_free(PL_ptr_table);
            PL_ptr_table = NULL;
        }

        modperl_interp_clone_init(interp);

        PERL_SET_CONTEXT(perl);

#ifdef MP_USE_GTOP
        MP_TRACE_m_do(
            modperl_gtop_do_proc_mem_after(MP_FUNC, "perl_clone");
        );
#endif
    }

    MP_TRACE_i(MP_FUNC, "0x%lx / perl: 0x%lx / parent perl: 0x%lx",
               (unsigned long)interp, (unsigned long)interp->perl,
               (unsigned long)perl);

    return interp;
}

void modperl_interp_destroy(modperl_interp_t *interp)
{
    void **handles;
    dTHXa(interp->perl);

    PERL_SET_CONTEXT(interp->perl);

    MP_TRACE_i(MP_FUNC, "interp == 0x%lx / perl: 0x%lx",
               (unsigned long)interp, (unsigned long)interp->perl);

    if (MpInterpIN_USE(interp)) {
        MP_TRACE_i(MP_FUNC, "*error - still in use!*");
    }

    handles = modperl_xs_dl_handles_get(aTHX);

    modperl_perl_destruct(interp->perl);

    modperl_xs_dl_handles_close(handles);

    free(interp);
}

apr_status_t modperl_interp_cleanup(void *data)
{
    modperl_interp_destroy((modperl_interp_t *)data);
    return APR_SUCCESS;
}

modperl_interp_t *modperl_interp_get(server_rec *s)
{
    MP_dSCFG(s);
    modperl_interp_t *interp = NULL;
    modperl_interp_pool_t *mip = scfg->mip;
    modperl_list_t *head;

    head = modperl_tipool_pop(mip->tipool);
    interp = (modperl_interp_t *)head->data;

    MP_TRACE_i(MP_FUNC, "head == 0x%lx, parent == 0x%lx",
               (unsigned long)head, (unsigned long)mip->parent);

    MP_TRACE_i(MP_FUNC, "selected 0x%lx (perl==0x%lx)",
               (unsigned long)interp,
               (unsigned long)interp->perl);

#ifdef MP_TRACE
    interp->tid = MP_TIDF;
    MP_TRACE_i(MP_FUNC, "thread == 0x%lx", interp->tid);
#endif

    MpInterpIN_USE_On(interp);

    return interp;
}

apr_status_t modperl_interp_pool_destroy(void *data)
{
    modperl_interp_pool_t *mip = (modperl_interp_pool_t *)data;

    if (mip->tipool) {
        modperl_tipool_destroy(mip->tipool);
        mip->tipool = NULL;
    }

    if (MpInterpBASE(mip->parent)) {
        /* multiple mips might share the same parent
         * make sure its only destroyed once
         */
        MP_TRACE_i(MP_FUNC, "parent == 0x%lx",
                   (unsigned long)mip->parent);

        modperl_interp_destroy(mip->parent);
    }

    return APR_SUCCESS;
}

static void *interp_pool_grow(modperl_tipool_t *tipool, void *data)
{
    modperl_interp_pool_t *mip = (modperl_interp_pool_t *)data;
    MP_TRACE_i(MP_FUNC, "adding new interpreter to the pool");
    return (void *)modperl_interp_new(mip, mip->parent->perl);
}

static void interp_pool_shrink(modperl_tipool_t *tipool, void *data,
                               void *item)
{
    modperl_interp_destroy((modperl_interp_t *)item);
}

static void interp_pool_dump(modperl_tipool_t *tipool, void *data,
                             modperl_list_t *listp)
{
    while (listp) {
        modperl_interp_t *interp = (modperl_interp_t *)listp->data;
        MP_TRACE_i(MP_FUNC, "listp==0x%lx, interp==0x%lx, requests=%d",
                 (unsigned long)listp, (unsigned long)interp,
                 interp->num_requests);
        listp = listp->next;
    }
}

static modperl_tipool_vtbl_t interp_pool_func = {
    interp_pool_grow,
    interp_pool_grow,
    interp_pool_shrink,
    interp_pool_shrink,
    interp_pool_dump,
};

void modperl_interp_init(server_rec *s, apr_pool_t *p,
                         PerlInterpreter *perl)
{
    apr_pool_t *server_pool = modperl_server_pool();
    pTHX;
    MP_dSCFG(s);
    modperl_interp_pool_t *mip =
        (modperl_interp_pool_t *)apr_pcalloc(p, sizeof(*mip));

    MP_TRACE_i(MP_FUNC, "server=%s", modperl_server_desc(s, p));

    if (modperl_threaded_mpm()) {
        mip->tipool = modperl_tipool_new(p, scfg->interp_pool_cfg,
                                         &interp_pool_func, mip);
    }

    mip->server = s;
    mip->parent = modperl_interp_new(mip, NULL);
    aTHX = mip->parent->perl = perl;

    /* this happens post-config in mod_perl.c:modperl_init_clones() */
    /* modperl_tipool_init(tipool); */

    apr_pool_cleanup_register(server_pool, (void*)mip,
                              modperl_interp_pool_destroy,
                              apr_pool_cleanup_null);

    scfg->mip = mip;
}

apr_status_t modperl_interp_unselect(void *data)
{
    modperl_interp_t *interp = (modperl_interp_t *)data;
    modperl_interp_pool_t *mip = interp->mip;

    MP_ASSERT(interp && MpInterpIN_USE(interp) && interp->refcnt > 0);
    MP_TRACE_i(MP_FUNC, "unselect(interp=%pp): refcnt=%d",
               interp, interp->refcnt);

    --interp->refcnt;

    if (interp->refcnt > 0) {
        MP_TRACE_i(MP_FUNC, "interp=0x%lx, refcnt=%d -- interp still in use",
                   (unsigned long)interp, interp->refcnt);
        return APR_SUCCESS;
    }

    if (!MpInterpIN_USE(interp)){
        MP_TRACE_i(MP_FUNC, "interp=0x%pp, refcnt=%d -- interp already not in use",
                   interp, interp->refcnt);
        return APR_SUCCESS;
    }

    MpInterpIN_USE_Off(interp);

    modperl_thx_interp_set(interp->perl, NULL);
#ifdef MP_DEBUG
    PERL_SET_CONTEXT(NULL);
#endif

    if (interp == mip->parent) {
        MP_TRACE_i(MP_FUNC, "parent interp=%pp freed", interp);
    }
    else {
        interp->ccfg->interp = NULL;
        modperl_tipool_putback_data(mip->tipool, data, interp->num_requests);
        MP_TRACE_i(MP_FUNC, "interp=%pp freed, tipool(size=%ld, in_use=%ld)",
                   interp, mip->tipool->size, mip->tipool->in_use);
    }

    return APR_SUCCESS;
}

/* XXX:
 * interp is marked as in_use for the scope of the pool it is
 * stashed in.  this is done to avoid the tipool->tlock whenever
 * possible.  neither approach is ideal.
 */
#define MP_INTERP_KEY "MODPERL_INTERP"

#define get_interp(p) \
    (void)apr_pool_userdata_get((void **)&interp, MP_INTERP_KEY, p)

#define set_interp(p) \
     (void)apr_pool_userdata_set((void *)interp, MP_INTERP_KEY, \
                                 modperl_interp_unselect, \
                                 p)

modperl_interp_t *modperl_interp_pool_get(apr_pool_t *p)
{
    modperl_interp_t *interp = NULL;
    get_interp(p);
    return interp;
}

void modperl_interp_pool_set(apr_pool_t *p,
                             modperl_interp_t *interp)
{
    (void)apr_pool_userdata_set((void *)interp, MP_INTERP_KEY, NULL, p);
}

/*
 * used in the case where we don't have a request_rec or conn_rec,
 * such as for directive handlers per-{dir,srv} create and merge.
 * "request time pool" is most likely a request_rec->pool.
 */
modperl_interp_t *modperl_interp_pool_select(apr_pool_t *p,
                                             server_rec *s)
{
    int is_startup = (p == s->process->pconf);
    modperl_interp_t *interp = NULL;

    if (is_startup) {
        MP_dSCFG(s);
        if (scfg) {
            MP_TRACE_i(MP_FUNC, "using parent interpreter at startup");

            if (!scfg->mip) {
                /* we get here if directive handlers are invoked
                 * before server merge.
                 */
                modperl_init_vhost(s, p, NULL);
                if (!scfg->mip) {
                    /* FIXME: We get here if global "server_rec" == s, scfg->mip
                     * is not created then. I'm not sure if that's bug or 
                     * bad/good design decicision. For now just return NULL.
                     */
                    return NULL;
                }
            }

            interp = scfg->mip->parent;
        }
        else {
            if (!(interp = modperl_interp_pool_get(p))) {
        	interp = modperl_interp_get(s);
                modperl_interp_pool_set(p, interp);

                MP_TRACE_i(MP_FUNC, "set interp %pp in pconf pool %pp",
                           interp, p);
            }
            else {
                MP_TRACE_i(MP_FUNC, "found interp %pp in pconf pool %pp",
                           interp, p);
            }
        }

        MpInterpIN_USE_On(interp);
        interp->refcnt++;
        /* set context (THX) for this thread */
        PERL_SET_CONTEXT(interp->perl);
        /* let the perl interpreter point back to its interp */
        modperl_thx_interp_set(interp->perl, interp);

        return interp;
    }
    else {
        request_rec *r;
        apr_pool_userdata_get((void **)&r, "MODPERL_R", p);
        MP_ASSERT(r);
        MP_TRACE_i(MP_FUNC, "found userdata MODPERL_R in pool %#lx as %lx",
                   (unsigned long)r->pool, (unsigned long)r);
        return modperl_interp_select(r, NULL, NULL);
    }
}

modperl_interp_t *modperl_interp_select(request_rec *r, conn_rec *c,
                                        server_rec *s)
{
    MP_dSCFG((r ? s=r->server : c ? s=c->base_server : s));
    MP_dDCFG;
    modperl_config_con_t *ccfg;
    const char *desc = NULL;
    modperl_interp_t *interp = NULL;
    apr_pool_t *p = NULL;

    /* What does the following condition mean?
     * (r || c): if true we are at runtime. There is some kind of request
     *           being processed.
     * threaded_mpm: self-explanatory
     *
     * Thus, it is true if we are either at initialization time or at runtime
     * but with prefork-MPM. */
    if (!((r || c) && modperl_threaded_mpm())) {
        interp = scfg->mip->parent;
        MpInterpIN_USE_On(interp);
        interp->refcnt++;
        /* XXX: if no VirtualHosts w/ PerlOptions +Parent we can skip this */
        PERL_SET_CONTEXT(interp->perl);
        /* let the perl interpreter point back to its interp */
        modperl_thx_interp_set(interp->perl, interp);

        MP_TRACE_i(MP_FUNC,
                   "using parent 0x%pp (perl=0x%pp) for %s:%d refcnt set to %d",
                   interp, interp->perl, s->server_hostname, s->port,
                   interp->refcnt);
        return interp;
    }

    if(!c) c = r->connection;
    ccfg = modperl_config_con_get(c);

    if (ccfg && ccfg->interp) {
        ccfg->interp->refcnt++;

        MP_TRACE_i(MP_FUNC,
                   "found interp 0x%lx in con config, refcnt incremented to %d",
                   (unsigned long)ccfg->interp, ccfg->interp->refcnt);
        /* set context (THX) for this thread */
        PERL_SET_CONTEXT(ccfg->interp->perl);
        /* modperl_thx_interp_set() is not called here because the interp
         * already belongs to the perl interpreter
         */
        return ccfg->interp;
    }

    MP_TRACE_i(MP_FUNC,
               "fetching interp for %s:%d", s->server_hostname, s->port);
    interp = modperl_interp_get(s);
    MP_TRACE_i(MP_FUNC, "  --> got %pp (perl=%pp)", interp, interp->perl);
    ++interp->num_requests; /* should only get here once per request */
    interp->refcnt = 1;

    /* set context (THX) for this thread */
    PERL_SET_CONTEXT(interp->perl);
    /* let the perl interpreter point back to its interp */
    modperl_thx_interp_set(interp->perl, interp);

    /* make sure ccfg is initialized */
    modperl_config_con_init(c, ccfg);
    ccfg->interp = interp;
    interp->ccfg = ccfg;

    MP_TRACE_i(MP_FUNC,
               "pulled interp %pp (perl=%pp) from mip, num_requests is %d",
               interp, interp->perl, interp->num_requests);

    return interp;
}

/* currently up to the caller if mip needs locking */
void modperl_interp_mip_walk(PerlInterpreter *current_perl,
                             PerlInterpreter *parent_perl,
                             modperl_interp_pool_t *mip,
                             modperl_interp_mip_walker_t walker,
                             void *data)
{
    modperl_list_t *head = mip->tipool ? mip->tipool->idle : NULL;

    if (!current_perl) {
        current_perl = PERL_GET_CONTEXT;
    }

    if (parent_perl) {
        PERL_SET_CONTEXT(parent_perl);
        walker(parent_perl, mip, data);
    }

    while (head) {
        PerlInterpreter *perl = ((modperl_interp_t *)head->data)->perl;
        PERL_SET_CONTEXT(perl);
        walker(perl, mip, data);
        head = head->next;
    }

    PERL_SET_CONTEXT(current_perl);
}

void modperl_interp_mip_walk_servers(PerlInterpreter *current_perl,
                                     server_rec *base_server,
                                     modperl_interp_mip_walker_t walker,
                                     void *data)
{
    server_rec *s = base_server->next;
    modperl_config_srv_t *base_scfg = modperl_config_srv_get(base_server);
    PerlInterpreter *base_perl = base_scfg->mip->parent->perl;

    modperl_interp_mip_walk(current_perl, base_perl,
                            base_scfg->mip, walker, data);

    while (s) {
        MP_dSCFG(s);
        PerlInterpreter *perl = scfg->mip->parent->perl;
        modperl_interp_pool_t *mip = scfg->mip;

        /* skip vhosts who share parent perl */
        if (perl == base_perl) {
            perl = NULL;
        }

        /* skip vhosts who share parent mip */
        if (scfg->mip == base_scfg->mip) {
            mip = NULL;
        }

        if (perl || mip) {
            modperl_interp_mip_walk(current_perl, perl,
                                    mip, walker, data);
        }

        s = s->next;
    }
}

#define MP_THX_INTERP_KEY "modperl2::thx_interp_key"
modperl_interp_t *modperl_thx_interp_get(pTHX)
{
    modperl_interp_t *interp;
    SV **svp = hv_fetch(PL_modglobal, MP_THX_INTERP_KEY,
                        strlen(MP_THX_INTERP_KEY), 0);
    if (!svp) return NULL;
    interp = INT2PTR(modperl_interp_t *, SvIV(*svp));
    return interp;
}

void modperl_thx_interp_set(pTHX_ modperl_interp_t *interp)
{
    (void)hv_store(PL_modglobal, MP_THX_INTERP_KEY, strlen(MP_THX_INTERP_KEY),
                   newSViv(PTR2IV(interp)), 0);
    return;
}

#else

void modperl_interp_init(server_rec *s, apr_pool_t *p,
                         PerlInterpreter *perl)
{
    MP_dSCFG(s);
    scfg->perl = perl;
}

apr_status_t modperl_interp_cleanup(void *data)
{
    return APR_SUCCESS;
}

#endif /* USE_ITHREADS */

/*
 * Local Variables:
 * c-basic-offset: 4
 * indent-tabs-mode: nil
 * End:
 */
