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

#ifdef MP_IO_TIE_PERLIO

/***************************
 * The PerlIO Apache layer *
 ***************************/

/* PerlIO ":Apache2" layer is used to use the Apache callbacks to read
 * from STDIN and write to STDOUT. The PerlIO API is documented in
 * perliol.pod */

typedef struct {
    struct _PerlIO base;
    request_rec *r;
} PerlIOApache;

/* _open just allocates the layer, _pushed does the real job of
 * filling the data in */
static PerlIO *
PerlIOApache_open(pTHX_ PerlIO_funcs *self, PerlIO_list_t *layers, IV n,
                  const char *mode, int fd, int imode, int perm,
                  PerlIO *f, int narg, SV **args)
{
    if (!f) {
        f = PerlIO_allocate(aTHX);
    }
    if ( (f = PerlIO_push(aTHX_ f, self, mode, args[0])) ) {
        PerlIOBase(f)->flags |= PERLIO_F_OPEN;
    }

    MP_TRACE_o(MP_FUNC, "mode %s", mode);

    return f;
}

/* this callback is used by pushed() and binmode() to add the layer */
static IV
PerlIOApache_pushed(pTHX_ PerlIO *f, const char *mode, SV *arg,
                    PerlIO_funcs *tab)
{
    IV code;
    PerlIOApache *st = PerlIOSelf(f, PerlIOApache);

    if (arg) {
        st->r = modperl_sv2request_rec(aTHX_ arg);
        MP_TRACE_o(MP_FUNC, "stored request_rec obj: 0x%lx", st->r);
    }
    else {
        Perl_croak(aTHX_"failed to insert the :Apache2 layer. "
                   "Apache2::RequestRec object argument is required");
        /* XXX: try to get Apache2->request? */
    }

    /* this method also sets the right flags according to the
     * 'mode' */
    code = PerlIOBase_pushed(aTHX_ f, mode, Nullsv, tab);

    return code;
}

static SV *
PerlIOApache_getarg(pTHX_ PerlIO *f, CLONE_PARAMS *param, int flags)
{
    PerlIOApache *st = PerlIOSelf(f, PerlIOApache);
    SV *sv;

    if (!st->r) {
        Perl_croak(aTHX_ "an attempt to getarg from a stale io handle");
    }

    sv = newSV(0);
    sv_setref_pv(sv, "Apache2::RequestRec", (void*)(st->r));

    MP_TRACE_o(MP_FUNC, "retrieved request_rec obj: 0x%lx", st->r);

    return sv;
}

static IV
PerlIOApache_fileno(pTHX_ PerlIO *f)
{
    /* XXX: we could return STDIN => 0, STDOUT => 1, but that wouldn't
     * be correct, as the IO goes through the socket, may be we should
     * return the filedescriptor of the socket?
     *
     * -1 in this case indicates that the layer cannot provide fileno
     */
    MP_TRACE_o(MP_FUNC, "did nothing");
    return -1;
}

static SSize_t
PerlIOApache_read(pTHX_ PerlIO *f, void *vbuf, Size_t count)
{
    PerlIOApache *st = PerlIOSelf(f, PerlIOApache);
    request_rec *r = st->r;

    if (!(PerlIOBase(f)->flags & PERLIO_F_CANREAD) ||
        PerlIOBase(f)->flags & (PERLIO_F_EOF|PERLIO_F_ERROR)) {
        return 0;
    }

    return modperl_request_read(aTHX_ r, (char*)vbuf, count);
}

static SSize_t
PerlIOApache_write(pTHX_ PerlIO *f, const void *vbuf, Size_t count)
{
    PerlIOApache *st = PerlIOSelf(f, PerlIOApache);
    modperl_config_req_t *rcfg = modperl_config_req_get(st->r);
    apr_size_t bytes = 0;

    if (!(PerlIOBase(f)->flags & PERLIO_F_CANWRITE)) {
        return 0;
    }

    MP_CHECK_WBUCKET_INIT("print");

    MP_TRACE_o(MP_FUNC, "%4db [%s]", count,
               MP_TRACE_STR_TRUNC(rcfg->wbucket->pool, vbuf, count));

    MP_RUN_CROAK(modperl_wbucket_write(aTHX_ rcfg->wbucket, vbuf, &count),
                 ":Apache2 IO write");

    bytes += count;

    return (SSize_t) bytes;
}

static IV
PerlIOApache_flush(pTHX_ PerlIO *f)
{
    PerlIOApache *st = PerlIOSelf(f, PerlIOApache);
    modperl_config_req_t *rcfg;

    if (!st->r) {
        Perl_warn(aTHX_ "an attempt to flush a stale IO handle");
        return -1;
    }

    /* no flush on readonly io handle */
    if (! (PerlIOBase(f)->flags & PERLIO_F_CANWRITE) ) {
        return -1;
    }

    rcfg = modperl_config_req_get(st->r);

    MP_CHECK_WBUCKET_INIT("flush");

    MP_TRACE_o(MP_FUNC, "%4db [%s]", rcfg->wbucket->outcnt,
               MP_TRACE_STR_TRUNC(rcfg->wbucket->pool,
                                  rcfg->wbucket->outbuf,
                                  rcfg->wbucket->outcnt));

    MP_RUN_CROAK_RESET_OK(st->r->server,
                          modperl_wbucket_flush(rcfg->wbucket, FALSE),
                          ":Apache2 IO flush");

    return 0;
}

/* 5.8.0 doesn't export PerlIOBase_noop_fail, so we duplicate it here */
static IV PerlIOApache_noop_fail(pTHX_ PerlIO *f)
{
    return -1;
}

static IV
PerlIOApache_close(pTHX_ PerlIO *f)
{
    IV code = PerlIOBase_close(aTHX_ f);
    PerlIOApache *st = PerlIOSelf(f, PerlIOApache);

    MP_TRACE_o(MP_FUNC, "done with request_rec obj: 0x%lx", st->r);
    /* prevent possible bugs where a stale r will be attempted to be
     * reused (e.g. dupped filehandle) */
    st->r = NULL;

    return code;
}

static IV
PerlIOApache_popped(pTHX_ PerlIO *f)
{
    /* XXX: just temp for tracing */
    MP_TRACE_o(MP_FUNC, "done");
    return PerlIOBase_popped(aTHX_ f);
}


static PerlIO_funcs PerlIO_Apache = {
    sizeof(PerlIO_funcs),
    "Apache2",
    sizeof(PerlIOApache),
    PERLIO_K_MULTIARG | PERLIO_K_RAW,
    PerlIOApache_pushed,
    PerlIOApache_popped,
    PerlIOApache_open,
    PerlIOBase_binmode,
    PerlIOApache_getarg,
    PerlIOApache_fileno,
    PerlIOBase_dup,
    PerlIOApache_read,
    PerlIOBase_unread,
    PerlIOApache_write,
    NULL,                       /* can't seek on STD{IN|OUT}, fail on call*/
    NULL,                       /* can't tell on STD{IN|OUT}, fail on call*/
    PerlIOApache_close,
    PerlIOApache_flush,
    PerlIOApache_noop_fail,     /* fill */
    PerlIOBase_eof,
    PerlIOBase_error,
    PerlIOBase_clearerr,
    PerlIOBase_setlinebuf,
    NULL,                       /* get_base */
    NULL,                       /* get_bufsiz */
    NULL,                       /* get_ptr */
    NULL,                       /* get_cnt */
    NULL,                       /* set_ptrcnt */
};

/* ***** End of PerlIOApache tab ***** */

MP_INLINE void modperl_io_apache_init(pTHX)
{
    PerlIO_define_layer(aTHX_ &PerlIO_Apache);
}

#endif /* defined MP_IO_TIE_PERLIO */

/******  Other request IO functions  *******/


MP_INLINE SSize_t modperl_request_read(pTHX_ request_rec *r,
                                       char *buffer, Size_t len)
{
    SSize_t total = 0;
    Size_t wanted = len;
    int seen_eos = 0;
    char *tmp = buffer;
    apr_bucket_brigade *bb;

    if (len <= 0) {
        return 0;
    }

    bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
    if (bb == NULL) {
        r->connection->keepalive = AP_CONN_CLOSE;
        Perl_croak(aTHX_ "failed to create bucket brigade");
    }

    do {
        apr_size_t read;
        apr_status_t rc;

        rc = ap_get_brigade(r->input_filters, bb, AP_MODE_READBYTES,
                            APR_BLOCK_READ, len);
        if (rc != APR_SUCCESS) {
            /* if we fail here, we want to stop trying to read data
             * from the client.
             */
            r->connection->keepalive = AP_CONN_CLOSE;
            apr_brigade_destroy(bb);
            modperl_croak(aTHX_ rc, "Apache2::RequestIO::read");
        }

        /* If this fails, it means that a filter is written
         * incorrectly and that it needs to learn how to properly
         * handle APR_BLOCK_READ requests by returning data when
         * requested.
         */
        if (APR_BRIGADE_EMPTY(bb)) {
            apr_brigade_destroy(bb);
            /* we can't tell which filter is broken, since others may
             * just pass data through */
            Perl_croak(aTHX_ "Apache2::RequestIO::read: "
                       "Aborting read from client. "
                       "One of the input filters is broken. "
                       "It returned an empty bucket brigade for "
                       "the APR_BLOCK_READ mode request");
        }

        if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) {
            seen_eos = 1;
        }

        read = len;
        rc = apr_brigade_flatten(bb, tmp, &read);
        if (rc != APR_SUCCESS) {
            apr_brigade_destroy(bb);
            modperl_croak(aTHX_ rc, "Apache2::RequestIO::read");
        }

        total += read;
        tmp   += read;
        len   -= read;

        /* XXX: what happens if the downstream filter returns more
         * data than the caller has asked for? We can't return more
         * data than requested, so it needs to be stored somewhere and
         * dealt with on the subsequent calls to this function. or may
         * be we should just assert, blaming a bad filter. at the
         * moment I couldn't find a spec telling whether it's wrong
         * for the filter to return more data than it was asked for in
         * the AP_MODE_READBYTES mode.
         */

        apr_brigade_cleanup(bb);

    } while (len > 0 && !seen_eos);

    apr_brigade_destroy(bb);

    MP_TRACE_o(MP_FUNC, "wanted %db, read %db [%s]", wanted, total,
               MP_TRACE_STR_TRUNC(r->pool, buffer, total));

    return total;
}
