//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Mon May  3 21:54:58 PDT 2010
// Last Modified: Thu May  6 23:16:19 PDT 2010
// Last Modified  Mon Feb  7 17:55:04 PST 2011 (fixed md5sum calculation)
// Filename:      ...sig/examples/all/make64.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/humpdf.cpp
// Syntax:        C++; museinfo
//
// Description:   Embed a Humdrum file into a PDF file as an attachment.
//
// Links: 
// PDF 1.7 reference (ISO 32000.1 2008):
//    http://www.adobe.com/devnet/acrobat/pdfs/PDF32000_2008.pdf
//    http://blogs.adobe.com/pdfdevjunkie/PDF_Inside_and_Out.pdf
// PDF 1.4 reference:
//    http://www.adobe.com/devnet/pdf/pdfs/PDFReference.pdf
//        section 3.10.3 Embedded File Streams, page 123
//

#include "humdrum.h"
#include "PerlRegularExpression.h"
#include "PDFFile.h"
#include "CheckSum.h"

#ifndef OLDCPP
   #include <sstream>
   #define SSTREAM stringstream
   #define CSTRING str().c_str()
   #include <iostream>
   #include <fstream>
   #include <string>
   #include <cassert>
   #include <cstring>
   using namespace std;
#else
   #ifdef VISUAL
      #include <strstrea.h>     /* for windows 95 */
   #else
      #include <strstream.h>
   #endif
   #include <iostream.h>
   #include <fstream.h>
   #include <string.h>
   #include <cassert.h>
   #include <cstring.h>
   #define SSTREAM strstream
   #define CSTRING str()
#endif

#include <sys/stat.h>
#include <unistd.h>
#include <time.h>

   
void      checkOptions(Options& opts, int argc, char* argv[]);
void      example(void);
void      usage(const char* command);
void      printMimeEncoding(ostream& out, int count, char char1, char char2, 
                               char char3);
void      createStreamData(ostream& out, SSTREAM& datatoencode,
                               const char* filename);
int       printStreamObject(ostream& out, int objnum, SSTREAM& datatoencode, 
                               const char* filename, Array<int>& objectindex,
                               Array<int>& offsetindex, int initialoffset);
int       createFileEntry(SSTREAM& out, HumdrumFile& infile, 
                               const char* filename, int nextobject,
                               Array<int>& objectindex, 
                               Array<int>& offsetindex, int initialoffset);
int       generateNewXref(SSTREAM& out, Array<int>& objectindex, 
                               Array<int>& offsetindex, int filesize);
void      printPdfDate(ostream& out, struct tm* date);
void      addTrailerPrev(Array<char>& trailerstring, int newprevoffset);
int       linkToRootObject(ostream& out, Array<int>& objectindex, 
                               Array<int>& offsetindex, int initialoffset, 
                               Array<char> trailerstring, int xrefoffset, 
                               istream& file, int nextobject,
                               PDFFile& pdffile);
void      getObject(ostream& out, istream& file, int offset);
int       updateNamesObject(ostream& out, Array<int>& objectindex, 
                               Array<int>& offsetindex, int initialoffset, 
                               ostream& file, int nextobject, int ndoffset);
int       updateRootObject(ostream& out, int rootobjnum, int initialoffset, 
                               PDFFile& pdffile, Array<char>& rootstring, 
                               Array<int>& objectindex, Array<int>& offsetindex,
                               int embedcount, int nextobject);
void      addDictionaryEntry(Array<char>& objectstring, Array<char>& entry);
int       getSequentialObjectCount(Array<int>& list, int starti);

// global variables:
Options     options;
const char* pdffilename = "";  // used with -p option
int         footerQ  = 0;      // used with -A option
int         keepdirQ = 0;      // used with -D option
int         hiddenQ  = 0;      // used with --hidden option (not active)
int         debugQ   = 0;      // used with --debug option
int         prefixQ  = 0;      // used with -P option
const char* prefix   = "";     // used with -P option


///////////////////////////////////////////////////////////////////////////

int main(int argc, char** argv) {
   // process the command-line options
   checkOptions(options, argc, argv);

   istream *file;
   ifstream filestream;
   if (strcmp(pdffilename, "") == 0) {
      // read standard input if no -p option given
      file = &cin;
   } else {
      filestream.open(pdffilename, ios::in | ios::binary);
      file = &filestream;
      if (!filestream.is_open()) {
         cerr << "ERROR: cannot open file: " << pdffilename << "\n";
         exit(1);
      }
   }

   int i;
   int initialoffset = 0;
   HumdrumFile infile;
   PDFFile     pdffile;

   pdffile.process(*file); // reads structural information from PDF file.
                          // A pointer to ifstream file is stored in
			  // pdffile, so don't close file while still
			  // extracting data using pdffile.

   int filesize   = pdffile.getFileSize();
   int xrefoffset = pdffile.getXrefOffset(0);
   int nextobject = pdffile.getObjectCount();

   if (xrefoffset <= 5) {
      cerr << "ERROR: no xref offset found in file " << pdffilename << endl;
      exit(1);
   }

   // figure out the number of input files to process
   int numinputs = options.getArgCount();

   Array<SSTREAM*> filesegments;  // temporary storage for embedded files
   if (numinputs <= 0) {
      filesegments.setSize(1);
   } else {
      filesegments.setSize(numinputs);
   }
   filesegments.allowGrowth(0);
   for (i=0; i<filesegments.getSize(); i++) {
      filesegments[i] = new SSTREAM;
   }

   Array<int> objectindex(1000);
   Array<int> offsetindex(1000);
   objectindex.setGrowth(10000);
   offsetindex.setGrowth(10000);
   objectindex.setSize(0);
   offsetindex.setSize(0);

   const char* filename = "";
   int fcounter = 0;
   for (i=0; i<numinputs; i++) {
      infile.clear();
      filename = options.getArg(i+1);
      infile.read(filename);
      nextobject = createFileEntry(*filesegments[fcounter], infile, 
            filename, nextobject, objectindex, offsetindex, initialoffset);
      initialoffset += filesegments[fcounter]->str().length();
      fcounter++;
   }

   if (!footerQ) {
      // print initial contents of input PDF
      pdffile.print(cout);
      cout << flush;
   }

   for (i=0; i<filesegments.getSize(); i++) {
      cout << filesegments[i]->str() << flush;
   }

   Array<char> trailerstring;
   pdffile.getTrailerString(0, trailerstring);

   if (!hiddenQ) {
      // If "hidden", the file will disappear if Save As... is used to save,
      // because the document root does not know about it.
      SSTREAM rootlink;
      nextobject = linkToRootObject(rootlink, objectindex, offsetindex, 
		         initialoffset, trailerstring, xrefoffset, *file, 
			 nextobject, pdffile);
      cout << rootlink.str() << flush;
      initialoffset += rootlink.str().length();
   }

   // update the object count (/Size) in the trailer:
   char replacement[128] = {0};
   sprintf(replacement, "/Size %d", nextobject);
   PerlRegularExpression pre;
   pre.sar(trailerstring, "\\/Size\\s+\\d+", replacement);

   // remove any /Prev entry in trailer, and replace with 
   // new one
   addTrailerPrev(trailerstring, xrefoffset);

   SSTREAM xrefstream;

   int newxrefoffset = generateNewXref(xrefstream, objectindex, 
		   offsetindex, filesize);
   newxrefoffset += initialoffset;
   cout << xrefstream.str();
   cout << trailerstring.getBase() << endl;
   cout << "startxref\n";
   // byte location of new xref goes here:
   cout << newxrefoffset + filesize << endl;
   cout << "%%EOF" << endl;

   for (i=0; i<filesegments.getSize(); i++) {
      delete filesegments[i];
      filesegments[i] = NULL;
   }

   return 0;
}

///////////////////////////////////////////////////////////////////////////


//////////////////////////////
//
// linkToRootObject -- 
//
//  Link embedded files to to root entry of PDF.  If the embedded 
//  file is not linked from the Root entry, then it looks like it 
//  is removed automatically when "Save As..." is used to save the file.
//  
//  Basic algorithm:
//
//  (1) Locate the Root object (found from the Trailer /Root dictionary entry).
//  (2) Identify any previous /Names entry in Root's dictionary
//      such as:  
//         /Names 261 0 R
//  (4) If no /Names entry, then add one to the Root object, and create entry.
//  (3) If Root has a /Names entry, then update the indirect object 
//       (and probably don't need to update Root entry).
//  (5) To create a /Names indirect object entry:
//  (6) In the /Names indirect object listed in the Root, add a line such as:
//             /EmbeddedFiles 400 0 R       
//         [Note: have to figure out how to deal with multiple embedded files.]
//         where 400 0 R is an indirect object for for an object which 
//	 contains a /Names entry (name of object internally):
//	 <<
//	    /Names [(_^@U^@n^@t^@i^@t^@l^@e^@d^@ ^@O^@b^@j^@e^@c^@t) 398 0 R]
//         >>
//	 The value for /Names entry is an array of two items:
//	    (A) the name of the object in UTF-16, usually called
//	        "_Untitled Object"
//            (B) An indirect object reference point to an object which
//                contains the /Type /FileSpec in the dictionary
//                (which contains the true filesystem's filename, and
//                 an indirect object reference to a stream object which
//                 contains the actual contents of a file.
//
// General Map of attaching the embedded file to the Root object:
//   Root object --> Names Dictionary --> Embedded List --> Embedded File
//        Specification --> Embedded File Stream
//
// * Root object has a /Names entry in its dictionary which points to 
//     an indirect object that gives the Names dictionary of the Root object.
//     (create a /Names entry if it does not already exist and update
//      the Root object in the PDF file; otherwise, leave the original
//      Root entry unchanged, and go to the Name Dictionary object for
//      further processing. Example Root object:
//      1 0 obj<</Type/Catalog/Pages 3 0 R/Metadata 11 0 R/Names 14 0 R>> endobj
//      or without a /Names entry in the dictionary:
//      1 0 obj<</Type/Catalog/Pages 3 0 R/Metadata 11 0 R>> endobj
// * Names Dictionary should have a dictionary entry called /EmbeddedFiles
//     which points to an indirect object which will list the embedded files.
//     Example Names Dictionary:
//        14 0 obj << /EmbeddedFiles 15 0 R >>
//     This entry says that the list of embedded files is found in indirect
//     object 15.  If the Names Dictionary does not contain an /EmbeddedFiles
//     entry, then update the object to add one.
// * Embedded List: A dictionary with a /Names entry which contains an
//     array of pairs of entries which list the embedded files.  The first
//     of the pair is a Unicode-16 string giving the embedded name of the
//     data (not the filename, and not really used for anything that I can
//     figure out.  The typical name is "_Untitled Object".  The second
//     value of the pair is a reference to an indirect object.  In the example
//     below, the indirect object is #13.  This is a link to the /FileSpec
//     entry for the embedded file.
//      15 0 obj
//      << /Names [(_^@U^@n^@t^@i^@t^@l^@e^@d^@ ^@O^@b^@j^@e^@c^@t) 13 0 R] >>
//      endobj
// * Embedded File Specification:
//         13 0 obj
//         << /Type /Filespec
//            /F    (file.krn)
//            /EF   << /F 12 0 R >>
//            /Desc (Short Description of File)
//         >>
//         endobj
//      The Embedded file specification lists the name of the file in the /F
//      entry, the embedded content stream is listed as an indirect object
//      in the /F entry in the dictionary of the /EF entry (in this case
//      object #12).
// * Embedded File Stream:
//      Contains the actual contents of the embedded file plus some
//      file content information:
//      
//      12 0 obj
//      <<
//         /Type    /EmbeddedFile
//         /SubType /application#2fx-humdrum
//         /Length  34
//         /Params
//         <<
//            /CreationDate (D:20100510042439-08'00')
//            /ModDate      (D:20100510042439-08'00')
//            /EmbedDate    (D:19991214040506-08'00')
//            /Size         34
//            /CheckSum     <e3740828edc5d78e85765c9daf1a0>
//         >>
//      >>
//      stream
//      **kern
//      *M4/4
//      *k[]
//      *c:
//      =-
//      1c
//      ==
//      *-
//      endstream
//      endobj
//
//     The /Length field is required and gives the number of bytes
//     between the string "stream\n" and endstream.  "stream" should
//     have the newline 0x0a or "0x0d 0x0a" after it (but not 0x0d alone).  
//     An optional newline before "endstream" is allowed, and will not be 
//     considered part of the data inside of the stream.
// 
//

int linkToRootObject(ostream& out, Array<int>& objectindex, 
      Array<int>& offsetindex, int initialoffset, Array<char> trailerstring, 
      int xrefoffset, istream& file, int nextobject, PDFFile& pdffile) { 

   // when this function is called, only embedded files have been
   // added to the PDF.  There are two indirect objects for each
   // embedded file (the /FileSpec entry and the actual contents,
   // So embedcount is the number of files which have been included:
   int embedcount = objectindex.getSize() / 2;

   int rootindex  = pdffile.getRootIndex();
   int rootoffset = pdffile.getObjectOffset(rootindex);

   //// Now go to root object and check to see if there is a /Names entry

   int i;
   if (debugQ) {
      cerr << ">>> Indirect object byte offset table:" << endl;
      for (i=0; i<pdffile.getObjectCount(); i++) {
         cerr << ">>> " << i << ":\t" << pdffile.getObjectOffset(i) << endl;
      }
   }

   SSTREAM rootstream;

   getObject(rootstream, file, rootoffset);

   Array<char> rootstring;
   rootstring.setSize(rootstream.str().length()+1);
   for (i=0; i<(int)rootstream.str().length(); i++) {
      rootstring[i] = rootstream.str()[i];
   }
   rootstring[rootstring.getSize()-1] = '\0';

   //// if there is a /Names entry in dictionary, then don't bother updating
   //// the Root entry and instead go directly to the /Names object and modify.
   //// If there is not a /Names entry, then add one as indirect object 
   //// and also insert a revised Root object.

   PerlRegularExpression pre;
   if (pre.search(rootstring.getBase(), "/Names\\s+(\\d+)\\s+(\\d)\\s+R", "")) {
      // int namesobj = atol(pre.getSubmatch(1));
      // int namesver = atol(pre.getSubmatch(2));
      // ggg
      // nextobject = updateNamesObject(out, objectindex, offsetindex, 
      //      initialoffset, file, nextobject, objectoffsets[namesobj]);
   } else {
      // update Root dictionary to add /Names entry, create Names dictionary
      // and create the list of Embedded files in another object.
      SSTREAM newroot;
      nextobject = updateRootObject(newroot, rootindex, initialoffset, pdffile,
         rootstring, objectindex, offsetindex, embedcount, nextobject);
      initialoffset += newroot.str().length();
      out << newroot.str() << flush;
   }

   return nextobject;
}



//////////////////////////////
//
// updateRootObject --
//   newroot        == Output data stream for revised root object
//   rootobjnum     == Root indirect object number (second value presumed 0)
//   initialoffset  == Byte offset from the start of the file to start
//                     of newroot stream.
//   pdffile        == Storage for byte offset data.
//   rootstring     == The original Root object entry which needs to have
//                     an added name dictionary, etc.
//   objectindex    == List of indirect objects created by this program
//                     (index in order of creation).
//   offsetindex    == List of byte offsets for objects created by this
//                     program.  (index in order of creation).
//   embedcount     == Number of embedded files added by program.
//

int updateRootObject(ostream& out, int rootobjnum, int initialoffset, 
      PDFFile& pdffile, Array<char>& rootstring, Array<int>& objectindex, 
      Array<int>& offsetindex, int embedcount, int nextobject) {

   SSTREAM newroot;
   newroot << "\n";
   // objectoffsets[rootobjnum] = initialoffset + newroot.str().length();
   objectindex.append(rootobjnum);
   int tval = newroot.str().length() + initialoffset;
   offsetindex.append(tval);

   int ndobjectnumber = pdffile.getObjectCount() + embedcount*2;
   int ndversion = 0;
   nextobject++;
 
   Array<char> entry;
   entry.setSize(1024);
   sprintf(entry.getBase(), " /Names %d %d R ", ndobjectnumber, ndversion);
   entry.setSize(strlen(entry.getBase())+1);
   addDictionaryEntry(rootstring, entry);

   int i;
   for (i=0; i<rootstring.getSize()-2; i++) {
      newroot << rootstring[i];
   }
   newroot << "\n";
   initialoffset += newroot.str().length();
   out << newroot.str() << flush;

   // create a name dictionary for the root object:

   SSTREAM namedict;
   namedict << "\n";
   objectindex.append(ndobjectnumber);
   int tempoffset = initialoffset + namedict.str().length();
   offsetindex.append(tempoffset);
   namedict << ndobjectnumber << " " << ndversion << " obj\n";
   namedict << "<<\n";

   int embedlistobjnum = pdffile.getObjectCount()+1 + embedcount*2;

   namedict << "/EmbeddedFiles " << embedlistobjnum << " 0 R\n";
   namedict << ">>\n";
   namedict << "endobj\n";
   initialoffset += namedict.str().length();
   nextobject++;

   out << namedict.str() << flush;

   // add the embedded file listing:

   objectindex.append(embedlistobjnum);
   SSTREAM embedlist;
   embedlist << "\n";
   tempoffset = initialoffset + embedlist.str().length();
   offsetindex.append(tempoffset);
   
   embedlist << embedlistobjnum << " 0 obj\n";
   embedlist << "<<\n"; 
   embedlist << "   /Names [\n";
   int tempobjnum;
   char nullchar = (char)0;
   for (i=0; i<embedcount; i++) {
      // point to the second object of each pair in the
      // objectindex array (up to the count of file added):
      tempobjnum = objectindex[i*2+1];
      embedlist << "           ";
      embedlist << "(";
      embedlist << (char)0xfe << (char)0xff;
      embedlist << nullchar << "U";
      embedlist << nullchar << "n";
      embedlist << nullchar << "t";
      embedlist << nullchar << "i";
      embedlist << nullchar << "t";
      embedlist << nullchar << "l";
      embedlist << nullchar << "e";
      embedlist << nullchar << "d";
      embedlist << nullchar << " ";
      embedlist << nullchar << "O";
      embedlist << nullchar << "b";
      embedlist << nullchar << "j";
      embedlist << nullchar << "e";
      embedlist << nullchar << "c";
      embedlist << nullchar << "t) ";
      embedlist << tempobjnum << " 0 R\n";
   }
   embedlist << "          ]\n";
   embedlist << ">>\n";
   embedlist << "endobj\n";

   initialoffset += embedlist.str().length();
   out << embedlist.str() << flush;

   return nextobject;
}



//////////////////////////////
//
// updateNamesObject -- Adds an EmbeddedFiles entry into the Root's
//     Name Dictionary, or creates an EmbeddedFiles entry if none
//     exists.
// 261 0 obj
// <<
//   /Dests 254 0 R
//   /EmbeddedFiles 400 0 R       % Added Embedded Files list
// >>
// endobj
//

int updateNamesObject(ostream& out, Array<int>& objectindex, 
      Array<int>& offsetindex, int initialoffset, ifstream& file, 
      int nextobject, int ndoffset) {

   SSTREAM ndstream;
   getObject(ndstream, file, ndoffset);
   Array<char> ndstring;
   ndstring.setSize(ndstream.str().length()+1);
   int i;
   for (i=0; i<(int)ndstream.str().length(); i++) {
      ndstring[i] = ndstream.str()[i];
   }
   ndstring[ndstring.getSize()-1] = '\0';

   PerlRegularExpression pre;

   // char buffer[128] = {0};
   Array<char> entry;
   entry.setSize(1000);

   if (pre.search(ndstring.getBase(), 
         "/EmbeddedFiles\\s+(\\d+)\\s+(\\d+)\\s+R", "")) {
      // nothing to change in Name Dictionary, just
      // go to the list of embedded files...
      // int iobject = atol(pre.getSubmatch(1));
      // nextobject = updateEmbeddedFileList(out, objectindex, offsetindex, 
      //         initialoffset, file, nextobject, offsetindex[iobject]) 
      // ggg
   } else {
      // Add an /EmbeddedFiles entry to the Name Dictionary
      int assignednum = nextobject++;
      int version = 0;
      sprintf(entry.getBase(), " /EmbeddedFiles %d %d R ", assignednum, 
         version);
      entry.setSize(strlen(entry.getBase())+1);
      addDictionaryEntry(ndstring, entry);
      // print the new Root's name dictionay object

      // and create a list of embedded files.
  
      // ggg

   }

   return nextobject;
}



//////////////////////////////
//
// addDictionaryEntry -- add an entry to a dictionary.  You should
//    do a check before calling this function to make sure that
//    the name key is not already in the dictionary.
//

void addDictionaryEntry(Array& objectstring, Array& entry) {
   int level = 0;
   Array<char> newobject;
   newobject.setSize(objectstring.getSize()+entry.getSize()+100);
   newobject.setGrowth(1000);
   newobject.setSize(0);
   int inserted = 0;
   char ch;
   int i, j;
   // char buffer[128] = {0};
   // int plen;

   for (i=0; i<objectstring.getSize(); i++) {
      newobject.append(objectstring[i]);
      if (objectstring[i] == '<') {
         level++;
      }
      if (objectstring[i] == '>') {
         level--;
      }
      if (inserted || (level != 2)) {
         continue;
      }

      inserted = 1;
      for (j=0; j<entry.getSize()-1; j++) {
         newobject.append(entry[j]);
      }

      continue;
   }

   ch = '\0'; newobject.append(ch);
   objectstring.setSize(newobject.getSize());
   strncpy(objectstring.getBase(), newobject.getBase(), newobject.getSize());
}



//////////////////////////////
//
// getObject -- get an object's contents (but not intended for stream objects,
//    although it will usually work for them as well).
//

void getObject(ostream& out, istream& file, int offset) {
   int level = 0;
   int endindex = 0;
   char endstate[128] = {0};
   strcpy(endstate, "endobj");
   int endtarget = strlen(endstate);

   char ch;
   file.seekg(offset, ios::beg);
   file.get(ch);

   while (!file.eof()) {
      if (ch == '<') {
         level++;
      } else if (ch == '>') {
         level--;
      }
      if (level <= 0) {
         if (endstate[endindex] == ch) {
            endindex++;
         } else {
            endindex = 0;
         }
         if (endindex == endtarget) {
            out << ch;
            return;
         }
      }
      out << ch;
      file.get(ch);
   }

   cerr << "ERROR: end of Object had strange error" << endl;
   exit(1);

   return;
}



//////////////////////////////
//
// generateNewXref --
//    filesize == size in bytes of original file.
//

int generateNewXref(SSTREAM& finalout, Array<int>& objectindex, 
      Array<int>& offsetindex, int filesize) {

   SSTREAM out;

   // temporary code for testing:
   // filesize = 0;
  
   int output = 0;

   out << "\n";

   output = out.str().length();

   out << "xref\n";
   // don't need the null object:
   // out << "0 1\n";
   // out << "0000000000 65535 f" << (char)0x0d << (char)0x0a;

   // output the starting object number in a sequence
   // and then how many follow, then the offset and version numbers
   // followed by " n" 0x0d 0x0a.

   if (objectindex.getSize() == 0) {
      return output;
   }

   int i;
   int currenti = 0;
   int currentlen = getSequentialObjectCount(objectindex, currenti);
   int value;

   while (currenti < objectindex.getSize()) {
      out << objectindex[currenti] << " " << currentlen << "\n";

      for (i=currenti; i<currenti+currentlen; i++) {
         value = offsetindex[i] + filesize;
         if (value < 0) {
            cerr << "ERROR: offset is negative which is impossible." << endl;
	    cerr << "Value is " << value << endl;
	    cerr << "original filesize is " << filesize << endl;
	    cerr << "offset value is " << offsetindex[i] << endl;
            exit(1);
         }
         if (value < 10)         { out << '0'; }  // 0-
         if (value < 100)        { out << '0'; }  // 00-
         if (value < 1000)       { out << '0'; }  // 000-
         if (value < 10000)      { out << '0'; }  // 0000-
         if (value < 100000)     { out << '0'; }  // 00000-
         if (value < 1000000)    { out << '0'; }  // 000000-
         if (value < 10000000)   { out << '0'; }  // 0000000-
         if (value < 100000000)  { out << '0'; }  // 00000000-
         if (value < 1000000000) { out << '0'; }  // 000000000-
         out << value;
         out << " ";
         out << "00000";
         out << " ";
         out << "n";
         out << (char)0x0d << (char)0x0a;
      }
      currenti  += currentlen;
      currentlen = getSequentialObjectCount(objectindex, currenti);
   }

   finalout << out.str();
   return output;
}



//////////////////////////////
//
// getSequentialObjectCount --
//

int getSequentialObjectCount(Array& list, int starti) {
   int i;
   int output = 1;
   for (i=starti+1; i<list.getSize(); i++) {
      if (list[i] - list[i-1] != 1) {
         break;
      } else {
         output++;
      }
   }

   return output;
}



//////////////////////////////
//
// addTrailerPrev -- Add a /Prev entry to a trailer, or change one
//     if it already exists in the trailer.
//

void addTrailerPrev(Array& trailerstring, int newprevoffset) {
   int level = 0;
   Array<char> newtrailer;
   newtrailer.setSize(trailerstring.getSize()+1000);
   newtrailer.setGrowth(1000);
   newtrailer.setSize(0);
   PerlRegularExpression pre;
   int prevprinted = 0;
   char ch;
   int i, j;
   char buffer[128] = {0};
   int plen;
   for (i=0; i<trailerstring.getSize(); i++) {
      if (trailerstring[i] == '<') {
         level++;
      }
      if (trailerstring[i] == '>') {
         level--;
      }
      if (level != 2) {
         newtrailer.append(trailerstring[i]);
         continue;
      }
      if (trailerstring[i] != '/') {
         newtrailer.append(trailerstring[i]);
         continue;
      }
      if (pre.search(trailerstring.getBase()+i, "^(/Prev\\s+\\d+)", "")) {
         const char* ptr = pre.getSubmatch(1);
         plen = strlen(ptr);
         i+= plen-1;
         sprintf(buffer, "/Prev %d", newprevoffset);
         j = 0;
         while (buffer[j] != '\0') {
            newtrailer.append(buffer[j++]);
         }
         prevprinted = 1;
         continue;
      }
      newtrailer.append(trailerstring[i]);
      continue;
   }
   ch = '\0'; newtrailer.append(ch);

   if (!prevprinted) {
      // need to insert a /Prev entry (at end of dictionary)
      level = 0;
      for (i=newtrailer.getSize()-1; i>=0; i--) {
         if (newtrailer[i] == '>') { 
            level++;
         }
         if (newtrailer[i] == '<') { 
            level--;
         }
         if (level != 2) {
            continue;
         }
         sprintf(buffer, " /Prev %d\n", newprevoffset);
         plen = strlen(buffer);
         int oldlen = newtrailer.getSize();
         newtrailer.setSize(oldlen+plen);
         int newlen = newtrailer.getSize();
         for (j=0; j<oldlen-i+1; j++) {
            newtrailer[newlen-j-1] = newtrailer[newlen-j-plen-1];
         }
         for (j=0; j<plen; j++) {
            newtrailer[i+j] = buffer[j];
         }
	 break;
      }
   }

   trailerstring.setSize(newtrailer.getSize());
   strncpy(trailerstring.getBase(), newtrailer.getBase(), newtrailer.getSize());
}



//////////////////////////////
//
// createFileEntry --
//
//

int createFileEntry(SSTREAM& out, HumdrumFile& infile, const char* filename,
       int nextobject, Array<int>& objectindex, Array<int>& offsetindex,
       int initialoffset) {
   SSTREAM datatoencode;
   infile.write(datatoencode);
   nextobject = printStreamObject(out, nextobject, datatoencode, filename,
                      objectindex, offsetindex, initialoffset);
   return nextobject;
}



/////////////////////////////
//
// printStreamObject --
//

int printStreamObject(ostream& finalout, int objnum, SSTREAM& datatoencode, 
      const char* filename, Array<int>& objectindex, Array<int>& offsetindex,
      int initialoffset) {
   SSTREAM streamcontents;
   SSTREAM out;
   createStreamData(streamcontents, datatoencode, filename);
   int contentsize = streamcontents.str().length();
   char newline = 0x0a;
   int version = 0;

   int initiallen = out.str().length();

   // print the embedded file content stream object ///////////////////

   out << "\n";

   objectindex.append(objnum);
   int offset = out.str().length() - initiallen;
   offset += initialoffset;
   offsetindex.append(offset);

   out << objnum++ << " " << version << " obj\n";
   out << "<<\n";
   out << "   /Type    /EmbeddedFile\n";
   out << "   /SubType /application#2fx-humdrum\n";
   out << "   /Length  " << contentsize << "\n";

   struct stat attrib;
   stat(filename, &attrib);
   struct tm* moddate;
   moddate = gmtime(&(attrib.st_mtime));
   out << "   /Params\n";
   out << "   <<\n";
   out << "      /CreationDate (";
   printPdfDate(out, moddate); // example: D:20050727132644-04'00'
   out << ")\n";
   out << "      /ModDate      (";
   printPdfDate(out, moddate); // example D:20050727143111-04'00'
   out << ")\n";
   out << "      /Size         " <<  datatoencode.str().length() << "\n";
   out << "      /CheckSum     <";
   CheckSum::getMD5Sum(out, datatoencode); 
   // such as 5C94A7BE7C695C70271E29A26B5705C1
   out << ">\n";
   out << "   >>\n";

   out << ">>\n";
   out << "stream" << newline;
   out << streamcontents.str();
   int len1 = streamcontents.str().length();
   if ((streamcontents.str()[len1-1] != 0x0a) && 
       (streamcontents.str()[len1-1] != 0x0d)) {
      out << "\n";
   }
   out << "endstream\n";
   out << "endobj\n";

   // print the file spec object: /////////////////////////////////////
  
   Array<char> outfilename;
   int len = strlen(filename);
   outfilename.setSize(1000 + len);
   outfilename.setGrowth(1000);
   strcpy(outfilename.getBase(), filename);
   outfilename.setSize(len+1);
   
   PerlRegularExpression pre;

   if (!keepdirQ) {
      pre.sar(outfilename, ".*/", "", "g");
   }

   out << "\n";

   objectindex.append(objnum);
   offset = out.str().length() - initiallen;
   offset += initialoffset;
   offsetindex.append(offset);

   out << objnum << " " << version << " obj\n";
   objnum++;
   out << "<<\n";
   out << "   /Type /Filespec\n";
   out << "   /F    (";
   if (prefixQ) {
      out << prefix;
   }
   out << outfilename.getBase();
   out << ")\n";
   out << "   /EF   << /F " << objnum-2 << " 0 R >>\n"; // object with contents
   out << "   /Desc (Embedded Humdrum File)\n";         // descripion of file
   out << ">>\n";
   out << "endobj\n";

   finalout << out.str();

   return objnum;
}



//////////////////////////////
//
// printPdfDate -- time is printed in UTC  plus deviation from UTC for 
//       localtime.
// example D:20050727143111-04'00'
//         D:yyyymmddhhmmss-HH'MM'
//

void printPdfDate(ostream& out, struct tm* date) {
   char buffer[128] = {0};
   strftime(buffer, 128, "D:%Y%m%d%H%M%S", date);
   out << buffer;

   // print time zone information (need to check on daylight savings)
// Remove timezone info for now since Apple OS X is having difficulties.
int value  = 0; 
//   int value = timezone;
   char sign = '-';
//   if (timezone < 0) {
//      value = -timezone;
//   }

   int hour = value / 3600;
   int min  = value - hour * 3600;    

   if (min < 0) { min = 0; }
   out << sign;
   if (hour < 10) { out << "0"; }
   out << hour << "'";
   if (min < 10) { out << "0"; }
   out << min << "'";

}



//////////////////////////////
//
// createStreamData --
//

void createStreamData(ostream& out, SSTREAM& datatoencode, 
      const char* filename) {
   out << datatoencode.str();
}


void createStreamDataOld(ostream& out, SSTREAM& datatoencode, 
      const char* filename) {
   datatoencode << ends;

   out << "<humdrum encoding='base64' source='";
   out << filename;
   out << "'>\n";

   string sss = datatoencode.str();
   // out << "Length of string " << sss.length() << endl;

   int count = sss.length() - 1;
   int packets = count / 3;
   // deal with leftovers later...
   int i;
   for (i=0; i<packets; i++) {
      printMimeEncoding(out, 3, sss[i*3], sss[i*3+1], sss[i*3+2]);
      if ((i+1) % 19 == 0) {
         out << endl;
      }
   }
   int leftovers = count % 3;
   switch (leftovers) {
      case 1:
         printMimeEncoding(out, 1, sss[count-1], 0, 0);
         break;
      case 2:
         printMimeEncoding(out, 2, sss[count-2], sss[count-1], 0);
         break;
   }
   out << "\n</humdrum>\n";
}



//////////////////////////////
//
// printMimeEncoding -- Not used any longer...
//

void printMimeEncoding(ostream& out, int count, char char1, char char2, 
      char char3) {
   static char table[64] = {
         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};

   out << table[char1 >> 2];
   out << table[((char1 & 0x03) << 4) | (char2 >> 4)];
   if (count == 1) {
      out << "==";
      return;
   }
   out << table[((char2 & 0x0f) << 2) | (char3 >> 6)];
   if (count == 2) {
      out << "=";
      return;
   }
   out << table[char3 & 0x3f];

}



//////////////////////////////
//
// checkOptions -- validate and process command-line options.
//

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("p|pdf=s:", "PDF file on which file(s) will be attached");
   opts.define("A|append-only=b", "output only data to append to PDF");
   opts.define("debug=b", "print debugging statements to standard error");
   opts.define("D|keep-directory=b", "keep directory in filename");
   opts.define("d|directory=b",      "append directory path to filename");
   opts.define("P|prefix=s:",        "prepend path to written filename");

   opts.define("author=b",  "author of program");
   opts.define("version=b", "compilation info");
   opts.define("example=b", "example usages");
   opts.define("help=b",    "short description");
   opts.process(argc, argv);
   
   // handle basic options:
   if (opts.getBoolean("author")) {
      cout << "Written by Craig Stuart Sapp, "
           << "craig@ccrma.stanford.edu, May 2010" << endl;
      exit(0);
   } else if (opts.getBoolean("version")) {
      cout << argv[0] << ", version: 5 May 2010" << endl;
      cout << "compiled: " << __DATE__ << endl;
      cout << MUSEINFO_VERSION << endl;
      exit(0);
   } else if (opts.getBoolean("help")) {
      usage(opts.getCommand());
      exit(0);
   } else if (opts.getBoolean("example")) {
      example();
      exit(0);
   }

   if (opts.getBoolean("pdf")) {
      pdffilename = opts.getString("pdf");
   } else {
      //// No -p option is now allowed.  It means that the PDF will be
      //// coming into the program from standard input.
      // cerr << "Error: -p file.pdf option is required." << endl;
      // exit(1);
   }

   footerQ  = opts.getBoolean("append-only");
   debugQ   = opts.getBoolean("debug");
   keepdirQ = opts.getBoolean("keep-directory");
   prefixQ  = opts.getBoolean("prefix");
   if (prefixQ) {
      prefix = opts.getString("prefix");
      keepdirQ = 0;
   }
}



//////////////////////////////
//
// example -- example usage of the quality program
//

void example(void) {
   cout <<
   "                                                                         \n"
   << endl;
}



//////////////////////////////
//
// usage -- gives the usage statement for the meter program
//

void usage(const char* command) {
   cout <<
   "                                                                         \n"
   << endl;
}




// md5sum: 9202c66af90809a144c86dd132461f0b humpdf.cpp [20111105]