// -*-c++-*-
// $Id: ReportGenerator.cc 460 2008-01-14 15:10:24Z dflater $

#include <iomanip>
#include <ctime>
#include <cstdlib>
#include <climits>
#include <algorithm>
#include "Types.hh"
#include "Exceptions.hh"
#include "BorderLogic.hh"
#include "RankedOrderDetails.hh"


// Field widths.
const unsigned lineWidth   (80);
const unsigned labelWidth  (40);
const unsigned numberWidth (11);

// Return codes.
const int IncorrectUsage          (0x01);
const int NoSuchReportingContext  (0x02);
const int ConnectException        (0x04);
const int ExceptionWhileConnected (0x08);
const int DisconnectException     (0x10);


void checkedStrftime (char *s, size_t max, const char *format,
                      const tm *tmstruct) {
  if (!s)
    throw PreconditionViolation ("checkedStrftime: s is NULL");
  if (!format)
    throw PreconditionViolation ("checkedStrftime: format is NULL");
  if (!tmstruct)
    throw PreconditionViolation ("checkedStrftime: tmstruct is NULL");
  if (strftime (s, max, format, tmstruct) == 0)
    throw StrftimeFailed();
}


void reportException (const std::exception &exception) {
  std::cerr << "Exception: " << exception.what() << std::endl;
}


tally_t noteVolume (tally_t count) {
  static tally_t reportTotalVolume (0);
  if (count < 0)
    throw NegativeCount();
  reportTotalVolume += count;
  return reportTotalVolume;
}


// There is no std::center.
void centerLine (constString caption) {
  if (!caption)
    throw PreconditionViolation ("centerLine: caption is NULL");
  std::cout << std::setw (std::max (0,
                                    ((int)lineWidth - (int)strlen(caption))/2))
            << "" << caption << std::endl;
}


void getTimestamp (std::string &time_out) {
  char timestamp[80];
  time_t now (time(NULL));
  checkedStrftime (timestamp, 80, "%Y-%m-%d %H:%M:%S%z", localtime(&now));
  time_out = timestamp;
}


void hline () {
  std::cout << "-------------------------------------------------------------------------------\n";
}


void reportHeader (char *context) {
  if (!context)
    throw PreconditionViolation ("reportHeader: context is NULL");
  std::string timestamp;
  getTimestamp (timestamp);
  hline();
  std::cout << "Report for context " << context
            << " generated " << timestamp << "\n\n";
}


void reportFooter () {
  hline();
}


void reportBallotCountsHeaderLine (constString c1,
                                   constString c2,
                                   constString c3) {
  if (!c1)
    throw PreconditionViolation ("reportBallotCountsHeaderLine: c1 is NULL");
  if (!c2)
    throw PreconditionViolation ("reportBallotCountsHeaderLine: c2 is NULL");
  if (!c3)
    throw PreconditionViolation ("reportBallotCountsHeaderLine: c3 is NULL");
  std::cout << std::left  << std::setw(labelWidth)  << c1
            << std::right << std::setw(numberWidth) << c2
            << std::right << std::setw(numberWidth) << c3
            << std::endl;
}


void reportBallotCountsTotalLine (const BallotCount &ballotCount) {
  std::cout << std::left  << std::setw(labelWidth)  << ballotCount.description
            << std::right << std::setw(numberWidth) << ballotCount.read
            << std::right << std::setw(numberWidth) << ballotCount.counted
            << std::endl;
  (void) noteVolume (ballotCount.read);
  (void) noteVolume (ballotCount.counted);
}


void reportBallotCountsCategoryLine (const BallotCount &ballotCount) {
  std::cout << std::right << std::setw(labelWidth)  << ballotCount.description
            << std::right << std::setw(numberWidth) << ballotCount.read
            << std::right << std::setw(numberWidth) << ballotCount.counted
            << std::endl;
  (void) noteVolume (ballotCount.read);
  (void) noteVolume (ballotCount.counted);
}


// Reduce desc to something shorter than labelWidth
void processLongChoiceDescription (std::string &desc) {
  // Output all but the last line of multi-line descriptions
  size_t crpos (desc.rfind("\n"));
  if (crpos != std::string::npos) {
    std::cout << desc.substr(0,crpos+1);
    desc.erase (0,crpos+1);
  }
  // If the last line is too long, wrap it.
  while (desc.length() > labelWidth) {
    size_t spacepos (labelWidth);
    while (strchr (" \f\n\r\t\v", desc[spacepos]) == NULL &&
           spacepos > 0)
      --spacepos;
    if (spacepos > 0) {
      std::cout << desc.substr(0,spacepos) << '\n';
      desc.erase (0,spacepos);
      desc[0] = ' ';
    } else
      break;
  }
}


void reportVoteTotalLine (const std::string &desc,
                          tally_t count) {
  std::string descRemaining (desc);
  processLongChoiceDescription (descRemaining);
  // Work around CLN ignorance of ostream flags and settings.
  // (Documented and observed behavior as of cln-1.1.13)
  std::ostringstream countStr;
  countStr << count;
  // Output last or only line of description and vote total.
  std::cout << std::left  << std::setw(labelWidth)  << descRemaining
            << std::right << std::setw(numberWidth) << countStr.str()
            << std::endl;
  (void) noteVolume (count);
}


// Don't call noteVolume on balance.  It should be zero, but if it's
// not, we don't need a NegativeCount exception.
void reportVoteBalanceLine (long count) {
  std::cout << std::left  << std::setw(labelWidth)  << "Balance"
            << std::right << std::setw(numberWidth) << count
            << std::endl;
}


void reportTotalBallotCounts (char *context) {
  if (!context)
    throw PreconditionViolation ("reportTotalBallotCounts: context is NULL");

  BallotCount ballotCount;
  fetchTotalBallotCounts (context, ballotCount);
  reportBallotCountsTotalLine (ballotCount);

  BallotCountVector ballotCounts;
  fetchBallotCountsByCategory (context, ballotCounts);
  for_each (ballotCounts.begin(),
	    ballotCounts.end(),
	    reportBallotCountsCategoryLine);

  fetchBlankBallotCounts (context, ballotCount);
  reportBallotCountsCategoryLine (ballotCount);
}


void reportBallotCountsByConfiguration (char *context) {
  if (!context)
    throw PreconditionViolation ("reportBallotCountsByConfiguration: context is NULL");

  StyleBallotCountVector styleBallotCountVector;
  fetchBallotCountsByConfiguration (context, styleBallotCountVector);

  for (StyleBallotCountVector::iterator styleIterator
                                              (styleBallotCountVector.begin());
	styleIterator != styleBallotCountVector.end();
	++styleIterator) {
    reportBallotCountsTotalLine ((BallotCount) {styleIterator->styleName,
				                styleIterator->read,
				                styleIterator->counted});
    BallotCountVector ballotCountVector;
    fetchBallotCountsByCategoryAndConfiguration (context,
						 styleIterator->styleId,
						 ballotCountVector);
    for_each (ballotCountVector.begin(),
	      ballotCountVector.end(),
	      reportBallotCountsCategoryLine);

    BallotCount ballotCount;
    if (fetchBlankBallotCountsByConfiguration (context,
					       styleIterator->styleId,
					       ballotCount))
      reportBallotCountsCategoryLine (ballotCount);
  }
}


void reportBallotCounts (char *context) {
  if (!context)
    throw PreconditionViolation ("reportBallotCounts: context is NULL");

  centerLine ("BALLOT COUNTS");
  std::cout << std::endl;
  reportBallotCountsHeaderLine ("Configuration", "Read", "Counted");
  reportBallotCountsHeaderLine ("-------------", "----", "-------");
  reportTotalBallotCounts (context);
  reportBallotCountsByConfiguration (context);
}


// New enough g++ have map::at to substitute for the const [] operator
// that is missing from the standard.  For portability, we use find
// instead.

void reportTallies (const TallyVector &tallies,
		    const ChoiceDescriptionMap &choices) {
  for (TallyVector::const_iterator it = tallies.begin();
       it != tallies.end();
       ++it) {
    const ChoiceDescriptionMap::const_iterator cit
                                                 (choices.find (it->choiceId));
    if (cit == choices.end())
      throw PreconditionViolation ("reportTallies: foreign key failure referencing choices");
    reportVoteTotalLine (cit->second, it->tally);
  }
}


void reportRegularContests (char *context) {
  if (!context)
    throw PreconditionViolation ("reportRegularContests: context is NULL");

  std::cout << std::endl;
  centerLine ("VOTE TOTALS");

  ContestStructVector contestIds;
  fetchNonRankedContestsInContext (context, contestIds);

  for (ContestStructVector::iterator contestIterator (contestIds.begin());
       contestIterator != contestIds.end();
       ++contestIterator) {
    const contestId_t contestId (contestIterator->contestId);
    ChoiceDescriptionMap choiceMap;
    TallyVector tallies;

    fetchChoicesInContest (contestId, choiceMap);
    fetchTalliesInContest (context, contestId, tallies);

    std::cout << std::endl << contestIterator->contestDescription << std::endl;
    reportTallies (tallies, choiceMap);
    reportVoteTotalLine ("Overvotes",
			 fetchOvervotes (context, contestId));
    reportVoteTotalLine ("Undervotes",
			 fetchUndervotes (context, contestId));
    reportVoteTotalLine ("Counted ballots",
			 fetchContestBallotCount (context, contestId));

    const long balance (fetchBalance (context, contestId));
    reportVoteBalanceLine (balance);
    if (balance)
      centerLine ("******************* DISCREPANCY DETECTED *******************");
  }
}


void reportRankedOrderContests (char *context, bool verbose) {
  if (!context)
    throw PreconditionViolation ("reportRankedOrderContests: context is NULL");

  ContestStructVector contests;
  fetchRankedOrderContestsInContext (context, contests);
  if (!contests.empty()) {
    std::cout << std::endl;
    centerLine ("RANKED ORDER CONTESTS");
    std::cout << "\nNOTICE:  RANKED ORDER LOGIC IS NOT NORMATIVE.\n"
     "The algorithm implemented by ReportGenerator is only one example of\n"
     "conforming behavior.  The algorithm implemented by ReportGenerator is\n"
     "not recommended or endorsed by the National Institute of Standards and\n"
     "Technology for use in elections and it is not the best algorithm\n"
     "available for the purpose.  It is used here only to provide output for\n"
     "comparison in simple cases where the implementation-dependent details\n"
     "have no impact.\n"
     "  * The quota is Hagenbach-Bischoff plus epsilon.\n"
     "  * Surpluses are transferred via the Gregory method using unlimited\n"
     "    precision rational numbers.\n"
     "  * No special cases are handled.  This means:  every choice must be\n"
     "    ranked on every ballot; every choice must be assigned a different\n"
     "    rank; only one choice is elected or eliminated at a time; and if a\n"
     "    tie occurs, the algorithm halts.\n";
  }
  for (ContestStructVector::iterator it (contests.begin());
       it != contests.end();
       ++it)
    doRankedOrderContest (*it, context, verbose);
}


void reportForContext (char *context, bool verbose) {
  if (!context)
    throw PreconditionViolation ("reportForContext: context is NULL");

  reportHeader (context);
  reportBallotCounts (context);
  reportRegularContests (context);
  reportRankedOrderContests (context, verbose);
  reportFooter ();
}


void reportVolume () {
  std::cout << "\nReport total volume: "
            << noteVolume(0) << std::endl
            << "  - Includes optional reporting of blank ballots.\n"
            << "  - Excludes separate reporting of ballots cast vs. read.\n";
}


int main (int argc, char **argv) {

  std::cout << "$Id: ReportGenerator.cc 460 2008-01-14 15:10:24Z dflater $\n";

  if (argc < 1)
    throw PreconditionViolation ("main: argc < 1");
  if (!argv)
    throw PreconditionViolation ("main: argv is NULL");


  // The PostgreSQL 8.2.3 Embedded SQL preprocessor does not support
  // the exact width integer types (uint32_t, etc.), which first
  // appeared in C99.  However, it does support long, which ISO/IEC
  // 9899 requires to be at least 32 bits.  PostgreSQL 8.2.3
  // unambiguously defines the SQL Integer data type to be 32 bits.
  if (!(sizeof(long) * CHAR_BIT >= 32))
    throw PreconditionViolation ("main: sizeof(long) < 32 bits");


  int returnVal (0), argCtr (1);
  bool verbose (false);
  if (argc > 1) {
    verbose = !strcmp (argv[1], "-v");
    if (verbose)
      ++argCtr;
  }

  if (argc < 2 || (argc == 2 && verbose)) {
    std::cerr << "Usage:  Infrastructure-Report [-v] context-name [context-name...]\n";
    returnVal |= IncorrectUsage;
  } else {
    try {
      connectDatabase();
      try {
        materializeViews();
        for (; argCtr<argc; ++argCtr) {
	  char *context (argv[argCtr]);
	  if (reportingContextValid (context))
	    reportForContext (context, verbose);
	  else {
	    std::cerr << context << ": no such reporting context\n";
	    returnVal |= NoSuchReportingContext;
	  }
        }
	reportVolume();
      } catch (std::exception &exception) {
        reportException (exception);
        returnVal |= ExceptionWhileConnected;
        std::cerr << "Disconnecting after exception.\n";
      }
      try {
        disconnectDatabase();
      } catch (std::exception &exception) {
        reportException (exception);
        returnVal |= DisconnectException;
        std::cerr << "Exiting after exception on attempt to disconnect.\n";
      }
    } catch (std::exception &exception) {
      reportException (exception);
      returnVal |= ConnectException;
      std::cerr << "Exiting after exception on attempt to connect.\n";
    }
  }

  return returnVal;
}
