#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include <sys/utsname.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#undef Fflush
#undef Mkdir
#undef Stat
#include <rpm/rpmlib.h>
#include <rpm/header.h>

#define HDFLAGS_NAME          0x00000001
#define HDFLAGS_VERSION       0x00000002
#define HDFLAGS_RELEASE       0x00000004
#define HDFLAGS_ARCH          0x00000008
#define HDFLAGS_GROUP         0x00000010
#define HDFLAGS_SIZE          0x00000020
#define HDFLAGS_SENSE         0x00080000
#define HDFLAGS_REQUIRES      0x00100000
#define HDFLAGS_PROVIDES      0x00200000
#define HDFLAGS_OBSOLETES     0x00400000
#define HDFLAGS_CONFLICTS     0x00800000
#define HDFLAGS_FILES         0x01000000
#define HDFLAGS_CONFFILES     0x02000000


char *get_name(Header header, int_32 tag) {
  int_32 type, count;
  char *name;

  headerGetEntry(header, tag, &type, (void **) &name, &count);
  return name;
}

int get_int(Header header, int_32 tag) {
  int_32 type, count;
  int *i;

  headerGetEntry(header, tag, &type, (void **) &i, &count);
  return *i;
}

int get_bflag(AV* flag) {
  int bflag = 0;
  int flag_len;
  SV** ret;
  STRLEN len;
  char* str;
  int i;

  flag_len = av_len(flag);
  for (i = 0; i <= flag_len; ++i) {
    ret = av_fetch(flag, i, 0); if (!ret) continue;
    str = SvPV(*ret, len);

    switch (len) {
    case 4:
      if (!strncmp(str, "name", 4))      bflag |= HDFLAGS_NAME;
      else if (!strncmp(str, "arch", 4)) bflag |= HDFLAGS_ARCH;
      else if (!strncmp(str, "size", 4)) bflag |= HDFLAGS_SIZE;
      break;
    case 5:
      if (!strncmp(str, "group", 5))      bflag |= HDFLAGS_GROUP;
      else if (!strncmp(str, "sense", 5)) bflag |= HDFLAGS_SENSE;
      else if (!strncmp(str, "files", 5)) bflag |= HDFLAGS_FILES;
      break;
    case 7:
      if (!strncmp(str, "version", 7))      bflag |= HDFLAGS_VERSION;
      else if (!strncmp(str, "release", 7)) bflag |= HDFLAGS_RELEASE;
      break;
    case 8:
      if (!strncmp(str, "requires", 8))      bflag |= HDFLAGS_REQUIRES;
      else if (!strncmp(str, "provides", 8)) bflag |= HDFLAGS_PROVIDES;
      break;
    case 9:
      if (!strncmp(str, "obsoletes", 9))      bflag |= HDFLAGS_OBSOLETES;
      else if (!strncmp(str, "conflicts", 9)) bflag |= HDFLAGS_CONFLICTS;
      else if (!strncmp(str, "conffiles", 9)) bflag |= HDFLAGS_CONFFILES;
      break;
    }
  }
  bflag |= HDFLAGS_NAME; /* this one should always be used */

  return bflag;
}

SV *get_table_sense(Header header, int_32 tag_name, int_32 tag_flags, int_32 tag_version, HV* iprovides) {
  AV* table_sense;
  int_32 type, count;
  char **list;
  int_32 *flags;
  char **list_evr;
  int i;

  char buff[4096];
  char *p;
  int len;

  headerGetEntry(header, tag_name, &type, (void **) &list, &count);
  if (tag_flags) headerGetEntry(header, tag_flags, &type, (void **) &flags, &count);
  else flags = 0;
  if (tag_version) headerGetEntry(header, tag_version, &type, (void **) &list_evr, &count);
  else list_evr = 0;

  if (list) {
    table_sense = newAV();
    if (!table_sense) return &PL_sv_undef;

    for(i = 0; i < count; i++) {
      len = strlen(list[i]); if (len >= sizeof(buff)) continue;
      memcpy(p = buff, list[i], len + 1); p+= len;

      if (flags) {
	if (flags[i] & RPMSENSE_PREREQ) {
	  if (p - buff + 3 >= sizeof(buff)) continue;
	  memcpy(p, "[*]", 4); p += 3;
	}
	if (list_evr) {
	  if (list_evr[i]) {
	    len = strlen(list_evr[i]);
	    if (len > 0) {
	      if (p - buff + 6 + len >= sizeof(buff)) continue;
	      *p++ = '[';
	      if (flags[i] & RPMSENSE_LESS) *p++ = '<';
	      if (flags[i] & RPMSENSE_GREATER) *p++ = '>';
	      if (flags[i] & RPMSENSE_EQUAL) *p++ = '=';
	      if ((flags[i] & (RPMSENSE_LESS|RPMSENSE_EQUAL|RPMSENSE_GREATER)) == RPMSENSE_EQUAL) *p++ = '=';
	      *p++ = ' ';
	      memcpy(p, list_evr[i], len); p+= len;
	      *p++ = ']';
	      *p = '\0';
	    }
	  }
	}
      }

      /* for getting provides about required files */
      if (iprovides && buff[0] == '/')
	hv_fetch(iprovides, buff, p - buff, 1);

      av_push(table_sense, newSVpv(buff, p - buff));
    }

    return newRV_noinc((SV*)table_sense);
  }
  return &PL_sv_undef;
}

HV* get_info(Header header, int bflag, HV* provides) {
  int_32 type, count;
  char **list;
  int_32 *flags;
  SV** ret;
  STRLEN len;
  char* str;
  int i;
  SV* sv_name = newSVpv(get_name(header, RPMTAG_NAME), 0);
  HV* header_info = newHV();

  /* correct bflag according to provides hash else not really usefull */
  if (provides) bflag |= HDFLAGS_REQUIRES;

  hv_store(header_info, "name", 4, sv_name, 0);
  if (bflag & HDFLAGS_VERSION)
    hv_store(header_info, "version", 7, newSVpv(get_name(header, RPMTAG_VERSION), 0), 0);
  if (bflag & HDFLAGS_RELEASE)
    hv_store(header_info, "release", 7, newSVpv(get_name(header, RPMTAG_RELEASE), 0), 0);
  if (bflag & HDFLAGS_ARCH)
    hv_store(header_info, "arch", 4, newSVpv(get_name(header, RPMTAG_ARCH), 0), 0);
  if (bflag & HDFLAGS_GROUP)
    hv_store(header_info, "group", 5, newSVpv(get_name(header, RPMTAG_GROUP), 0), 0);
  if (bflag & HDFLAGS_SIZE)
    hv_store(header_info, "size", 4, newSViv(get_int(header, RPMTAG_SIZE)), 0);
  if (bflag & HDFLAGS_REQUIRES)
    hv_store(header_info, "requires", 8, get_table_sense(header,                 RPMTAG_REQUIRENAME,
							 bflag & HDFLAGS_SENSE ? RPMTAG_REQUIREFLAGS : 0,
							 bflag & HDFLAGS_SENSE ? RPMTAG_REQUIREVERSION : 0, provides), 0);
  if (bflag & HDFLAGS_PROVIDES)
    hv_store(header_info, "provides", 8, get_table_sense(header,                 RPMTAG_PROVIDENAME,
							 bflag & HDFLAGS_SENSE ? RPMTAG_PROVIDEFLAGS : 0,
							 bflag & HDFLAGS_SENSE ? RPMTAG_PROVIDEVERSION : 0, 0), 0);
  if (bflag & HDFLAGS_OBSOLETES)
    hv_store(header_info, "obsoletes", 9, get_table_sense(header,                 RPMTAG_OBSOLETENAME,
							  bflag & HDFLAGS_SENSE ? RPMTAG_OBSOLETEFLAGS : 0,
							  bflag & HDFLAGS_SENSE ? RPMTAG_OBSOLETEVERSION : 0, 0), 0);
  if (bflag & HDFLAGS_CONFLICTS)
    hv_store(header_info, "conflicts", 9, get_table_sense(header,                 RPMTAG_CONFLICTNAME,
							  bflag & HDFLAGS_SENSE ? RPMTAG_CONFLICTFLAGS : 0,
							  bflag & HDFLAGS_SENSE ? RPMTAG_CONFLICTVERSION : 0, 0), 0);
  if (provides || (bflag & (HDFLAGS_FILES | HDFLAGS_CONFFILES))) {
    /* at this point, there is a need to parse all files to update provides of needed files,
       or to store them. */
    AV* table_files = bflag & HDFLAGS_FILES ? newAV() : 0;
    AV* table_conffiles = bflag & HDFLAGS_CONFFILES ? newAV() : 0;
    char ** baseNames, ** dirNames;
    int_32 * dirIndexes;

    headerGetEntry(header, RPMTAG_FILEFLAGS, &type, (void **) &flags, &count);

    headerGetEntry(header, RPMTAG_OLDFILENAMES, &type, (void **) &list, &count);
    if (list) {
      for (i = 0; i < count; i++) {
	SV** isv;

	len = strlen(list[i]);

	if (provides && (isv = hv_fetch(provides, list[i], len, 0)) != 0) {
	  if (!SvROK(*isv) || SvTYPE(SvRV(*isv)) != SVt_PVAV) {
	    SV* choice_table = (SV*)newAV();
	    SvREFCNT_dec(*isv); /* drop the old as we are changing it */
	    *isv = choice_table ? newRV_noinc(choice_table) : &PL_sv_undef;
	  }
	  if (*isv != &PL_sv_undef) av_push((AV*)SvRV(*isv), SvREFCNT_inc(sv_name));
	}
	/* if (provides && hv_exists(provides, list[i], len))
	   hv_store(provides, list[i], len, newSVpv(name, 0), 0); */
	if (table_files)
	  av_push(table_files, newSVpv(list[i], len));
	if (table_conffiles && flags && flags[i] & RPMFILE_CONFIG)
	  av_push(table_conffiles, newSVpv(list[i], len));
      }
    }

    headerGetEntry(header, RPMTAG_BASENAMES, &type, (void **) &baseNames, &count);
    headerGetEntry(header, RPMTAG_DIRINDEXES, &type, (void **) &dirIndexes, NULL);
    headerGetEntry(header, RPMTAG_DIRNAMES, &type, (void **) &dirNames, NULL);
    if (baseNames && dirNames && dirIndexes) {
      char buff[4096];
      char *p;

      for(i = 0; i < count; i++) {
	SV** isv;

	len = strlen(dirNames[dirIndexes[i]]);
	if (len >= sizeof(buff)) continue;
	memcpy(p = buff, dirNames[dirIndexes[i]], len + 1); p += len;
	len = strlen(baseNames[i]);
	if (p - buff + len >= sizeof(buff)) continue;
	memcpy(p, baseNames[i], len + 1); p += len;

	if (provides && (isv = hv_fetch(provides, buff, p - buff, 0)) != 0) {
	  if (!SvROK(*isv) || SvTYPE(SvRV(*isv)) != SVt_PVAV) {
	    SV* choice_table = (SV*)newAV();
	    SvREFCNT_dec(*isv); /* drop the old as we are changing it */
	    *isv = choice_table ? newRV_noinc(choice_table) : &PL_sv_undef;
	  }
	  if (*isv != &PL_sv_undef) av_push((AV*)SvRV(*isv), SvREFCNT_inc(sv_name));
	}
	if (table_files)
	  av_push(table_files, newSVpv(buff, p - buff));
	if (table_conffiles && flags && flags[i] & RPMFILE_CONFIG)
	  av_push(table_conffiles, newSVpv(buff, p - buff));
      }
    }

    if (table_files)
      hv_store(header_info, "files", 5, newRV_noinc((SV*)table_files), 0);
    if (table_conffiles)
      hv_store(header_info, "conffiles", 9, newRV_noinc((SV*)table_conffiles), 0);
  }
  if (provides) {
    /* we have to examine provides to update the hash here. */
    headerGetEntry(header, RPMTAG_PROVIDENAME, &type, (void **) &list, &count);

    if (list) {
      for (i = 0; i < count; i++) {
	SV** isv;

	len = strlen(list[i]);

	isv = hv_fetch(provides, list[i], len, 1);
	if (!SvROK(*isv) || SvTYPE(SvRV(*isv)) != SVt_PVAV) {
	  SV* choice_table = (SV*)newAV();
	  SvREFCNT_dec(*isv); /* drop the old as we are changing it */
	  *isv = choice_table ? newRV_noinc(choice_table) : &PL_sv_undef;
	}
	if (*isv != &PL_sv_undef) av_push((AV*)SvRV(*isv), SvREFCNT_inc(sv_name));
      }
    }
  }

  return header_info;
}

void callback_empty(void) {}

MODULE = rpmtools			PACKAGE = rpmtools


char *
arch()
  CODE:
  struct utsname u;
  if (uname(&u) == 0) RETVAL = u.machine; else RETVAL = NULL;
  OUTPUT:
  RETVAL

void*
db_open(prefix)
  char *prefix
  CODE:
  rpmdb db;
  rpmErrorCallBackType old_cb;
  old_cb = rpmErrorSetCallback(callback_empty);
  rpmSetVerbosity(RPMMESS_FATALERROR);
  RETVAL = rpmReadConfigFiles(NULL, NULL) == 0 && rpmdbOpen(prefix, &db, O_RDONLY, 0644) == 0 ? db : NULL;
  rpmErrorSetCallback(old_cb);
  rpmSetVerbosity(RPMMESS_NORMAL);
  OUTPUT:
  RETVAL

void
db_close(db)
  void *db
  CODE:
  rpmdbClose((rpmdb)db);

void
_exit(code)
  int code

int
db_traverse_tag(db, tag, names, flags, callback)
  void *db
  char *tag
  SV *names
  SV *flags
  SV *callback
  PREINIT:
  int count = 0;
  CODE:
  if (SvROK(flags) && SvTYPE(SvRV(flags)) == SVt_PVAV &&
      SvROK(names) && SvTYPE(SvRV(names)) == SVt_PVAV) {
    AV* flags_av = (AV*)SvRV(flags);
    AV* names_av = (AV*)SvRV(names);
    int bflag = get_bflag(flags_av);
    int len = av_len(names_av);
    HV* info;
    SV** isv;
    int i, rpmtag;
    STRLEN str_len;
    char *name;
    Header header;
    rpmdbMatchIterator mi;

    if (!strcmp(tag, "name"))
      rpmtag = RPMTAG_NAME;
    else if (!strcmp(tag, "whatprovides"))
      rpmtag = RPMTAG_PROVIDENAME;
    else if (!strcmp(tag, "whatrequires"))
      rpmtag = RPMTAG_REQUIRENAME;
    else if (!strcmp(tag, "group"))
      rpmtag = RPMTAG_GROUP;
    else if (!strcmp(tag, "triggeredby"))
      rpmtag = RPMTAG_BASENAMES;
    else if (!strcmp(tag, "path"))
      rpmtag = RPMTAG_BASENAMES;
    else {
      croak("unknown tag");
      len = 0;
    }

    for (i = 0; i <= len; ++i) {
      isv = av_fetch(names_av, i, 0);
      name = SvPV(*isv, str_len);
      mi = rpmdbInitIterator((rpmdb)db, rpmtag, name, str_len);
      while (header = rpmdbNextIterator(mi)) {
	count++;
	info = get_info(header, bflag, NULL);

	if (info != 0 && callback != &PL_sv_undef && SvROK(callback)) {
	  dSP;
	  ENTER;
	  SAVETMPS;
	  PUSHMARK(SP);
	  XPUSHs(sv_2mortal(newRV_noinc((SV*)info)));
	  XPUSHs(sv_2mortal(newSVpv(name, str_len)));
	  PUTBACK;
	  call_sv(callback, G_DISCARD | G_SCALAR);
	  FREETMPS;
	  LEAVE;
	}
      }
      rpmdbFreeIterator(mi);
    }
  } else croak("bad arguments list");
  RETVAL = count;
  OUTPUT:
  RETVAL

int
db_traverse(db, flags, callback)
  void *db
  SV *flags
  SV *callback
  PREINIT:
  int count = 0;
  CODE:
  if (SvROK(flags) && SvTYPE(SvRV(flags)) == SVt_PVAV) {
    AV* flags_av = (AV*)SvRV(flags);
    int bflag = get_bflag(flags_av);
    HV* info;
    Header header;
    rpmdbMatchIterator mi;

    mi = rpmdbInitIterator(db, RPMDBI_PACKAGES, NULL, 0);
    while (header = rpmdbNextIterator(mi)) {
      info = get_info(header, bflag, NULL);

      if (info != 0 && callback != &PL_sv_undef && SvROK(callback)) {
	dSP;
	ENTER;
	SAVETMPS;
	PUSHMARK(SP);
	XPUSHs(sv_2mortal(newRV_noinc((SV*)info)));
	PUTBACK;
	call_sv(callback, G_DISCARD | G_SCALAR);
	FREETMPS;
	LEAVE;
      }
      ++count;
    }
    rpmdbFreeIterator(mi);
  } else croak("bad arguments list");
  RETVAL = count;
  OUTPUT:
  RETVAL

void
_parse_(fileno_or_rpmfile, flag, info, ...)
  SV* fileno_or_rpmfile
  SV* flag
  SV* info
  PREINIT:
  SV* provides = &PL_sv_undef;
  PPCODE:
  if (items > 3)
    provides = ST(3);
  if (SvROK(flag) && SvROK(info) && (provides == &PL_sv_undef || SvROK(provides))) {
    FD_t fd;
    int fd_is_hdlist;
    Header header;

    int bflag;
    AV* iflag;
    HV* iinfo;
    HV* iprovides;
    SV** ret;
    I32 flag_len;
    STRLEN len;
    char* str;
    int i;

    if (SvIOK(fileno_or_rpmfile)) {
      fd = fdDup(SvIV(fileno_or_rpmfile));
      fd_is_hdlist = 1;
    } else {
      fd = fdOpen(SvPV_nolen(fileno_or_rpmfile), O_RDONLY, 0666);
      if (fd < 0) croak("unable to open rpm file %s", SvPV_nolen(fileno_or_rpmfile));
      fd_is_hdlist = 0;
    }

    if ((SvTYPE(SvRV(flag)) != SVt_PVAV) ||
	(SvTYPE(SvRV(info)) != SVt_PVHV) ||
	provides != &PL_sv_undef && (SvTYPE(SvRV(provides)) != SVt_PVHV))
      croak("bad arguments list");

    iflag = (AV*)SvRV(flag);
    iinfo = (HV*)SvRV(info);
    iprovides = (HV*)(provides != &PL_sv_undef ? SvRV(provides) : 0);

    /* examine flag and set up iflag, which is faster to fecth out */
    bflag = get_bflag(iflag);

    /* start the big loop,
       parse all header from fileno, then extract information to store into iinfo and iprovides. */
    while (fd_is_hdlist >= 0 ? (fd_is_hdlist > 0 ?
				((header=headerRead(fd, HEADER_MAGIC_YES)) != 0) :
				((fd_is_hdlist = -1), rpmReadPackageHeader(fd, &header, &i, NULL, NULL) == 0)) : 0) {
      char *name = get_name(header, RPMTAG_NAME);
      char *version = get_name(header, RPMTAG_VERSION);
      char *release = get_name(header, RPMTAG_RELEASE);
      char *fullname = (char*)alloca(strlen(name)+strlen(version)+strlen(release)+3);
      STRLEN fullname_len = sprintf(fullname, "%s-%s-%s", name, version, release);
      HV* header_info = get_info(header, bflag, iprovides);

      /* once the hash header_info is built, store a reference to it
	 in iinfo.
	 note sv_name is not incremented here, it has the default value of before. */
      hv_store(iinfo, name, strlen(name), newRV_noinc((SV*)header_info), 0);

      /* return fullname on stack */
      EXTEND(SP, 1);
      PUSHs(sv_2mortal(newSVpv(fullname, fullname_len)));

      /* dispose of some memory */
      headerFree(header);
    }
    fdClose(fd);
  } else croak("bad arguments list");