#define PROG_NAME "booklets_ag"
#define PROG_DESC "Enumerate exam booklets for multiple topics."
#define PROG_VERS "2013-12-15"

#define booklets_ag_C_COPYRIGHT \
  "Copyright © 2013 by the State University of Campinas (UNICAMP)"
/* Last edited on 2018-01-19 19:01:48 by stolfilocal */
    
#define PROG_HELP \
  "  " PROG_NAME " \\\n" \
  "    -numTopics {NT} \\\n" \
  "    -numBlocks {NB} \\\n" \
  "    -minShared {KMIN} \\\n" \
  "    -maxShared {KMAX} \\\n" \
  "    " argparser_help_info_HELP " \\\n" \
  "    > {OUTFILE}"

#define PROG_INFO \
  "NAME\n" \
  "  " PROG_NAME " - " PROG_DESC "\n" \
  "\n" \
  "SYNOPSIS\n" \
  PROG_HELP "\n" \
  "\n" \
  "DESCRIPTION\n" \
  "  The program enumerates exam booklets for an exam over {NT} topics, by assembling blocks of questions.  It assumes that, for each topic, there are {NB} alternative blocks of questions, each of them sufficient and necessary to cover that topic.  Each booklet must consist of {NT} question blocks, one for each topic.\n" \
  "\n" \
  "  The program finds a set of booklets, as large as it can, such that any two booklets have at most {K} question blocks in common.  These booklets are printed to standard output.  Each line has a count of the booklet (starting from 1) followed by {NT} fields describing one booklet.  Each field has one letter ('a', 'b', 'c', ..., 'z') identifying the topic, followed by an integer between 1 and {NB} identifying the block of questions to be included for that topic.  For example:\n" \
  "\n" \
  "    ...\n" \
  "         251 223411  a02 b03 c14 d05 e02\n" \
  "         252 125616  a05 b03 c09 d14 e03\n" \
  "    ...\n" \
  "\n" \
  "  This output means that booklet #251 of the selected set, which is #223411 in the list of all possible booklets, uses question block 02 for topic 'a', block 03 for topic 'b', block 14 for topic 'c', etc..  Booklet #252 uses question block 05 for topic 'a',  block 03 for topic 'b', block 09 for topic 'c', etc..  Note that these two booklets have one block of questions in common (block 03 for topic 'b')." \
  "\n" \
  "  The output starts with a set of {NB} booklets that have at most {KMIN} question blocks in common.  Then there are a set of booklets that have at most {KMIN+1} block in common with each other or with the booklets in the initial set.  Then more booklets that have at most {KMIN+2} blocks in common with each other or with the previous ones, and so on; until a set of booklets that have at most {KMAX} blocks in common among themselves and with previous booklets.\n" \
  "\n" \
  "OPTIONS\n" \
  "  -numTopics {NT}\n" \
  "    This mandatory argument is the number {NT} of topics in the exam.\n" \
  "\n" \
  "  -numBlocks {NB}\n" \
  "    This mandatory argument is the number {NB} of question blocks available for each topic.\n" \
  "\n" \
  "  -minShared {KMIN}\n" \
  "    This mandatory argument is the max number {KMIN} of question blocks that can be shared by two booklets in the initial set of boolets.\n" \
  "\n" \
  "  -maxShared {KMAX}\n" \
  "    This mandatory argument is the max number {KMAX} of question blocks that can be shared by two booklets in the final set of booklets.\n" \
  "\n" \
  "DOCUMENTATION OPTIONS\n" \
  argparser_help_info_HELP_INFO "\n" \
  "\n" \
  "SEE ALSO\n" \
  "  gawk(1).\n" \
  "\n" \
  "AUTHOR\n" \
  "  Created 2018-01-12 by Jorge Stolfi, IC-UNICAMP.\n" \
  "\n" \
  "MODIFICATION HISTORY\n" \
  "  2018-01-12 J.Stolfi: created.\n" \
  "  2018-01-19 J.Stolfi: added \"-minShared\" option.\n" \
  "\n" \
  "WARRANTY\n" \
  argparser_help_info_NO_WARRANTY "\n" \
  "\n" \
  "RIGHTS\n" \
  "  " booklets_ag_C_COPYRIGHT ".\n" \
  "\n" \
  argparser_help_info_STANDARD_RIGHTS

#define stringify(x) strngf(x)
#define strngf(x) #x

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
#include <math.h>

#include <jsmath.h>
#include <affirm.h>
#include <argparser.h>
#include <vec.h>
#include <bool.h>

typedef struct boag_options_t
  { int32_t numTopics;           /* Number of topics in the exam. */
    int32_t numBlocks;           /* Number of question blocks for each topic. */
    int32_t maxBooklets;         /* Max number of booklets to list. */
    int32_t minShared;           /* Max number of question blocks shared between two booklets in first set. */
    int32_t maxShared;           /* Max number of question blocks shared between two booklets in last set. */
  } boag_options_t;
  /* Arguments from command line. */
     
boag_options_t *boag_parse_options(int32_t argc, char **argv);

#define boag_vertices_MAX ((uint64_t)1000000000)
  /* Max number of vertices (possible booklets, {NB^NT}. */

/* 
  EXAM PARAMETERS
  
  In all procedures below, {NT} is the number of topics in the exam, {NB} is the number of 
  question blocks per topic, and {NV = NB^NT} is the total number of possible booklets.
  
  BOOKLET REPRESENTATION 
  
  A booklet can be represented as a /vector/ {qv[0..NT-1]} where each element is a 
  question block ID in {1..NB}; or as an /index/ {ix} in {0..NV-1}.  The correspondence
  is defined by {boag_booklet_index_from_vector} and {boag_booklet_index_to_vector}. 
  
  BOOKLET SETS
  
  In all procedures below, the boolean {sel[ix]} is true iff booklet with index {ix}
  has been selected for inclusion in the exam.  When defined, the boolean {val[ix]} is true
  iff the booklet with index {ix} is still a valid candidate for selection. */

bool_t boag_check_max_vertices(int32_t NT, int32_t NB);
  /* Returns true iff {NB^NT} does not exceed {boag_vertices_MAX}.  Avoids overflow. */

void boag_booklet_index_to_vector(int32_t NT, int32_t NB, int64_t NV, int64_t ix, int32_t qv[]);
  /* Converts a booklet index {ix} to the corresponding vector of question block IDs {qv[0..NT-1]}. */
  
void boag_booklet_vector_to_index(int32_t NT, int32_t NB, int64_t NV, int32_t qv[], int64_t *ixP);
  /* Stores into {*ixP} the booklet index corresponding to the vector {qv[0..NT-1]}. */
  
void boag_find_booklets(int32_t NT, int32_t NB, int64_t NV, bool_t sel[], int64_t *nselP, int32_t k, bool_t val[]);
  /* Enumerates booklets with
    at most {k} shared blocks between any two booklets. Assumes {NV = NB^NT}.
    
    Assumes that {sel[0..NV-1]} already contains a
    set of booklets with at most {k} (usually less than {k}) pairwise
    intersections. Uses the vector {val[0..NV-1]}. */

void boag_initialize_valid_set
  ( int32_t NT, 
    int32_t NB, 
    int64_t NV, 
    bool_t sel[], 
    bool_t val[], 
    int64_t *nvalP, 
    int32_t k
  );
  /* Sets the valid set {val[0..NV-1]} to all booklets, minus those that are
    in the selected set {sel[0..NV-1]} and those with more than {k} 
    intersections with them. */
    
void boag_choose_valid_booklet(int32_t NT, int32_t NB, int64_t NV, bool_t val[], int64_t *ixP);
  /* Finds the first booklet in the set {val[0..NV-1]} starting at index {(*ixP)+1}.
    Returns the index of that booklet in {*ixP}. */
    
void boag_select_booklet(int32_t NT, int32_t NB, int64_t NV, int64_t ix, bool_t sel[], int64_t *nselP);
  /* Adds the booklet with index {ix} to the set {sel[0..NV-1]}, updatinng {*nselP}. */

void boag_write_booklet(FILE *wr, int32_t NT, int32_t NB, int64_t NV, int32_t qv[], int64_t ix, int64_t nsel);
  /* Prints to {wr} the booklet with vector {qv[0..NT-1]} and index {ix}, in the format 
    "{nsel-1} {ix} a{qv[0]} b{qv[1]} ...".  The field "{nsel-1}" is omitted if {nsel} is zero or negative. */

void boag_invalidate_incompat_booklets
  ( int32_t NT, 
    int32_t NB, 
    int64_t NV, 
    int32_t qvb[], 
    int64_t ixb, 
    int32_t k, 
    bool_t val[], 
    int64_t *nvalP
  );
  /* Removes from the valid set {val[0..NV-1]} the booklet {ixb} (which must be in the valid set) and every booklet {ixc} 
    that has more than {k} question blocks in common with {ixb}.  Updates {*nvalP}.  Assumes that 
    {qvb[0..NT-1]} is the vector of the booklet {ixb}. */
    
void boag_cycle_booklet(int32_t NT, int32_t NB, int32_t qv[]);
  /* Modifies the booklet vector {qv[0..NT-1]} by adding 1 to every element,
    and resetting to 1 if exceeds {NB}. */

void boag_debug_booklet(char *pre, int32_t NT, int32_t NB, int64_t NV, int32_t qv[], int64_t ix);
  /* Prints the booklet with index {ix} and vector {qv[0..NT-1]} to {stderr}, prefixed by {pre}. */

int32_t main (int32_t argc, char **argv)
  {
    boag_options_t *o = boag_parse_options(argc, argv);
    
    int32_t NT = o->numTopics;
    int32_t NB = o->numBlocks;
    
    demand(boag_check_max_vertices(NT, NB), "too many possible exam boolets");
    int64_t NV = ipow(NB, NT); /* Number of possible exam booklets. */
    fprintf(stderr, "total %ld possible booklets\n", NV);
    
    /* Element {sel[iv]} is true iff booklet with index {ix} was selected: */
    bool_t *sel = notnull(malloc(NV*sizeof(bool_t)), "no mem"); 
    for (int64_t ix = 0; ix < NV; ix++) { sel[ix] = FALSE; }
    int64_t nsel = 0; /* Number of true elements in {sel[0..NV-1]}. */
    
    /* Work vector for {boag_find_booklets}: */
    bool_t *val = notnull(malloc(NV*sizeof(bool_t)), "no mem"); 
    
    /* Find the of booklets, one {k}-set at a time: */
    int32_t KMIN = o->minShared;
    int32_t KMAX = o->maxShared;
    for (int32_t k = KMIN; k <= KMAX; k++)
      { /* Add booklets with at most {k} intersections: */
        boag_find_booklets(NT, NB, NV, sel, &nsel, k, val);
      }

    fprintf(stderr, "done.\n");
    
    return 0;
  }

bool_t boag_check_max_vertices(int32_t NT, int32_t NB)
  { 
    /* Compute {p = floor(boag_vertices_MAX/(NB^NT))}: */
    uint64_t p = boag_vertices_MAX; 
    for (uint32_t e = 0; e < NT; e++) { p = p/((uint64_t)NB); }
    return (p > 0);
  }

void boag_booklet_index_to_vector(int32_t NT, int32_t NB, int64_t NV, int64_t ix, int32_t qv[])
  { demand((ix >= 0) && (ix < NV), "invalid booklet index");
    for (int32_t it = 0; it < NT; it++)
      { qv[it] = (int32_t)((ix % NB) + 1);
        ix = ix / NB;
      }
    assert(ix == 0);
  }
        
void boag_booklet_vector_to_index(int32_t NT, int32_t NB, int64_t NV, int32_t qv[], int64_t *ixP)
  {
    int64_t ix = 0;
    for (int32_t it = NT-1; it >= 0; it--)
      { int32_t ib = qv[it];
        demand((ib >= 1) && (ib <= NB), "invalid booklet vector");
        ix = ix*NB + (ib - 1);
      }
    assert((ix >= 0) && (ix < NV));
    (*ixP) = ix;
  }
  
void boag_find_booklets(int32_t NT, int32_t NB, int64_t NV, bool_t sel[], int64_t *nselP, int32_t k, bool_t val[])
  { 
    bool_t debug = TRUE;
    
    int64_t nsel = (*nselP);
    char *cadd = (nsel == 0 ? "" : "additional ");
    fprintf(stderr, "--- finding %sbooklets with at most %d intersections ---\n", cadd, k);
    fprintf(stdout, "# booklets with at most %d intersections:\n", k);
    int64_t nval = -1;
    /* Set {val} to all booklets, minus those in {sel} and those with more than {k} intersections with them. */
    boag_initialize_valid_set(NT, NB, NV, sel, val, &nval, k);
    /* Greedy search for a maximal independent set in the incompatibility graph: */
    int64_t nsel_tot_ini = nsel; /* Save starting booklet count. */
    int32_t qvb[NT]; /* Candidate booklet vector. */
    int64_t ixb = -1; /* Last selected booklet for this {k}. */
    while (nval > 0)
      { fprintf(stdout, "\n");
        /* Select the first valid booklet {ixb,qvb}: */
        boag_choose_valid_booklet(NT, NB, NV, val, &ixb);
        boag_booklet_index_to_vector(NT, NB, NV, ixb, qvb);
        if (debug) { boag_debug_booklet("ixb = ", NT, NB, NV, qvb, ixb); }
        /* Try to exploit the symmetry under cyclic shifts of question blocks: */
        int64_t nsel_lot_ini = nsel; /* Save {nsel}.*/
        int64_t nval_lot_ini = nval; /* Save {nval}.*/
        for (int32_t j = 0; j < NB; j++)
          { boag_select_booklet(NT, NB, NV, ixb, sel, &nsel);
            boag_write_booklet(stdout, NT, NB, NV, qvb, ixb, nsel);
            boag_invalidate_incompat_booklets(NT, NB, NV, qvb, ixb, k, val, &nval);
            boag_cycle_booklet(NT, NB, qvb);
            boag_booklet_vector_to_index(NT, NB, NV, qvb, &ixb);
          }
        int64_t nsel_lot = nsel - nsel_lot_ini; /* Number selected in this symmetry class. */
        int64_t nrem_lot = nval_lot_ini - nval; /* Number excluded in this symmetry class. */
        fprintf(stderr, "selected %ld booklets", nsel_lot);
        fprintf(stderr, "  eliminated %ld\n", nrem_lot);
        if (nsel_lot != NB) { fprintf(stderr, "  !! warning - simmetry broken\n"); }
      }
    fprintf(stdout, "\n");
    int64_t nsel_tot = nsel - nsel_tot_ini; /* Number selected in this {k}. */
    fprintf(stderr, "found %ld %sbooklets (%ld total)", nsel_tot, cadd, nsel);
    fprintf(stderr, " with at most %d intersections.\n", k);
    (*nselP) = nsel;
  }
        
void boag_initialize_valid_set
  ( int32_t NT, 
    int32_t NB, 
    int64_t NV, 
    bool_t sel[], 
    bool_t val[], 
    int64_t *nvalP, 
    int32_t k
  )
  { for (int64_t ix = 0; ix < NV; ix++) { val[ix] = TRUE; }
    int64_t nval = NV;
    int32_t qv[NT]; /* Booklet vectot. */
    for (int64_t ix = 0; ix < NV; ix++) 
      { if (sel[ix])
          { if (val[ix]) { val[ix] = FALSE; nval--; }
            boag_booklet_index_to_vector(NT, NB, NV, ix, qv);
            boag_invalidate_incompat_booklets(NT, NB, NV, qv, ix, k, val, &nval);
          }
      }
    fprintf(stderr, "starting with %ld valid booklets\n", nval);
    *(nvalP) = nval;     
  }
  
void boag_choose_valid_booklet(int32_t NT, int32_t NB, int64_t NV, bool_t val[], int64_t *ixP)
  {
    int64_t ix = (*ixP) + 1;
    while (ix < NV)
      { if (val[ix]) { (*ixP) = ix; return; }
        ix++;
      }
    demand(FALSE, "no more valid booklets");
  }

void boag_select_booklet(int32_t NT, int32_t NB, int64_t NV, int64_t ix, bool_t sel[], int64_t *nselP)
  { 
    demand((ix >= 0) && (ix < NV), "invalid booklet index");
    demand(((*nselP) >= 0) && ((*nselP) < NV), "invalid selected booklet count");
    demand(!sel[ix], "booklet already selected");
    sel[ix] = TRUE;
    (*nselP)++;
  }
        
void boag_write_booklet(FILE *wr, int32_t NT, int32_t NB, int64_t NV, int32_t qv[], int64_t ix, int64_t nsel)
  { 
    demand((ix >= 0) && (ix < NV), "invalid booklet index");
    demand((nsel <= 0) || ((nsel >= 1) && (nsel <= NV)), "invalid selected booklet count");
    demand(NT <= 26, "too many topics");
    demand(NB <= 99, "too many question blocks");
    if (nsel > 0) { fprintf(wr, "%8ld ", nsel-1); }
    fprintf(wr, "%8ld ", ix);
    for (int32_t it = 0; it < NT; it++)
      { char ct = (char)('a' + it); 
        fprintf(wr, " %c%02d", ct, qv[it]);
      }
    fprintf(wr, "\n");
  }

void boag_invalidate_incompat_booklets
  ( int32_t NT, 
    int32_t NB, 
    int64_t NV, 
    int32_t qvb[], 
    int64_t ixb, 
    int32_t k, 
    bool_t val[], 
    int64_t *nvalP
  )
  {
    demand((ixb >= 0) && (ixb < NV), "invalid booklet index");
    demand(((*nvalP) >= 0) && ((*nvalP) <= NV), "invalid valid booklet count");
    /* Eliminate the booklet {ixb}, if valid: */
    if (val[ixb]) { val[ixb] = FALSE; (*nvalP)--; }
    /* Enumerate all booklets and exclude those with excessive intersection with {ixb}: */
    int32_t qvc[NT]; /* Booklet vector. */
    for (int64_t ixc = 0; ixc < NV; ixc++)
      { if (val[ixc])
          { /* See whether it is compatible with {ixb}: */
            boag_booklet_index_to_vector(NT, NB, NV, ixc, qvc);
            int32_t nshare = 0;
            for (int32_t it = 0; (it < NT) && (nshare <= k); it++)
              { if (qvc[it] == qvb[it]) { nshare++; } }
            if (nshare > k)
              { /* Booklet {ixc} is incompatible with {ixb}: */
                val[ixc] = FALSE;
                (*nvalP)--;
              }
           }
      }
  }

void boag_cycle_booklet(int32_t NT, int32_t NB, int32_t qv[])
  {
    for (int32_t it = 0; it < NT; it++)
      { int32_t ib = qv[it];
        demand((ib >= 1) && (ib <= NB), "invalid question block index in vector");
        qv[it] = (ib == NB ? 1 : ib + 1);
      }
  }

void boag_debug_booklet(char *pre, int32_t NT, int32_t NB, int64_t NV, int32_t qv[], int64_t ix)
  { fprintf(stderr, "  %s", pre);
    boag_write_booklet(stderr, NT, NB, NV, qv, ix, -1);
  }

#define boag_numTopics_MAX 26
  /* Maximum number of exam topics. */

#define boag_numBlocks_MAX 20
  /* Maximum number of question blocks per topic. */

boag_options_t *boag_parse_options(int32_t argc, char **argv)
  {
    /* Initialize argument parser: */
    argparser_t *pp = argparser_new(stderr, argc, argv);
    argparser_set_help(pp, PROG_NAME " version " PROG_VERS ", usage:\n" PROG_HELP);
    argparser_set_info(pp, PROG_INFO);
    argparser_process_help_info_options(pp);

    /* Allocate the command line argument record: */
    boag_options_t *o = notnull(malloc(sizeof(boag_options_t)), "no mem");

    /* Parse keyword parameters: */

    argparser_get_keyword(pp, "-numTopics");
    o->numTopics = (int32_t)argparser_get_next_int(pp, 1, boag_numTopics_MAX);
    
    argparser_get_keyword(pp, "-numBlocks");
    o->numBlocks = (int32_t)argparser_get_next_int(pp, 1, boag_numBlocks_MAX);
    
    argparser_get_keyword(pp, "-minShared");
    o->minShared = (int32_t)argparser_get_next_int(pp, 0, o->numTopics - 1);
    
    argparser_get_keyword(pp, "-maxShared");
    o->maxShared = (int32_t)argparser_get_next_int(pp, o->minShared, o->numTopics - 1);
    
    /* Check for spurious arguments: */
    argparser_finish(pp);

    return o;
  }
