/*
*
* Copyright (c) 2018, cPanel, LLC.
* All rights reserved.
* http://cpanel.net
*
* This is free software; you can redistribute it and/or modify it under the
* same terms as Perl itself.
*
*/

#include <EXTERN.h>
#include <perl.h>
#include <XSUB.h>
#include <embed.h>

#include "FileCheck.h"

/*
*  Macro to make the moking process easier
*     for now keep them there, so we can hack them in the same file
*/

/* generic macro with args */
#define _CALL_REAL_PP(zOP) (* ( gl_overload_ft->op[zOP].real_pp ) )(aTHX)
#define _RETURN_CALL_REAL_PP_IF_UNMOCK(zOP) if (!gl_overload_ft->op[zOP].is_mocked) return _CALL_REAL_PP(zOP);

/* simplified versions for our custom usage */
#define CALL_REAL_OP()            _CALL_REAL_PP(PL_op->op_type)
#define RETURN_CALL_REAL_OP_IF_UNMOCK() _RETURN_CALL_REAL_PP_IF_UNMOCK(PL_op->op_type)

#define INIT_FILECHECK_MOCK(op_name, op_type, f) \
  newCONSTSUB(stash, op_name,    newSViv(op_type) ); \
  gl_overload_ft->op[op_type].real_pp = PL_ppaddr[op_type]; \
  PL_ppaddr[op_type] = f;

/* TODO: need to improve for Perl <= 5.014 */
#define RETURN_CALL_REAL_OP_IF_CALL_WITH_DEFGV() STMT_START { \
    if (gl_overload_ft->op[OP_STAT].is_mocked) { \
      SV *arg = *PL_stack_sp; GV *gv; \
      if ( SvTYPE(arg) == SVt_PVAV ) arg = arg + AvMAX( arg ); \
      /* GV *gv =  MAYBE_DEREF_GV(arg); */ \
      if ( PL_op->op_flags & OPf_REF ) \
        gv = cGVOP_gv; \
      else { \
        gv = MAYBE_DEREF_GV(arg); \
       } \
      /* printf ("### XXX ---> arg %d %p vs GV %p vs defgv %p \n", SvFLAGS(arg), *PL_stack_sp, gv, PL_defgv ); */ \
      /* get the GV from the arg if it s not a GV */ \
      if ( SvTYPE(arg) == SVt_NULL || gv == PL_defgv ) { \
        return CALL_REAL_OP(); \
      } \
    } \
  } STMT_END

/* a Stat_t struct has 13 elements */
#define STAT_T_MAX 13

/* ----------- start there --------------- */

OverloadFTOps  *gl_overload_ft = 0;

/*
* common helper to callback the pure perl function Overload::FileCheck::_check
*   and get the mocked value for the -X check
*
*  1 check is true  -> OP returns Yes
*  0 check is false -> OP returns No
*  TODO:            -> OP returns undef
* -1 fallback to the original OP
*/
int _overload_ft_ops() {
  SV *const arg = *PL_stack_sp;
  int optype = PL_op->op_type;  /* this is the current op_type we are mocking */
  int check_status = -1;        /* 1 -> YES ; 0 -> FALSE ; -1 -> delegate */
  int count;

  dSP;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  PUSHs(sv_2mortal(newSViv(optype)));
  PUSHs(arg);

  PUTBACK;

  count = call_pv("Overload::FileCheck::_check", G_SCALAR);

  SPAGAIN;

  if (count != 1)
    croak("No return value from Overload::FileCheck::_check for OP #%d\n", optype);

  check_status = POPi; /* TOOO pop on SV* for true / false & co */

  /* printf ("######## The result is %d /// OPTYPE is %d\n", check_status, optype); */

  PUTBACK;
  FREETMPS;
  LEAVE;

  return check_status;
}

SV* _overload_ft_ops_sv() {
  SV *const arg = *PL_stack_sp;
  int optype = PL_op->op_type;  /* this is the current op_type we are mocking */
  SV *status;        /* 1 -> YES ; 0 -> FALSE ; -1 -> delegate */

  dSP;
  int count;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  PUSHs(sv_2mortal(newSViv(optype)));
  PUSHs(arg);

  PUTBACK;

  count = call_pv("Overload::FileCheck::_check", G_SCALAR);

  SPAGAIN;

  if (count != 1)
    croak("No return value from Overload::FileCheck::_check for OP #%d\n", optype);

  status = POPs;
  SvREFCNT_inc( status );

  /* printf ("######## The result is %d /// OPTYPE is %d\n", check_status, optype); */

  PUTBACK;
  FREETMPS;
  LEAVE;

  return status;
}

/*
*   view perldoc to call SVs, method, ...
*
*   https://perldoc.perl.org/perlcall.html
*
*   but also https://perldoc.perl.org/perlguts.html
*/

#define set_stat_from_aryix(st, ix) \
  rsv = ary[ix]; \
  if (SvROK(rsv)) croak("Overload::FileCheck - Item %d should not be one RV\n", ix); \
  if (SvIOK(rsv)) st = SvIV( rsv ); \
  else if (SvUOK(rsv)) st = SvUV( rsv ); \
  else if (SvNOK(rsv)) st = SvNV( rsv ); \
  else croak("Overload::FileCheck - Item %d is not numeric...\n", ix);


/*
*   similar to _overload_ft_ops but expect more args from _check
*   which returns values for a fake stat
*
*   Note: we could also call a dedicated function as _check_stat
*/
int _overload_ft_stat(Stat_t *stat, int *size) {
  SV *const arg = *PL_stack_sp;
  int optype = PL_op->op_type;  /* this is the current op_type we are mocking */
  int check_status = -1;        /* 1 -> YES ; 0 -> FALSE ; -1 -> delegate */

  dSP;
  int count;
  SV *sv;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  PUSHs(sv_2mortal(newSViv(optype)));
  PUSHs(arg);
  PUTBACK;

  count = call_pv("Overload::FileCheck::_check", G_ARRAY);

  SPAGAIN;

  if (count < 1)
    croak("Overload::FileCheck::_check for stat OP #%d should return at least one SV.\n", optype);
  if (count > 2)
    croak("Overload::FileCheck::_check for stat OP #%d should return no more than two SVs.\n", optype);

  /* popping the stack from last entry to first */
  if (count == 2) sv = POPs; /* RvAV */
  check_status = POPi; /* TOOO pop on SV* for true / false & co */

  *size = -1; /* by default it fails */

  if ( check_status == 1 ) {
    AV *stat_array;
    SV **ary;
    SV *rsv;
    int av_size;

    if (count != 2)
      croak("Overload::FileCheck::_check for stat OP #%d should return two SVs on success.\n", optype);

    if ( ! SvROK(sv) )
      croak( "Overload::FileCheck::_check need to return an array ref" );

    stat_array = MUTABLE_AV( SvRV( sv ) );
    if ( SvTYPE(stat_array) !=  SVt_PVAV )
      croak( "Overload::FileCheck::_check need to return an array ref" );

    av_size = AvFILL(stat_array);
    if ( av_size > 0 && av_size != ( STAT_T_MAX - 1 ) )
      croak( "Overload::FileCheck::_check: Array should contain 13 elements" );

    *size = av_size; /* store the av_size */
    if ( av_size > 0 ) {

      ary = AvARRAY(stat_array);

      /* fill the stat struct */
      set_stat_from_aryix( stat->st_dev, 0 );       /* IV */
      set_stat_from_aryix( stat->st_ino, 1 );       /* IV or UV : neg = PL_statcache.st_ino < 0 */
      set_stat_from_aryix( stat->st_mode, 2 );      /* UV */
      set_stat_from_aryix( stat->st_nlink, 3 );     /* UV */
      set_stat_from_aryix( stat->st_uid, 4 );       /* IV ? */
      set_stat_from_aryix( stat->st_gid, 5 );       /* IV ? */
      set_stat_from_aryix( stat->st_rdev, 6 );      /* IV or PV */
      set_stat_from_aryix( stat->st_size, 7 );      /* NV or IV */
      set_stat_from_aryix( stat->st_atime, 8 );     /* NV or IV */
      set_stat_from_aryix( stat->st_mtime, 9 );     /* NV or IV */
      set_stat_from_aryix( stat->st_ctime, 10 );    /* NV or IV */
      set_stat_from_aryix( stat->st_blksize, 11 );  /* UV or PV */
      set_stat_from_aryix( stat->st_blocks, 12 );   /* UV or PV */
    }

  }

  PUTBACK;
  FREETMPS;
  LEAVE;

  return check_status;
}


/* a generic OP to overload the FT OPs returning yes or no */
/* FIXME also need to handle undef */
PP(pp_overload_ft_yes_no) {
  int check_status;

  assert( gl_overload_ft );

  /* not currently mocked */
  RETURN_CALL_REAL_OP_IF_UNMOCK();
  RETURN_CALL_REAL_OP_IF_CALL_WITH_DEFGV();

  check_status = _overload_ft_ops();

  {
    FT_SETUP_dSP_IF_NEEDED;

    if ( check_status == 1 ) FT_RETURNYES;
    if ( check_status == 0 ) FT_RETURNUNDEF;
    /* if ( check_status == -1 ) FT_RETURNUNDEF; */ /* TODO */
  }

  /* fallback */
  return CALL_REAL_OP();
}

PP(pp_overload_ft_int) {
  int check_status;

  assert( gl_overload_ft );

  /* not currently mocked */
  RETURN_CALL_REAL_OP_IF_UNMOCK();
  RETURN_CALL_REAL_OP_IF_CALL_WITH_DEFGV();

  check_status = _overload_ft_ops();

  /* SETERRNO(EEXIST,RMS_FEX); */ /* TODO */
  if ( check_status == -1 )
    return CALL_REAL_OP();

  {
    dTARGET;
    FT_SETUP_dSP_IF_NEEDED;

    /* TODO this is over simplistic some OPs can return one NV instead of IV */
    sv_setiv(TARG, (IV) check_status);
    FT_RETURN_TARG;
  }
}

PP(pp_overload_ft_nv) {
  SV *status;

  assert( gl_overload_ft );

  /* not currently mocked */
  RETURN_CALL_REAL_OP_IF_UNMOCK();
  RETURN_CALL_REAL_OP_IF_CALL_WITH_DEFGV();

  status = _overload_ft_ops_sv();

  if ( SvIOK(status) && SvIV(status) == -1 )
    return CALL_REAL_OP();

  if ( SvNOK(status) && SvNV(status) == -1 )
    return CALL_REAL_OP();

  {
    dTARGET;
    FT_SETUP_dSP_IF_NEEDED;

    if ( SvNOK(status) )
      sv_setnv(TARG, (NV) SvNV(status) );
    else if ( SvIOK(status) )
      sv_setiv(TARG, (IV) SvIV(status) );

    FT_RETURN_TARG;
  }
}

PP(pp_overload_stat) { /* stat & lstat */
  Stat_t mocked_stat = { 0 };  /* fake stats */
  int check_status = 0;
  int size;


  assert( gl_overload_ft );

  /* not currently mocked */
  RETURN_CALL_REAL_OP_IF_UNMOCK();
  RETURN_CALL_REAL_OP_IF_CALL_WITH_DEFGV();

  /* calling with our own tmp stat struct, instead of passing directly PL_statcache: more control */
  check_status = _overload_ft_stat(&mocked_stat, &size);

  /* explicit ask for fallback */
  if ( check_status == -1 )
    return CALL_REAL_OP();

  /*
  * The idea is too fool the stat function
  *   like if it was called by passing _ or *_
  *
  * We are setting these values as if stat was previously called
  *   - PL_laststype
  *   - PL_statcache
  *   - PL_laststatval
  *   - PL_statname
  *
  */

  {
      dSP;

      /* drop & replace our stack first element with *_ */
      /* Unexpected warning: Attempt to free unreferenced scalar: SV 0x119bd80. */
      //SV *previous_stack = sv_2mortal(POPs); /* what do we want to do with this ? */
      SV *previous_stack = POPs;

      /* copy the content of mocked_stat to PL_statcache */
      memcpy(&PL_statcache, &mocked_stat, sizeof(PL_statcache));

      if ( size >=  0) { /* yes it succeeds */
        PL_laststatval = 0;
      } else { /* the stat call fails */
        PL_laststatval = -1;
      }

      PL_laststype   = PL_op->op_type;  /* this was for our OP */

      /* Here, we cut early when stat() returned no values
       * In such a case, we set the statcache, but do not call
       * the real op (CALL_REAL_OP)
      */
      if ( size < 0 )
        RETURN;

      PUSHs( MUTABLE_SV( PL_defgv ) ); /* add *_ to the stack */

      /* probably not real necesseary, make warning messages nicer */
      if ( previous_stack && SvPOK(previous_stack) )
        sv_setpv(PL_statname, SvPV_nolen(previous_stack) );

    return CALL_REAL_OP();
  }

}

/*
*  extract from https://perldoc.perl.org/functions/-X.html
*
*  -r  File is readable by effective uid/gid.
*  -w  File is writable by effective uid/gid.
*  -x  File is executable by effective uid/gid.
*  -o  File is owned by effective uid.
*  -R  File is readable by real uid/gid.
*  -W  File is writable by real uid/gid.
*  -X  File is executable by real uid/gid.
*  -O  File is owned by real uid.
*  -e  File exists.
*  -z  File has zero size (is empty).
*  -s  File has nonzero size (returns size in bytes).
*  -f  File is a plain file.
*  -d  File is a directory.
*  -l  File is a symbolic link (false if symlinks aren't
*      supported by the file system).
*  -p  File is a named pipe (FIFO), or Filehandle is a pipe.
*  -S  File is a socket.
*  -b  File is a block special file.
*  -c  File is a character special file.
*  -t  Filehandle is opened to a tty.
*  -u  File has setuid bit set.
*  -g  File has setgid bit set.
*  -k  File has sticky bit set.
*  -T  File is an ASCII or UTF-8 text file (heuristic guess).
*  -B  File is a "binary" file (opposite of -T).
*  -M  Script start time minus file modification time, in days.
*  -A  Same for access time.
*  -C  Same for inode change time
*/

MODULE = Overload__FileCheck       PACKAGE = Overload::FileCheck

SV*
mock_op(optype)
     SV* optype;
 ALIAS:
      Overload::FileCheck::_xs_mock_op               = 1
      Overload::FileCheck::_xs_unmock_op             = 2
 CODE:
 {
     /* mylogger = INT2PTR(MyLogger*, SvIV(SvRV(self))); */
      int opid = 0;

      if ( ! SvIOK(optype) )
        croak("first argument to _xs_mock_op / _xs_unmock_op must be one integer");

      opid = SvIV( optype );
      if ( !opid || opid < 0 || opid >= OP_MAX )
          croak( "Invalid opid value %d", opid );

      switch (ix) {
         case 1: /* _xs_mock_op */
              gl_overload_ft->op[opid].is_mocked = 1;
          break;
         case 2: /* _xs_unmock_op */
              gl_overload_ft->op[opid].is_mocked = 0;
          break;
          default:
              croak("Unsupported function at index %d", ix);
              XSRETURN_EMPTY;
      }

      XSRETURN_EMPTY;
 }
 OUTPUT:
     RETVAL


SV*
get_basetime()
CODE:
  RETVAL = newSViv(PL_basetime);
OUTPUT:
  RETVAL


BOOT:
    if (!gl_overload_ft) {
         HV *stash;
         SV *sv;
         int ix = 0;

         Newxz( gl_overload_ft, 1, OverloadFTOps);

         stash = gv_stashpvn("Overload::FileCheck", 19, TRUE);

         newCONSTSUB(stash, "_loaded", newSViv(1) );

         /* provide constants to standardize return values from mocked functions */
         newCONSTSUB(stash, "CHECK_IS_TRUE",         &PL_sv_yes );   /* could use newSViv(1) or &PL_sv_yes */
         newCONSTSUB(stash, "CHECK_IS_FALSE",        &PL_sv_no );    /* could use newSViv(0) or &PL_sv_no  */
         newCONSTSUB(stash, "CHECK_IS_NULL",         &PL_sv_undef ); /* FIXME: need to handle this as a valid answer */
         newCONSTSUB(stash, "FALLBACK_TO_REAL_OP",  newSVnv(-1) );

         /* provide constants to add entry in a fake stat array */

         newCONSTSUB(stash, "ST_DEV",                newSViv(ix++) );
         newCONSTSUB(stash, "ST_INO",                newSViv(ix++) );
         newCONSTSUB(stash, "ST_MODE",               newSViv(ix++) );
         newCONSTSUB(stash, "ST_NLINK",              newSViv(ix++) );
         newCONSTSUB(stash, "ST_UID",                newSViv(ix++) );
         newCONSTSUB(stash, "ST_GID",                newSViv(ix++) );
         newCONSTSUB(stash, "ST_RDEV",               newSViv(ix++) );
         newCONSTSUB(stash, "ST_SIZE",               newSViv(ix++) );
         newCONSTSUB(stash, "ST_ATIME",              newSViv(ix++) );
         newCONSTSUB(stash, "ST_MTIME",              newSViv(ix++) );
         newCONSTSUB(stash, "ST_CTIME",              newSViv(ix++) );
         newCONSTSUB(stash, "ST_BLKSIZE",            newSViv(ix++) );
         newCONSTSUB(stash, "ST_BLOCKS",             newSViv(ix++) );
         assert(STAT_T_MAX == ix);
         newCONSTSUB(stash, "STAT_T_MAX",            newSViv(STAT_T_MAX) );

         /* copy the original OP then plug our own custom OP function */
         /* view pp_sys.c for complete list */

         /* PP(pp_ftrread) - yes/no/undef */
         INIT_FILECHECK_MOCK( "OP_FTRREAD",   OP_FTRREAD,   &Perl_pp_overload_ft_yes_no);   /* -R */
         INIT_FILECHECK_MOCK( "OP_FTRWRITE",  OP_FTRWRITE,  &Perl_pp_overload_ft_yes_no);   /* -W */
         INIT_FILECHECK_MOCK( "OP_FTREXEC",   OP_FTREXEC,   &Perl_pp_overload_ft_yes_no);   /* -X */
         INIT_FILECHECK_MOCK( "OP_FTEREAD",   OP_FTEREAD,   &Perl_pp_overload_ft_yes_no);   /* -r */
         INIT_FILECHECK_MOCK( "OP_FTEWRITE",  OP_FTEWRITE,  &Perl_pp_overload_ft_yes_no);   /* -w */
         INIT_FILECHECK_MOCK( "OP_FTEEXEC",   OP_FTEEXEC,   &Perl_pp_overload_ft_yes_no);   /* -x */

         /* PP(pp_ftis) - yes/undef/true/false */
         INIT_FILECHECK_MOCK( "OP_FTIS",      OP_FTIS,      &Perl_pp_overload_ft_yes_no);   /* -e */
         INIT_FILECHECK_MOCK( "OP_FTSIZE",    OP_FTSIZE,    &Perl_pp_overload_ft_int);   /* -s */
         INIT_FILECHECK_MOCK( "OP_FTMTIME",   OP_FTMTIME,   &Perl_pp_overload_ft_nv);   /* -M */
         INIT_FILECHECK_MOCK( "OP_FTCTIME",   OP_FTCTIME,   &Perl_pp_overload_ft_nv);   /* -C */
         INIT_FILECHECK_MOCK( "OP_FTATIME",   OP_FTATIME,   &Perl_pp_overload_ft_nv);   /* -A */

         /* PP(pp_ftrowned) yes/no/undef */
         INIT_FILECHECK_MOCK( "OP_FTROWNED",  OP_FTROWNED,  &Perl_pp_overload_ft_yes_no);   /* -O */
         INIT_FILECHECK_MOCK( "OP_FTEOWNED",  OP_FTEOWNED,  &Perl_pp_overload_ft_yes_no);   /* -o */
         INIT_FILECHECK_MOCK( "OP_FTZERO",    OP_FTZERO,    &Perl_pp_overload_ft_yes_no);   /* -z */
         INIT_FILECHECK_MOCK( "OP_FTSOCK",    OP_FTSOCK,    &Perl_pp_overload_ft_yes_no);   /* -S */
         INIT_FILECHECK_MOCK( "OP_FTCHR",     OP_FTCHR,     &Perl_pp_overload_ft_yes_no);   /* -c */
         INIT_FILECHECK_MOCK( "OP_FTBLK",     OP_FTBLK,     &Perl_pp_overload_ft_yes_no);   /* -b */
         INIT_FILECHECK_MOCK( "OP_FTFILE",    OP_FTFILE,    &Perl_pp_overload_ft_yes_no);   /* -f */
         INIT_FILECHECK_MOCK( "OP_FTDIR",     OP_FTDIR,     &Perl_pp_overload_ft_yes_no);   /* -d */
         INIT_FILECHECK_MOCK( "OP_FTPIPE",    OP_FTPIPE,    &Perl_pp_overload_ft_yes_no);   /* -p */
         INIT_FILECHECK_MOCK( "OP_FTSUID",    OP_FTSUID,    &Perl_pp_overload_ft_yes_no);   /* -u */
         INIT_FILECHECK_MOCK( "OP_FTSGID",    OP_FTSGID,    &Perl_pp_overload_ft_yes_no);   /* -g */
         INIT_FILECHECK_MOCK( "OP_FTSVTX",    OP_FTSVTX,    &Perl_pp_overload_ft_yes_no);   /* -k */

         /* PP(pp_ftlink) - yes/no/undef */
         INIT_FILECHECK_MOCK( "OP_FTLINK",    OP_FTLINK,    &Perl_pp_overload_ft_yes_no);   /* -l */

         /* PP(pp_fttty) - yes/no/undef */
         INIT_FILECHECK_MOCK( "OP_FTTTY",     OP_FTTTY,     &Perl_pp_overload_ft_yes_no);   /* -t */

        /* PP(pp_fttext) - yes/no/undef */
         INIT_FILECHECK_MOCK( "OP_FTTEXT",    OP_FTTEXT,    &Perl_pp_overload_ft_yes_no);   /* -T */
         INIT_FILECHECK_MOCK( "OP_FTBINARY",  OP_FTBINARY,  &Perl_pp_overload_ft_yes_no);   /* -B */

         /* PP(pp_stat) also used for: pp_lstat() */
         INIT_FILECHECK_MOCK( "OP_STAT",      OP_STAT,      &Perl_pp_overload_stat);        /* stat */
         INIT_FILECHECK_MOCK( "OP_LSTAT",     OP_LSTAT,     &Perl_pp_overload_stat);        /* lstat */

    }