/* 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 "modperl_largefiles.h"

#include "mod_perl.h"
#include "modperl_apr_perlio.h"

#if defined(PERLIO_LAYERS) && defined(PERLIO_K_MULTIARG) /* 5.7.2+ */

/**********************************************************************
 * The PerlIO APR layer.
 * The PerlIO API is documented in perliol.pod.
 **********************************************************************/

/*
 * APR::PerlIO implements a PerlIO layer using apr_file_io as the core.
 */

/*
 * XXX: Since we cannot snoop on the internal apr_file_io buffer
 * currently the IO is not buffered on the Perl side so every read
 * requests a char at a time, which is slow. Consider copying the
 * relevant code from PerlIOBuf to implement our own buffer, similar
 * to what PerlIOBuf does or push :perlio layer on top of this layer
 */

typedef struct {
    struct _PerlIO base;
    apr_file_t *file;
    apr_pool_t *pool;
} PerlIOAPR;

static IV PerlIOAPR_pushed(pTHX_ PerlIO *f, const char *mode,
                           SV *arg, PerlIO_funcs *tab)
{
    IV code = PerlIOBase_pushed(aTHX_ f, mode, arg, tab);
    if (*PerlIONext(f)) {
        /* XXX: not sure if we can do anything here, but see
         * PerlIOUnix_pushed for things that it does
         */
    }
    return code;
}

static PerlIO *PerlIOAPR_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)
{
    SV *arg = (narg > 0) ? *args : PerlIOArg;
    PerlIOAPR *st;
    const char *path;
    apr_int32_t apr_flag;
    apr_status_t rc;
    SV *sv;

    if (!(SvROK(arg) || SvPOK(arg))) {
        return NULL;
    }

    /* XXX: why passing only SV* for arg, check this out in PerlIO_push */
    if (!f) {
        f = PerlIO_push(aTHX_ PerlIO_allocate(aTHX), self, mode, arg);
    }
    else {
        f = PerlIO_push(aTHX_ f, self, mode, arg);
    }

    /* grab the last arg as a filepath */
    path = (const char *)SvPV_nolen(args[narg-2]);

    switch (*mode) {
      case 'a':
        apr_flag = APR_APPEND | APR_CREATE;
        break;
      case 'w':
        apr_flag = APR_WRITE | APR_CREATE | APR_TRUNCATE;
        break;
      case 'r':
        apr_flag = APR_READ;
        break;
      default:
        Perl_croak(aTHX_ "unknown open mode: %s", mode);
    }

    /* APR_BINARY:   we always do binary read and PerlIO is supposed
     *               to handle :crlf if any (by pushing this layer at
     *               open().
     * APR_BUFFERED: XXX, not sure if it'll be needed if we will push
     *               :perlio (== PerlIOBuf) layer on top
     */
    apr_flag |= APR_BUFFERED | APR_BINARY;

    st = PerlIOSelf(f, PerlIOAPR);

    /* XXX: can't reuse a wrapper mp_xs_sv2_APR__Pool */
    /* XXX: should probably add checks on pool validity in all other callbacks */
    sv = args[narg-1];
    if (SvROK(sv) && (SvTYPE(SvRV(sv)) == SVt_PVMG)) {
        st->pool = INT2PTR(apr_pool_t *, SvIV((SV*)SvRV(sv)));
    }
    else {
        Perl_croak(aTHX_ "argument is not a blessed reference "
                   "(expecting an APR::Pool derived object)");
    }

    rc = apr_file_open(&st->file, path, apr_flag, APR_OS_DEFAULT, st->pool);

    MP_TRACE_o(MP_FUNC, "obj=0x%lx, file=0x%lx, name=%s, rc=%d",
               (unsigned long)f, (unsigned long)st->file,
               path ? path : "(UNKNOWN)", rc);

    if (rc != APR_SUCCESS) {
        /* it just so happens that since $! is tied to errno, we get
         * it set right via the system call that apr_file_open has
         * performed internally, no need to do anything special */
        PerlIO_pop(aTHX_ f);
        return NULL;
    }

    PerlIOBase(f)->flags |= PERLIO_F_OPEN;
    return f;
}

static IV PerlIOAPR_fileno(pTHX_ PerlIO *f)
{
    /* apr_file_t* is an opaque struct, so fileno is not available.
     * -1 in this case indicates that the layer cannot provide fileno
     */
    return -1;
}

static PerlIO *PerlIOAPR_dup(pTHX_ PerlIO *f, PerlIO *o,
                             CLONE_PARAMS *param, int flags)
{
    apr_status_t rc;

    if ((f = PerlIOBase_dup(aTHX_ f, o, param, flags))) {
        PerlIOAPR *fst = PerlIOSelf(f, PerlIOAPR);
        PerlIOAPR *ost = PerlIOSelf(o, PerlIOAPR);

        rc = apr_file_dup(&fst->file, ost->file, ost->pool);

        MP_TRACE_o(MP_FUNC, "obj=0x%lx, "
                   "file=0x%lx => 0x%lx, rc=%d",
                   (unsigned long)f, (unsigned long)ost->file,
                   (unsigned long)fst->file, rc);

        if (rc == APR_SUCCESS) {
            fst->pool = ost->pool;
            return f;
        }
    }

    return NULL;
}

static SSize_t PerlIOAPR_read(pTHX_ PerlIO *f, void *vbuf, Size_t count)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    apr_status_t rc;

    rc = apr_file_read(st->file, vbuf, &count);

    MP_TRACE_o(MP_FUNC, "%db [%s]", (int)count,
               MP_TRACE_STR_TRUNC(st->pool, (char *)vbuf, (int)count));

    if (rc == APR_EOF) {
        PerlIOBase(f)->flags |= PERLIO_F_EOF;
        return count;
    }
    else if (rc != APR_SUCCESS) {
        modperl_croak(aTHX_ rc, "APR::PerlIO::read");
    }

    return count;
}

static SSize_t PerlIOAPR_write(pTHX_ PerlIO *f, const void *vbuf, Size_t count)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    apr_status_t rc;

    MP_TRACE_o(MP_FUNC, "%db [%s]", (int)count,
               MP_TRACE_STR_TRUNC(st->pool, (char *)vbuf, (int)count));

    rc = apr_file_write(st->file, vbuf, &count);
    if (rc == APR_SUCCESS) {
        return (SSize_t) count;
    }

    PerlIOBase(f)->flags |= PERLIO_F_ERROR;
    return (SSize_t) -1;
}

static IV PerlIOAPR_flush(pTHX_ PerlIO *f)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    apr_status_t rc;

    rc = apr_file_flush(st->file);
    if (rc == APR_SUCCESS) {
        return 0;
    }

    PerlIOBase(f)->flags |= PERLIO_F_ERROR;
    return -1;
}

static IV PerlIOAPR_seek(pTHX_ PerlIO *f, Off_t offset, int whence)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    apr_seek_where_t where;
    apr_status_t rc;
    apr_off_t seek_offset = 0;

#ifdef MP_LARGE_FILES_CONFLICT
    if (offset != 0) {
        Perl_croak(aTHX_ "PerlIO::APR::seek with non-zero offset"
                   "is not supported with Perl built w/ -Duselargefiles"
                   " and APR w/o largefiles support");
    }
#else
    seek_offset = offset;
#endif

    /* Flush the fill buffer */
    if (PerlIO_flush(f) != 0) {
        return -1;
    }

    switch(whence) {
      case 0:
        where = APR_SET;
        break;
      case 1:
        where = APR_CUR;
        break;
      case 2:
        where = APR_END;
        break;
      default:
        Perl_croak(aTHX_ "unknown whence mode: %d", whence);
    }

    rc = apr_file_seek(st->file, where, &seek_offset);
    if (rc == APR_SUCCESS) {
        return 0;
    }

    return -1;
}

static Off_t PerlIOAPR_tell(pTHX_ PerlIO *f)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    apr_off_t offset = 0;
    apr_status_t rc;

    rc = apr_file_seek(st->file, APR_CUR, &offset);
    if (rc == APR_SUCCESS) {
        return (Off_t) offset;
    }

    return (Off_t) -1;
}

static IV PerlIOAPR_close(pTHX_ PerlIO *f)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    IV code = PerlIOBase_close(aTHX_ f);
    apr_status_t rc;

#ifdef MP_TRACE
    const char *new_path = NULL;
    apr_os_file_t os_file;

    if (!PL_dirty) {
        /* if this is called during perl_destruct we are in trouble */
        apr_file_name_get(&new_path, st->file);
    }

    rc = apr_os_file_get(&os_file, st->file);
    if (rc != APR_SUCCESS) {
        Perl_croak(aTHX_ "filedes retrieval failed!");
    }

    MP_TRACE_o(MP_FUNC, "obj=0x%lx, file=0x%lx, fd=%d, name=%s",
               (unsigned long)f, (unsigned long)st->file, os_file,
               new_path ? new_path : "(UNKNOWN)");
#endif

    if (PL_dirty) {
        /* there should not be any PerlIOAPR handles open
         * during perl_destruct
         */
        Perl_warn(aTHX_ "leaked PerlIOAPR handle 0x%lx",
                  (unsigned long)f);
        return -1;
    }

    rc = apr_file_flush(st->file);
    if (rc != APR_SUCCESS) {
        return -1;
    }

    rc = apr_file_close(st->file);
    if (rc != APR_SUCCESS) {
        return -1;
    }

    return code;
}

#if 0 /* we may use it if the buffering will be done at this layer */

static IV PerlIOAPR_fill(pTHX_ PerlIO *f)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    apr_status_t rc;
    SSize_t avail;
    Size_t count = st->base.bufsiz;

    if (!st->base.buf) {
        PerlIO_get_base(f);  /* allocate via vtable */
    }

    MP_TRACE_o(MP_FUNC, "asked to fill %d chars", count);

    rc = apr_file_read(st->file, st->base.ptr, &count);
    if (rc != APR_SUCCESS) {
        PerlIOBase(f)->flags |= PERLIO_F_ERROR;
        return -1;
    }

    MP_TRACE_o(MP_FUNC, "got to fill %d chars", count);

    avail = count; /* apr_file_read() sets how many chars were read in count */
    if (avail <= 0) {
        if (avail == 0) {
            PerlIOBase(f)->flags |= PERLIO_F_EOF;
        }
        else {
            PerlIOBase(f)->flags |= PERLIO_F_ERROR;
        }

        return -1;
    }
    st->base.end = st->base.buf + avail;

    /* indicate that the buffer this layer currently holds unconsumed
       data read from layer below. */
    PerlIOBase(f)->flags |= PERLIO_F_RDBUF;

    return 0;
}

#endif

static IV PerlIOAPR_eof(pTHX_ PerlIO *f)
{
    PerlIOAPR *st = PerlIOSelf(f, PerlIOAPR);
    apr_status_t rc;

    rc = apr_file_eof(st->file);
    switch (rc) {
      case APR_EOF:
        return 1;
      default:
        return 0;
    }

    return -1;
}

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

static PerlIO_funcs PerlIO_APR = {
    sizeof(PerlIO_funcs),
    "APR",
    sizeof(PerlIOAPR),
    PERLIO_K_MULTIARG | PERLIO_K_RAW,
    PerlIOAPR_pushed,
    PerlIOBase_popped,
    PerlIOAPR_open,
    PerlIOBase_binmode,         /* binmode() is handled by :crlf */
    NULL,                       /* no getarg needed */
    PerlIOAPR_fileno,
    PerlIOAPR_dup,
    PerlIOAPR_read,
    PerlIOBase_unread,
    PerlIOAPR_write,
    PerlIOAPR_seek,
    PerlIOAPR_tell,
    PerlIOAPR_close,
    PerlIOAPR_flush,            /* flush */
    PerlIOAPR_noop_fail,        /* fill */
    PerlIOAPR_eof,
    PerlIOBase_error,
    PerlIOBase_clearerr,
    PerlIOBase_setlinebuf,
    NULL,                       /* get_base */
    NULL,                       /* get_bufsiz */
    NULL,                       /* get_ptr */
    NULL,                       /* get_cnt */
    NULL,                       /* set_ptrcnt */
};

void modperl_apr_perlio_init(pTHX)
{
    APR_REGISTER_OPTIONAL_FN(modperl_apr_perlio_apr_file_to_PerlIO);
    APR_REGISTER_OPTIONAL_FN(modperl_apr_perlio_apr_file_to_glob);

    PerlIO_define_layer(aTHX_ &PerlIO_APR);
}


/* ***** End of PerlIOAPR tab ***** */


/* ***** PerlIO <=> apr_file_t helper functions ***** */

PerlIO *modperl_apr_perlio_apr_file_to_PerlIO(pTHX_ apr_file_t *file,
                                              apr_pool_t *pool,
                                              modperl_apr_perlio_hook_e type)
{
    char *mode;
    const char *layers = ":APR";
    PerlIOAPR *st;
    PerlIO *f = PerlIO_allocate(aTHX);

    if (!f) {
        Perl_croak(aTHX_ "Failed to allocate PerlIO struct");
    }

    switch (type) {
      case MODPERL_APR_PERLIO_HOOK_WRITE:
        mode = "w";
        break;
      case MODPERL_APR_PERLIO_HOOK_READ:
        mode = "r";
        break;
      default:
        Perl_croak(aTHX_ "unknown MODPERL_APR_PERLIO type: %d", type);
    };

    PerlIO_apply_layers(aTHX_ f, mode, layers);
    if (!f) {
        Perl_croak(aTHX_ "Failed to apply the ':APR' layer");
    }

    st = PerlIOSelf(f, PerlIOAPR);

#ifdef MP_TRACE
    {
        apr_status_t rc;
        apr_os_file_t os_file;

        /* convert to the OS representation of file */
        rc = apr_os_file_get(&os_file, file);
        if (rc != APR_SUCCESS) {
            croak("filedes retrieval failed!");
        }

        MP_TRACE_o(MP_FUNC, "converting to PerlIO fd %d, mode '%s'",
                   os_file, mode);
    }
#endif

    st->pool = pool;
    st->file = file;
    PerlIOBase(f)->flags |= PERLIO_F_OPEN;

    return f;
}

static SV *modperl_apr_perlio_PerlIO_to_glob(pTHX_ PerlIO *pio,
                                             modperl_apr_perlio_hook_e type)
{
    SV *retval = modperl_perl_gensym(aTHX_ "APR::PerlIO");
    GV *gv = (GV*)SvRV(retval);

    gv_IOadd(gv);

    switch (type) {
      case MODPERL_APR_PERLIO_HOOK_WRITE:
          /* if IoIFP() is not assigned to it'll be never closed, see
           * Perl_io_close() */
        IoIFP(GvIOp(gv)) = IoOFP(GvIOp(gv)) = pio;
        IoFLAGS(GvIOp(gv)) |= IOf_FLUSH;
        IoTYPE(GvIOp(gv)) = IoTYPE_WRONLY;
        break;
      case MODPERL_APR_PERLIO_HOOK_READ:
        IoIFP(GvIOp(gv)) = pio;
        IoTYPE(GvIOp(gv)) = IoTYPE_RDONLY;
        break;
    };

    return sv_2mortal(retval);
}

SV *modperl_apr_perlio_apr_file_to_glob(pTHX_ apr_file_t *file,
                                        apr_pool_t *pool,
                                        modperl_apr_perlio_hook_e type)
{
    return modperl_apr_perlio_PerlIO_to_glob(aTHX_
                                     modperl_apr_perlio_apr_file_to_PerlIO(aTHX_ file, pool, type),
                                     type);
}

#else /* defined(PERLIO_LAYERS) (5.6.x) */

#ifdef USE_PERLIO /* 5.6.x + -Duseperlio */
#define MP_IO_TYPE PerlIO
#else
#define MP_IO_TYPE FILE
#endif

static MP_IO_TYPE *modperl_apr_perlio_apr_file_to_PerlIO(pTHX_ apr_file_t *file,
                                                 modperl_apr_perlio_hook_e type)
{
    MP_IO_TYPE *retval;
    char *mode;
    int fd;
    apr_os_file_t os_file;
    apr_status_t rc;

    switch (type) {
      case MODPERL_APR_PERLIO_HOOK_WRITE:
        mode = "w";
        break;
      case MODPERL_APR_PERLIO_HOOK_READ:
        mode = "r";
        break;
    };

    /* convert to the OS representation of file */
    rc = apr_os_file_get(&os_file, file);
    if (rc != APR_SUCCESS) {
        Perl_croak(aTHX_ "filedes retrieval failed!");
    }

    MP_TRACE_o(MP_FUNC, "converting fd %d", os_file);

    /* let's try without the dup, it seems to work fine:

       fd = PerlLIO_dup(os_file);
       MP_TRACE_o(MP_FUNC, "fd old: %d, new %d", os_file, fd);
       if (!(retval = PerlIO_fdopen(fd, mode))) {
       ...
       }

       in any case if we later decide to dup, remember to:

       apr_file_close(file);

       after PerlIO_fdopen() or that fh will be leaked

    */

    if (!(retval = PerlIO_fdopen(os_file, mode))) {
        PerlLIO_close(fd);
        Perl_croak(aTHX_ "fdopen failed!");
    }

    return retval;
}

SV *modperl_apr_perlio_apr_file_to_glob(pTHX_ apr_file_t *file,
                                        apr_pool_t *pool,
                                        modperl_apr_perlio_hook_e type)
{
    SV *retval = modperl_perl_gensym(aTHX_ "APR::PerlIO");
    GV *gv = (GV*)SvRV(retval);

    gv_IOadd(gv);

    switch (type) {
      case MODPERL_APR_PERLIO_HOOK_WRITE:
        IoIFP(GvIOp(gv)) = IoOFP(GvIOp(gv)) =
            modperl_apr_perlio_apr_file_to_PerlIO(aTHX_ file, type);
        IoFLAGS(GvIOp(gv)) |= IOf_FLUSH;
        IoTYPE(GvIOp(gv)) = IoTYPE_WRONLY;
        break;
      case MODPERL_APR_PERLIO_HOOK_READ:
        IoIFP(GvIOp(gv)) = modperl_apr_perlio_apr_file_to_PerlIO(aTHX_ file,
                                                                 type);
        IoTYPE(GvIOp(gv)) = IoTYPE_RDONLY;
        break;
    };

    return sv_2mortal(retval);
}

void modperl_apr_perlio_init(pTHX)
{
    APR_REGISTER_OPTIONAL_FN(modperl_apr_perlio_apr_file_to_glob);
}

#endif /* PERLIO_LAYERS */
