/*****************************************************************************
Copyright (c) 2008, Christopher D. Twigg

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.  
* Redistributions in binary form must reproduce the above copyright notice,
	this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/




#include "latexParser.h"
#include "bibtexParser.h"
#include "htmlWrappers.h"
#include "httpWrapper.h"
#include "DBMWrapper.h"
#include "homePages.h"
#include "shortenedPublications.h"
#include "utility.h"
#include "exception.h"

#include <cstdio>
#include <iostream>
#include <vector>
#include <map>
#include <set>
#include <fstream>
#include <sstream>
#include <stack>
#include <iomanip>

#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/exception.hpp>
#include <boost/filesystem/fstream.hpp>

#include <boost/program_options.hpp>
#include <boost/numeric/conversion/cast.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/iostreams/tee.hpp>
#include <boost/utility.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/scoped_array.hpp>

using namespace boost::spirit;
using namespace std;

const unsigned long megabyte = 1048576;
const unsigned long kilobyte = 1024;
const size_t oldestYear = 2001;
const unsigned int iconWidth = 50;


// We often need to wrap abstracts with an extra set of 
// braces so that BiBTeX doesn't complain about embedded
// quotation marks: e.g., 
//    image = "...",
//    abstract = {{ In this work, we define "stuff"... }},
// This function will strip out any such braces.
std::string stripBraces( const std::string& str )
{
  std::string result;
  for( std::string::const_iterator itr = str.begin();
       itr != str.end(); ++itr )
  {
    if( *itr == '{' || *itr == '}' )
      continue;

    result.push_back( *itr );
  }

  return result;
}

// For PHP single-quoted strings, this function will
// escape any internal single quotes
std::string phpEscapedString( const std::string& str )
{
  std::string result;
  result.push_back( '\'' );
  for( std::string::const_iterator itr = str.begin();
    itr != str.end(); ++itr )
  {
    if( *itr == '\'' )
      result.push_back( '\\' );
    result.push_back( *itr );
  }

  result.push_back( '\'' );
  return result;
}

bool isEmptyImage( const Magick::Image& image )
{
  Magick::Geometry geom = image.size();
  return ((geom.width() == 0) || (geom.height() == 0));
}

// Publications need to be sorted in order from most recent
//   to least, this class has the correct ordering:
class DateSorter
{
public:
  bool operator()( const BTEntry& first, const BTEntry& second )
  {
    if( first.year() == second.year() && first.month() == second.month() )
      return (first.title().compare( second.title() ) < 0);
    if( first.year() == second.year() )
      return greater<unsigned int>()( first.month(), second.month() );
    else
      return greater<unsigned int>()( first.year(), second.year() );
  }
};

void yearMenu( ostream& out, size_t latestYear, size_t currentYear )
{
  HtmlThing div( out, "div", true, "class=\"yearnav\"" );
  HtmlThing ul( out, "ul", true, "class=\"yearnavlist\"" );
          
  for( size_t year = latestYear; year >= oldestYear; --year )
  {
    HtmlThing li( out, "li", true );
    HtmlThing ref( out, "a", false,
      std::string("href=\"abstracts.") + 
        boost::lexical_cast<std::string>(year) + std::string(".php\"") + 
      ((year == currentYear) ? std::string(" class=\"current\"") : std::string()) );
    out << year;
    if( year == oldestYear )
      out << " and before";
  }
}

void checkDirectory( const boost::filesystem::path& path )
{
  try
  {
    if( exists(path) )
    {
      if( !is_directory(path) )
        throw IOException( "'" + path.native_file_string() + "' is not a directory." );
    }
    else
    {
      create_directory( path );
    }
  }
  catch( boost::filesystem::filesystem_error& e )
  {
    throw IOException( "Error creating directory '" + path.native_file_string() + "': " + e.what() );
  }
}

int main( int argc, char** argv )
{
  boost::filesystem::path::default_name_check( &boost::filesystem::native );

  namespace po = boost::program_options;
  using boost::filesystem::path;
  using boost::filesystem::ifstream;
  using boost::filesystem::ofstream;

  // options allowed on both command line and in config file:
  po::options_description both_desc("Command line/config file options");
  both_desc.add_options()
                ("bibtex-file", po::value< std::string >(), "BiBTeX file (*.bib)")
                ("output-directory", po::value< std::string >(), "Directory for all output files")
                ("homepage-file", po::value< std::string >(),
                   "File listing home pages in this format: \"Harry Bovik http://www.cs.cmu.edu/~bovik/\" (default: homePages.txt)")
                ("bbl-file", po::value< std::string >(), "Compiled bibtex file (*.bbl) (deafult: dummy.bbl)")
                ("header", po::value< std::string >(), "Header file: appears at the beginning of every generated HTML page (default: header.txt)")
                ("footer", po::value< std::string >(), "Footer file: appears at the end of every generated HTML page (default: footer.txt)")
                ("extension", po::value< std::string >(), "Extension to put at the end of every HTML file: usually .html or .php")
                ("bibtex-database", po::value< std::string >(), "GDBM database to store BibTex entries in")
                ("venues-list", po::value< std::string >(), "List of venues with their shortened names, in format \"\"regex, e.g. SIGGRAPH \\d{4}\" ACM SIGGRAPH\" (defaults to conferences.txt)")
                ("images-root-filesystem", po::value< std::string >(), "Filesystem path for root images directory")
                ("images-root-server", po::value< std::string >(), "Server path for root images directory")
                ("server-name", po::value< std::string >(),
                   "Server name, so we can shorten URLs that start with it; e.g., "
                   "http://graphics.cs.cmu.edu/projects/something -> /projects/something.  "
                   "Should be the fully qualified domain name, e.g., \"http://graphics.cs.cmu.edu\".");

  po::options_description command_only_desc("Command line only options");
  command_only_desc.add_options()
                ("help", "produce help message")
                ("config-file", "Configuration file in format \"option = value\"");

  po::options_description cmdline_options;
  cmdline_options.add(command_only_desc).add(both_desc);

  po::positional_options_description pd;
  pd.add("config-file", 1);

  po::variables_map vm;
  try
  {
    po::store(po::command_line_parser(argc, argv).
      options(cmdline_options).positional(pd).run(), vm);
    po::notify(vm);

    if( vm.count("config-file") )
    {
      std::string configFileName = vm["config-file"].as< std::string >();
      std::ifstream ifs( configFileName.c_str() );
      if( !ifs )
      {
        std::cerr << "Unable to open configuration file '" << configFileName << "' for reading.";
        return 1;
      }

      po::store(po::parse_config_file(ifs, both_desc), vm);
      po::notify(vm);
    }
  }
  catch( po::error& err )
  {
    std::cerr << err.what() << std::endl;
    return 1;
  }

  if( vm.count("help") )
  {
    cout << cmdline_options << "\n";
    return 0;
  }

  path outputPath;
  if( vm.count("output-directory") )
  {
    std::string pathStr = vm["output-directory"].as<std::string>();
    try
    {
      outputPath = path( pathStr );
    }
    catch( boost::filesystem::filesystem_error& )
    {
      std::cerr << "Invalid path: '" << pathStr << "'\n";
      return 1;
    }
  }

  std::string headerFilename( "header.txt" );
  if( vm.count("header") )
    headerFilename = vm["header"].as<std::string>();

  std::string footerFilename( "footer.txt" );
  if( vm.count("footer") )
    footerFilename = vm["footer"].as<std::string>();

  std::string extension( "php" );
  if( vm.count("extension") )
    extension = vm["extension"].as< std::string >();

  std::string dbmFile( "bibtexEntries.gdbm" );
  if( vm.count("bibtex-database") )
    dbmFile = vm["bibtex-database"].as<std::string>();

  boost::filesystem::path bblPath( "dummy.bbl" );
  if( vm.count("bbl-file") )
  {
    std::string pathStr = vm["bbl-file"].as< std::string >();
    try
    {
      bblPath = path( pathStr );
    }
    catch( boost::filesystem::filesystem_error& )
    {
      std::cerr << "Invalid path: '" << pathStr << "'\n";
      return 1;
    }
  }

  std::string homepagesFile( "homePages.txt" );
  if( vm.count("homepage-file") )
    homepagesFile = vm["homepage-file"].as< std::string >();

  std::string venuesListFilename( "conferences.txt" );
  if( vm.count("venues-list") )
    venuesListFilename = vm["venues-list"].as<std::string>();

  std::string bibtexFilename;
  if( vm.count("bibtex-file") )
    bibtexFilename = vm["bibtex-file"].as< std::string >();

  if( vm.count("server-name") )
    serverName = vm["server-name"].as< std::string >();
  
  if( bibtexFilename.empty() )
  {
    std::cerr << "No BiBTeX file specified.\n";
    return 1;
  }

  path imagePathFilesystem;
  if( vm.count("images-root-filesystem") )
  {
    std::string pathStr = vm["images-root-filesystem"].as< std::string >();
    try
    {
      imagePathFilesystem = path( pathStr );
    }
    catch( boost::filesystem::filesystem_error& )
    {
      std::cerr << "Invalid path: '" << pathStr << "'\n";
      return 1;
    }
  }
  else
  {
    std::cerr << "No image root directory specified.\n";
    return 1;
  }

  // make sure appropriate directory structure exists
  path abstractImagesPath = imagePathFilesystem / "abstracts";
  path iconImagesPath = imagePathFilesystem / "icons";
  path sidebarImagesPath = imagePathFilesystem / "sidebar";

  std::string imageRootServer;
  if( vm.count("images-root-server") )
  {
    imageRootServer = vm["images-root-server"].as< std::string >();
  }
  else
  {
    std::cerr << "No image root path specified for server.\n";
    return 1;
  }

  try
  {
    checkDirectory( abstractImagesPath );
    checkDirectory( iconImagesPath );
    checkDirectory( sidebarImagesPath );

    HomePages homePages( homepagesFile );
    ShortenedPublications shortenedPubs( venuesListFilename );

    path summaryOutPath( outputPath / ("index." + extension + ".new") );
    ofstream summaryOut( summaryOutPath );
    if( !summaryOut )
    {
      std::cerr << "Unable to open file '" 
        << summaryOutPath.native_file_string() << "' for writing.\n";
      return 1;
    }

    // We will still keep the complete abstracts file, 
    //   in case anyone links to it
    path abstractsFullOutPath( outputPath / ("abstracts." + extension + ".new") );
    ofstream abstractsFullOut( abstractsFullOutPath );
    if( !abstractsFullOut )
    {
      std::cerr << "Unable to open file '" << 
        abstractsFullOutPath.native_file_string() << "' for writing.\n";
      return 1;
    }

    // Abstracts by year.
    boost::scoped_ptr<ofstream> abstractsYearOut;
    size_t currentYear = boost::numeric::bounds<size_t>::highest();

    // blurbs are always .php files
    path blurbsPath( outputPath / ("blurbs.php.new") );
    ofstream blurbs( blurbsPath );
    if( !blurbs )
    {
      std::cerr << "Unable to open file '" << 
        blurbsPath.native_file_string() << "' for writing.\n";
      return 1;
    }

    blurbs << "<?\n";
    blurbs << "  $blurbs = array(\n";
  
    copyFile( headerFilename, summaryOut );
    copyFile( headerFilename, abstractsFullOut );

    {
      HtmlThing div( summaryOut, "div", true, "id=\"summary\"" );

      if( !exists(bblPath) )
      {
        std::cerr << "Error opening file '" << bblPath.native_file_string() << "' for reading.\n";
        return 1;
      }

      CitationMap citations = parseBBLFile( bblPath.native_file_string() );
  
      std::vector<BTEntry> entries = parseBiBTeX( bibtexFilename );
  
      summaryOut << "<p><a href=\"abstracts." << extension << "\">See all abstracts.</a></p>\n";
  
      // we are now separating pubs in the summary list by year,
      //   so we will periodically have to end the current table
      //   and start a new one.  This method will ensure that
      //   whatever the current table is gets ended correctly
      //   when we go out of scope.
      boost::scoped_ptr<HtmlThing> table;
  
      DBMWrapper dbm( dbmFile );
  
      std::set<std::string> usedKeys;
  
      sort( entries.begin(), entries.end(), DateSorter() );
      size_t latestYear = entries.front().year();

      for( std::vector<BTEntry>::const_iterator itr = entries.begin();
        itr != entries.end();
        ++itr )
      {
        size_t newYear = std::max( itr->year(), oldestYear );
        assert( !abstractsYearOut || newYear <= currentYear );
        if( abstractsYearOut && newYear < currentYear )
        {
          yearMenu( *abstractsYearOut, latestYear, currentYear );
          copyFile( footerFilename, *abstractsYearOut );
          abstractsYearOut.reset();
          std::string filename( "abstracts." );
          filename.append( boost::lexical_cast<std::string>(currentYear) );
          filename.append( "." );
          filename.append( extension );

          swapFile( filename, outputPath );
        }

        if( newYear < currentYear )
        {
          table.reset();
          summaryOut << "<h2>" << newYear;
          if( newYear == oldestYear )
            summaryOut << " and before";
          summaryOut << "</h2>\n";
        }

        if( !abstractsYearOut )
        {
          currentYear = newYear;
          std::string filename( "abstracts." );
          filename.append( boost::lexical_cast<std::string>(currentYear) );
          filename.append( "." + extension + ".new" );
          abstractsYearOut.reset( new ofstream(outputPath / filename) );
  
          if( !abstractsYearOut->good() )
          {
            std::cerr << "Unable to open file '" + filename + "' for writing." << std::endl;
            return 1;
          }
  
          copyFile( headerFilename, *abstractsYearOut );
          yearMenu( *abstractsYearOut, latestYear, currentYear );
        }
  
        
        // We need to dump into both the year-specific abstracts file and the
        //   general abstracts file, so we'll use this handy boost utility
        //   class:
        typedef boost::iostreams::tee_device<ofstream, ofstream> ofstream_splitter_source;
        typedef boost::iostreams::stream< ofstream_splitter_source > splitter_stream;
        ofstream_splitter_source abstractsOutSplitter( abstractsFullOut, *abstractsYearOut );
        splitter_stream abstractsOut( abstractsOutSplitter );
  
        std::cout << "Handling entry '" << itr->key() << "'." << std::endl;
  
        {
          std::string key = itr->key();
          if( usedKeys.find(key) != usedKeys.end() )
          {
            std::cerr << "Duplicate key: " << key;
            return 1;
          }
  
          usedKeys.insert( key );
        }        
  
        // We need to go through all the links first so we can
        // pull out the project page in case we want to link
        // it from the image
        std::string projectPage;
        std::string linksStr;
        {
          std::ostringstream linksText;
          std::vector<std::string> links = itr->links();
          if( !itr->links().empty() )
          {
            HtmlThing ul( linksText, "ul", true );
            linksText << std::endl;
            for( std::vector<std::string>::const_iterator itr = links.begin();
              itr != links.end();
              ++itr )
            {
              std::string contentType = "";
              unsigned int response_size = 0;
              try
              {
                HttpResponse response( getHeader(*itr) );
                if( response.errorCode() >= 400 )
                {
                  // missing link
                    std::cout << "Link '" << *itr << "' returns an error code " 
                    << response.errorCode() << "; ignoring." << std::endl;
                  continue;
                }
                contentType = response.contentType();
                response_size = response.size();
              }
              catch( IOException& e )
              { 
                std::cerr << "Caught I/O exception: " << e.message() << " -- can't get link content type for '" << *itr << "'" << std::endl;
              }
  
              //std::cout << "link: " << *itr << std::endl;
              //std::cout << "contentType: " << contentType << std::endl;
  
              HtmlThing li( linksText, "li", true );
              std::string ext = "";
              for (unsigned int i = itr->size() - 1; i < itr->size(); --i)
              {
                if ((*itr)[i] == '/' || (*itr)[i] == '.') break;
                ext = (*itr)[i] + ext;
              }
              if( contentType.find( "text" ) != std::string::npos  && ext != "mov" && ext != "mp4" && ext != "avi" && ext != "pdf")
              {
                Link a( linksText, *itr );
                linksText << "Project page";
                projectPage = *itr;
              }
              else
              { 
                std::string description;
                std::vector<string> extraElements;
                if( contentType.find( "application" ) != std::string::npos )
                {
                  description = "Paper";
  
                  if( contentType.find( "pdf" ) != std::string::npos )
                    extraElements.push_back("PDF");
                  else if( contentType.find( "ps" ) != std::string::npos ||
                           contentType.find( "postscript" ) != std::string::npos )
                    extraElements.push_back("PostScript");
                  else if( contentType.find( "powerpoint" ) != std::string::npos )
                  {
                    extraElements.push_back("PowerPoint");
                    description = "Presentation";
                  }
                }
                else if( contentType.find( "video" ) != std::string::npos )
                {
                  description = "Video";
                  if( contentType.find( "quicktime" ) != std::string::npos )
                    extraElements.push_back("Quicktime");
                  else if( contentType.find( "msvideo" ) != std::string::npos )
                    extraElements.push_back("AVI");
                  else if( contentType.find( "mpeg" ) != std::string::npos )
                    extraElements.push_back("MPEG");
                }
                else
                {
                  std::cerr << "Ignoring content type / extension combo: '" << contentType << "'/'" << ext << "'." << std::endl;
                  // We'll have to use the extension instead.
                  //return 1;
                }
  
                {
                  Link a( linksText, *itr );
                  linksText << description;
                }

                if( response_size != 0 )
                {
                  double size = 
                    static_cast<double>(response_size) 
                      / static_cast<double>(kilobyte);
                  std::string units;
                  if( size >= 1024.0 )
                  {
                    size = static_cast<double>(response_size) 
                             / static_cast<double>(megabyte);
                    units = "MB";
                  }
                  else
                  {
                    units = "KB";
                  }
                  std::ostringstream oss;
                  oss << setprecision(3) << size << units;
                  extraElements.push_back( oss.str() );
                }
  
                if( !extraElements.empty() )
                {
                  linksText << " (";
                  for( std::vector<string>::const_iterator itr = extraElements.begin();
                    itr != extraElements.end();
                    ++itr )
                  {
                    if( itr != extraElements.begin() )
                      linksText << ", ";
                    linksText << *itr;
                  }
                  linksText << ")";
                }
              }
            }
          }

          // consensus decision was to _only_ link project page
          //   if there is one
          if( projectPage.empty() )
          {
            linksStr = linksText.str();
          }
          else
          {
            std::ostringstream oss;
            {
              HtmlThing ul( oss, "ul", true );
              HtmlThing li( oss, "li", true );
              Link a( oss, projectPage );
              oss << "Project page";
            }
            linksStr = oss.str();
          }
        }
  
        // store the Bibtex entry in the DBM database
        dbm.store( itr->key(), itr->bibEntry() );
  
        // dump the abstract
        HtmlThing div( abstractsOut, "div", true, "class=\"abstract\" id=\"" + itr->key() + "\"" );
  
        abstractsOut << std::endl;

        std::string iconImageName;
        Magick::Image iconImage;

        if( !itr->iconImage().empty() )
        {
          ImageResponse response( getImage( itr->iconImage() ) );
          if( response.errorCode() >= 400 )
          {
            std::cout << "Icon image '" << itr->iconImage() << "' returned error code "
              << response.errorCode() << "; ignoring." << std::endl;
          }
          else
          {
            iconImage = response.image();
          }
        }

        Magick::Image sidebarImage;
        if( !itr->sidebarImage().empty() )
        {
          ImageResponse response( getImage( itr->sidebarImage() ) );
          if( response.errorCode() >= 400 )
          {
            std::cout << "Sidebar image '" << itr->sidebarImage() 
                      << "' returned error code " << response.errorCode() 
                      << "; ignoring." << std::endl;
          }
          else
          {
            sidebarImage = response.image();
          }
        }


        if( !itr->image().empty() )
        {
          ImageResponse response( getImage( itr->image() ) );
          if( response.errorCode() >= 400 )
          {
            std::cout << "Image '" << itr->image() << "' returned error code " 
                      << response.errorCode() << "; ignoring." << std::endl;
          }
          else
          {
            Magick::Image image = response.image();

            // if no icon image specified, just use the image from the
            //   abstracts page
            if( isEmptyImage( iconImage ) )
            {
              iconImage = image;
            }

            // Same for the sidebar
            if( isEmptyImage( sidebarImage ) )
            {
              sidebarImage = image;
            }

            Magick::Geometry geom = image.size();
            double aspectRatio = 
              static_cast<double>( geom.width() ) / 
              static_cast<double>( geom.height() );

            // The image can't be more than 250px wide and 400px tall
            const size_t maxWidth = 250;
            const size_t maxHeight = 400;

            if( geom.width() > maxWidth )
            {
              geom.width( maxWidth );
              geom.height( boost::numeric_cast<double>(maxWidth) / aspectRatio );
            }

            if( geom.height() > maxHeight )
            {
              geom.height( maxHeight );
              geom.width( boost::numeric_cast<double>(maxHeight) * aspectRatio );
            }

            if( geom != image.size() )
            {
              image.scale( geom );
            }

            std::string imageName = itr->key() + std::string(".jpg");
            image.write( (abstractImagesPath / imageName).native_file_string() );

            boost::scoped_ptr<Link> projectLink;
            if( !projectPage.empty() )
              projectLink.reset( new Link(abstractsOut, projectPage) );
            abstractsOut << "<img src=\"" << imageRootServer << "/abstracts/" << imageName << 
                            "\" alt=\"\" width=\"" << geom.width() << "\" height=\"" << 
                            geom.height() << "\" />\n";
          }
        }

        if( !isEmptyImage( iconImage ) )
        {
          Magick::Geometry geom = iconImage.size();
          double aspectRatio = 
            static_cast<double>( geom.width() ) / 
            static_cast<double>( geom.height() );
  
          size_t iconImageWidth = geom.width();
          size_t iconImageHeight = boost::numeric_cast<size_t>(
            boost::numeric_cast<double>(iconWidth) / aspectRatio);

          geom.width( iconImageWidth );
          geom.height( iconImageHeight );
          iconImage.scale( geom );

          iconImageName = itr->key() + std::string(".jpg");
          const path filename = iconImagesPath / iconImageName;
          iconImage.write( filename.native_file_string() );
        }


        std::string abstractsPage( "abstracts." );
        abstractsPage.append( 
          boost::lexical_cast<std::string>( std::max(itr->year(), oldestYear)) );
        abstractsPage.append( "." + extension + "#" );
        abstractsPage.append( itr->key() );

        // Title needs to go after the <img> so that they appear 
        //  side-by-side in the page
        abstractsOut << "<h2>" << formatLatexAsHTML(itr->title()) << "</h2>" << std::endl;
        if( !table )
        {
          const unsigned int iconCellWidth = iconWidth + 5;
          table.reset( new HtmlThing( 
            summaryOut, "table", true, "class=\"sortable\"" ) );
          summaryOut << std::endl;
          summaryOut << "<tr><th style=\"width: " << iconCellWidth 
                     << "\"></th><th>Title</th><th style=\"width: 30%\">" 
                     << "Author(s)</th></tr>" << std::endl;
        }
  
        TableRow tableRow(summaryOut);
        {
          TableCell yearCell(summaryOut);
          if( !iconImageName.empty() )
          {
            Link ref( summaryOut, abstractsPage );
            Magick::Geometry geom = iconImage.size();
            summaryOut << "<img src=\"" << imageRootServer << "/icons/" << iconImageName 
                       << "\" width=\"" << geom.width() 
                       << "\" height=\"" << geom.height() 
                       << "\" alt=\"\" />";
          }
        }
        {
          TableCell titleCell(summaryOut);
          {
            HtmlThing cite( summaryOut, "cite" );
            {
              Link ref( summaryOut, abstractsPage );
              summaryOut << formatLatexAsHTML(itr->title());
            }
          }

          std::string pubName = itr->journal();
          if( pubName.empty() )
            pubName = itr->booktitle();
          if( !pubName.empty() )
          {
            std::string shortenedPubName = shortenedPubs.shortName(pubName);
            if( shortenedPubName.empty() )
            {
              std::cout << "Warning: no publication abbreviation found for '"
                << pubName << "'." << std::endl;
            }
            else
            {
              summaryOut << "<div class=\"venue\">" << shortenedPubName << "</div>";
            }
          }
        }
  
        {
          HtmlThing authorsPar(abstractsOut, "p", true);
          TableCell authorsCell(summaryOut);
          std::vector<Author> authors = itr->authors();
          for( std::vector<Author>::const_iterator authorItr = authors.begin();
            authorItr != authors.end();
            ++authorItr )
          {
            if( authorItr != authors.begin() )
            {
              summaryOut << ", ";
              if( authors.size() == 2 )
                abstractsOut << " ";
              else
                abstractsOut << ", ";
            }
   
            if( authorItr + 1 == authors.end() && authorItr != authors.begin() )
              abstractsOut << "and ";
  
            std::string homePage = homePages.pageForName( *authorItr );
            std::vector< boost::shared_ptr<Link> > links;
            if( !homePage.empty() )
            {
              links.push_back( boost::shared_ptr<Link>(new Link( summaryOut, homePage ) ) );
              links.push_back( boost::shared_ptr<Link>(new Link( abstractsOut, homePage ) ) );
            }
  
            if( !authorItr->von().empty() )
              summaryOut << formatLatexAsHTML(authorItr->von()) << " ";
            summaryOut << formatLatexAsHTML(authorItr->last());
  
            abstractsOut << formatLatexAsHTML(authorItr->toString());
          }
        }
  
        {
          if( !itr->abstract().empty() )
          {
            abstractsOut << "<h3>Abstract</h3>" << std::endl;
            HtmlThing p( abstractsOut, "p", true );
            abstractsOut << stripBraces(itr->abstract());
          }
  
          {
            abstractsOut << "<h3>Citation</h3>" << std::endl;
            HtmlThing p( abstractsOut, "p", true );
            CitationMap::const_iterator citationItr = citations.find( itr->key() );
            if( citationItr == citations.end() )
            {
              throw ParserException( "Missing citation: '" + itr->key() + "'." );
            }
            abstractsOut << citationItr->second;
            abstractsOut << " [<a href=\"/cgi-bin/bibtexEntry?key=" << 
                itr->key() << 
                "\" target=\"_blank\" onclick=\"link_popup(this); return false\">BiBTeX</a>]";
          }
  
          if( !linksStr.empty() )
          {
            abstractsOut << "<h3>Links</h3>" << std::endl;
            abstractsOut << linksStr;
          }
        }


        std::string sidebarText = itr->sidebarText();
        if( !sidebarText.empty() )
        {
          if( isEmptyImage( sidebarImage ) )
          {
            std::cout << "No image associated with sidebar text; ignoring." 
              << std::endl;
          }
          else
          {
            const unsigned int maxWidth = 200;
  
            Magick::Geometry geom = sidebarImage.size();
            if( geom.width() > maxWidth )
            {
              double aspectRatio = 
                static_cast<double>( geom.width() ) / 
                static_cast<double>( geom.height() );
  
              geom.width( maxWidth );
              geom.height( static_cast<unsigned int>(static_cast<double>(maxWidth) / aspectRatio) );
              sidebarImage.scale( geom );
            }

            const std::string imageName = itr->key() + ".jpg";
            const path filename = sidebarImagesPath / imageName;
            sidebarImage.write( filename.native_file_string() );
  
            std::string sidebarText = itr->sidebarText();
            if( sidebarText.empty() )
            {
              std::cerr << "Sidebar image for entry " << itr->key() << 
                " is missing sidebarText." << std::endl;
              return 1;
            }

            std::ostringstream blurb;
            {
              boost::scoped_ptr<Link> a;
              if( !projectPage.empty() )
              a.reset( new Link( blurb, projectPage ) );
              blurb << "<img src=\"" << imageRootServer << "/sidebar/" << itr->key() << ".jpg\" "
                    << "width=\"" << geom.width() 
                    << "\" height=\"" << geom.height() << "\" alt=\"\" />";
            }
            blurb << "<h2>" << stripBraces(sidebarText) << "</h2>";
  
            blurbs << "    " << phpEscapedString(blurb.str()) << ",\n";
          }
        }
  
        std::cout << "Completed entry '" << itr->key() << "'." << std::endl;
      }
  
      if( abstractsYearOut )
      {
        yearMenu( *abstractsYearOut, latestYear, currentYear );
        copyFile( footerFilename, *abstractsYearOut );
        abstractsYearOut.reset();
        std::string filename( "abstracts." );
        filename.append( boost::lexical_cast<std::string>(currentYear) );
        filename.append( "." );
        filename.append( extension );

        swapFile( filename, outputPath );
      }
  
      blurbs << "  );\n";
      blurbs << "?>\n";
    }
  
    copyFile( footerFilename, summaryOut );
    copyFile( footerFilename, abstractsFullOut );
    summaryOut.close();
    abstractsFullOut.close();

    swapFile( "index." + extension, outputPath );
    swapFile( "abstracts." + extension, outputPath );
    swapFile( "blurbs.php", outputPath );
  }
  catch( IOException& e )
  { 
    std::cerr << "Caught I/O exception: " << e.message() << std::endl;
    return 1;
  }
  catch( ParserException& e )
  {
    std::cerr << "Found error while parsing: '" 
      << e.message() << "'." << std::endl;
    return 1;
  }

}

