/*****************************************************************************
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 "exception.h"
#include "utility.h"

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

#include <xercesc/util/PlatformUtils.hpp>
#include <xercesc/dom/DOM.hpp>
#include <xercesc/dom/DOMImplementation.hpp>
#include <xercesc/dom/DOMImplementationLS.hpp>
#include <xercesc/dom/DOMError.hpp>


#include <xercesc/framework/StdOutFormatTarget.hpp>
#include <xercesc/framework/LocalFileFormatTarget.hpp>
#include <xercesc/parsers/XercesDOMParser.hpp>
#include <xercesc/util/XMLUni.hpp>
#include <xercesc/util/XercesDefs.hpp>
#include <xercesc/sax/ErrorHandler.hpp>
#include <xercesc/sax/SAXParseException.hpp>

#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/spirit/core.hpp>
#include <boost/spirit/attribute.hpp>
#include <boost/spirit/utility/chset.hpp>
#include <boost/spirit/utility/escape_char.hpp>
#include <boost/spirit/utility/confix.hpp>
#include <boost/spirit/iterator.hpp>

#include <boost/utility.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/scoped_array.hpp>

using namespace std;
using namespace xercesc;
using namespace boost::spirit;

class XMLPlatformUtilsInit
{
public:
  XMLPlatformUtilsInit()
  {
    // Initialize the XML4C2 system
    XMLPlatformUtils::Initialize();
  }

  ~XMLPlatformUtilsInit()
  {
    XMLPlatformUtils::Terminate();
  }

};

class XMLChWrapper : public boost::noncopyable
{
public:
  XMLChWrapper( const char* str )
    : xmlForm_( XMLString::transcode(str) )  {}

  ~XMLChWrapper()
  {
    XMLString::release(&xmlForm_);
  }

  XMLCh* get() { return xmlForm_; }

private:
  XMLCh* xmlForm_;
};

class StrX : public boost::noncopyable
{
public :
  StrX(const XMLCh* const toTranscode)
    : localForm_( XMLString::transcode(toTranscode) ) {}

  ~StrX()
  {
      XMLString::release(&localForm_);
  }

  const char* localForm() const
  {
      return localForm_;
  }

private :
    //      This is the local code page form of the string.
    char*   localForm_;
};

std::ostream& operator<<(std::ostream& target, const StrX& toDump)
{
    target << toDump.localForm();
    return target;
}

class SimpleDOMErrorHandler
  : public ErrorHandler
{
public:
  SimpleDOMErrorHandler()
    : errors_(0), warnings_(0) {}

  unsigned int errors() const { return errors_; }
  unsigned int warnings() const { return warnings_; }

  void warning(const SAXParseException& toCatch)
  {
    std::cerr << cerr << "\nWarning at file ";
    message( toCatch );
    ++warnings_;
  }

  void error(const SAXParseException& toCatch)
  {
    ++errors_;
    std::cerr << "\nError at file ";
    message( toCatch );
  }

  void fatalError(const SAXParseException& toCatch)
  {
    ++errors_;
    std::cerr << "\nFatal Error at file ";
    message( toCatch );
  }

  void resetErrors()  { errors_ = 0; warnings_ = 0; }



private:

  void message(const SAXParseException& toCatch)
  {
    StrX systemId( toCatch.getSystemId() );
    StrX message( toCatch.getMessage() );
    std::cerr <<  "\"" << systemId
      << "\", line " << toCatch.getLineNumber()
      << ", column " << toCatch.getColumnNumber()
      << "\n   Message: " << message << std::endl;
  }


  SimpleDOMErrorHandler(const SimpleDOMErrorHandler&);
  void operator=(const SimpleDOMErrorHandler&);
  unsigned int errors_;
  unsigned int warnings_;
};


DOMElement* asElement( DOMNode* node )
{
  if( node->getNodeType() != DOMNode::ELEMENT_NODE )
    throw ParserException( "Expecting element" );
  return dynamic_cast<DOMElement*>(node);
}

std::string optionalAttribute( DOMElement* element, const char* attributeName )
{
  XMLChWrapper xmlAttributeName( attributeName );
  StrX res( element->getAttribute( xmlAttributeName.get() ) );
  return std::string( res.localForm() );
}

std::string requiredAttribute( DOMElement* element, const char* attributeName )
{
  XMLChWrapper xmlAttributeName( attributeName );
  StrX res( element->getAttribute( xmlAttributeName.get() ) );
  if( strlen( res.localForm() ) == 0 )
    throw ParserException( 
      std::string("Expecting: '") + std::string(attributeName) + std::string("'.") );
  return std::string( res.localForm() );
}

unsigned int toInt( const std::string& value )
{
  unsigned int res;
  parse_info<char const*> info = parse( &value[0],
    uint_p[assign(res)],
    space_p );
  if( !info.hit )
  {
    std::ostringstream errMsg;
    errMsg << "Expected uint but got '" << value << "'.";
    throw ParserException( errMsg.str() );
  }
  return res;
}

std::string removeSpaces( const std::string& in )
{
  std::string result;
  for( std::string::const_iterator itr = in.begin();
       itr != in.end();
       ++itr )
  {
    if( !isspace( *itr ) )
      result.push_back( *itr );
  }

  return result;
}

class Course
{
public:
  class Offering
  {
  public:
    enum Semester
    {
      SPRING = 0,
      SUMMER = 1,
      FALL = 2
    };

    Offering( Semester semester, 
      unsigned int year, 
      const std::string& instructor,
      const std::string& link )
      : semester_(semester), year_(year), instructor_(instructor), link_(link) {}

    Semester semester() const			{ return semester_; }
    std::string semesterAsString() const
    {
      switch( semester_ )
      {
        case SPRING: return "Spring";
        case SUMMER: return "Summer";
        case FALL:   return "Fall";
        default:     assert( false ); return "Error";
      }
    }

    std::string link() const			{ return link_; }
    std::string instructor() const		{ return instructor_; }
    unsigned int year() const			{ return year_; }

    bool operator<( const Offering& other ) const
    {
      if( year() < other.year() )
        return true;
      if( year() == other.year() && semester() < other.semester() )
        return true;
      return false;
    }

  private:
    Semester semester_;
    unsigned int year_;
    std::string instructor_;
    std::string link_;
  };

  Course( const std::string& number,
    const std::string& name,
    const std::string& description,
    const std::string& image )
    : number_( number ), name_( name ), description_( description ), image_( image ) {}

  std::string number() const		{ return number_; }
  std::string name() const			{ return name_; }
  std::string description() const	{ return description_; }
  std::string image() const			{ return image_; }

  void addOffering( const Offering& offering )
  {
    typedef std::deque<Offering>::iterator OfferingItr;
    OfferingItr itr = std::upper_bound( offerings_.begin(), offerings_.end(), offering );
    offerings_.insert( itr, offering );

//    std::sort( offerings_.begin(), offerings_.end() );
  }

  Offering lastOffering() const
  {
    return offerings_.back();
  }

  unsigned int numOfferings() const             { return offerings_.size(); }
  Offering offering( unsigned int num ) const   { return offerings_[num]; }

private:
  std::string number_;
  std::string name_;
  std::string description_;
  std::string image_;

  std::deque< Offering > offerings_;
};

class RecentOfferingCourseComparator
{
public:
  bool operator()( const Course& left, const Course& right ) const
  {
    if( right.lastOffering() < left.lastOffering() )
      return true;
    if( left.lastOffering() < right.lastOffering() )
      return false;

    return left.number() < right.number();
  }
};

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()
                ("courses-file", po::value< std::string >(), "Courses file (default: courses.xml)")
                ("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)")
                ("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)")
                ("other-courses-file", po::value< std::string >(), "Extra material to appear at end of courses page (e.g., extra courses).  Default: other.txt")
                ("extension", po::value< std::string >(), "Extension to put at the end of every HTML file: usually .html or .php")
                ("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;
  }

  std::string coursesFile( "courses.xml" );
  if( vm.count("courses-file") )
    coursesFile = vm["courses-file"].as< std::string >();

  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 otherCoursesFilename( "other.txt" );
  if( vm.count("other-courses-file") )
    otherCoursesFilename = vm["other-courses-file"].as<std::string>();

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

  if( vm.count("server-name") )
    serverName = vm["server-name"].as< std::string >();


  boost::scoped_ptr<XMLPlatformUtilsInit> init;
  try
  {
    init.reset( new XMLPlatformUtilsInit );
  }
  catch(const XMLException &toCatch)
  {
    StrX message( toCatch.getMessage() );
    std::cerr << "Error during Xerces-c Initialization.\n"
       << "  Exception message:"
       << message << std::endl;
    return 1;
  }

  try
  {
    XercesDOMParser parser;
    SimpleDOMErrorHandler errHandler;
    parser.setErrorHandler(&errHandler);
    parser.parse( coursesFile.c_str() );

    HomePages homePages( homepagesFile );

    std::cout << "Parsed \"" << coursesFile << "\"; found " << 
      errHandler.errors() << " errors and " << 
      errHandler.warnings() << " warnings." << std::endl;

    if( errHandler.errors() != 0 )
    {
      std::cerr << "Errors occurred in parsing." << std::endl;
      return 1;
    }

    DOMDocument* doc = parser.getDocument();
    XMLChWrapper xmlCourses( "course" );
    DOMNodeList* coursesElts = doc->getElementsByTagName(
      xmlCourses.get() );
    std::cout << "Found " << coursesElts->getLength() << " courses." << std::endl;

    std::deque< Course > courses;
    for( XMLSize_t index = 0; index < coursesElts->getLength(); ++index )
    {
      DOMElement* courseElement = asElement( coursesElts->item(index) );
      courses.push_back( 
        Course( 
          requiredAttribute( courseElement, "number" ),
          requiredAttribute( courseElement, "name" ),
          requiredAttribute( courseElement, "description" ),
          optionalAttribute( courseElement, "image" ) ) );
      std::cout << "Found course " << courses.back().number() << ": " << courses.back().name() << std::endl;
      XMLChWrapper xmlOffering("offering");
      DOMNodeList* offerings = courseElement->getElementsByTagName(
        xmlOffering.get() );
      std::cout << "  Found " << offerings->getLength() << " offerings." << std::endl;

      for( unsigned int iOffering = 0; iOffering < offerings->getLength(); ++iOffering )
      {
        DOMElement* offeringElement = asElement( offerings->item(iOffering) );
        std::string semester = requiredAttribute( offeringElement, "semester" );
        Course::Offering::Semester sem;
 
        if( semester == "spring" || semester == "Spring" )
          sem = Course::Offering::SPRING;
        else if( semester == "fall" || semester == "Fall" 
              || semester == "autumn" || semester == "Autumn" )
          sem = Course::Offering::FALL;
        else if( semester == "summer" || semester == "Summer" )
          sem = Course::Offering::SUMMER;
        else
          throw ParserException( "Expected 'spring' or 'fall' for semester attribute." );

        courses.back().addOffering( 
          Course::Offering(
            sem,
            toInt( requiredAttribute( offeringElement, "year" ) ),
            requiredAttribute( offeringElement, "instructor" ),
            optionalAttribute( offeringElement, "href" ) ) );
      }
    }

    std::sort( courses.begin(), courses.end(), RecentOfferingCourseComparator() );

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

    path descriptionsOutPath( outputPath / ("descriptions." + extension + ".new") );
    ofstream descriptionsOut( descriptionsOutPath );
    if( !descriptionsOut.good() )
    {
      std::cerr << "Unable to open file '"
				<< descriptionsOutPath.native_file_string() << "' for writing." 
        << std::endl; 
      return 1;
    }

    copyFile( headerFilename, summaryOut );
    copyFile( headerFilename, descriptionsOut );

    {
      HtmlThing table( summaryOut, "table", true, "class=\"sortable\" id=\"summary\"" );
      summaryOut << "<tr><th width=\"90\">Number</th><th>Course name</th><th>Offerings</th></tr>" << std::endl;

      for( std::deque<Course>::const_iterator coursesItr = courses.begin();
        coursesItr != courses.end(); 
        ++coursesItr )
      {
        std::string key = removeSpaces( coursesItr->name() );
  
        HtmlThing div( descriptionsOut, "div", true, "class=\"abstract\" id=\"" + key + "\"" );
        descriptionsOut << std::endl;
        if( !coursesItr->image().empty() )
        {
          ImageResponse response( getImage( coursesItr->image() ) );
          descriptionsOut << "<img src=\"" << coursesItr->image() << "\" alt=\"\" width=\"" 
            << response.width() << "\" height=\"" << response.height() << "\" />" << std::endl;
        }
  
        descriptionsOut << "<h2>" << coursesItr->name() << "</h2>" << std::endl;
        descriptionsOut << "<p>" << coursesItr->description() << "</p>" << std::endl;
  
        TableRow tableRow(summaryOut);
        {
          TableCell numberCell(summaryOut, true);
          summaryOut << coursesItr->number();
        }
        {
          TableCell titleCell(summaryOut, true);
          {
            Link ref( summaryOut, "descriptions.php#" + key );
            summaryOut << coursesItr->name();
          }
        }
  
        TableCell offeringsCell(summaryOut, true);
        if( coursesItr->numOfferings() != 0 )
        {
          descriptionsOut << "<h3>Offerings</h3>" << std::endl;
  
          HtmlThing ul(descriptionsOut, "ul", true );
          descriptionsOut << std::endl;

          for( int iOffering = coursesItr->numOfferings() - 1; iOffering >= 0; --iOffering )
          {  
            HtmlThing li( descriptionsOut, "li", true );
  
            if( iOffering != static_cast<int>(coursesItr->numOfferings()) - 1 )
              summaryOut << ", ";

            Course::Offering offering = coursesItr->offering( iOffering );
            std::string href = offering.link();
            std::string semester = offering.semesterAsString();
            std::string instructor = offering.instructor();
            unsigned int year = offering.year();
  
            {
              boost::scoped_ptr<Link> absLink;
              boost::scoped_ptr<Link> sumLink;
              if( !href.empty() )
              {
                absLink.reset( new Link( descriptionsOut, href ) );
                sumLink.reset( new Link( summaryOut, href ) );
              }
  
              summaryOut << semester << " " << year;
              descriptionsOut << semester << " " << year;
            }
  
            {
              descriptionsOut << " (";
  
              std::vector<Author> instructors = authors( instructor.c_str() );
              for( std::vector<Author>::const_iterator itr = instructors.begin();
                   itr != instructors.end();
                   ++itr )
              {
                if( itr != instructors.begin() )
                  descriptionsOut << " and ";
                boost::scoped_ptr<Link> absLink;
                std::string homePage = homePages.pageForName( *itr );
                if( !homePage.empty() )
                   absLink.reset( new Link( descriptionsOut, homePage ) );
                descriptionsOut << itr->toString();
              }

              descriptionsOut << ")";
            }
          }
        }
      }
    }

    copyFile( otherCoursesFilename, summaryOut );

    copyFile( footerFilename, summaryOut );
    copyFile( footerFilename, descriptionsOut );

    summaryOut.close();
    descriptionsOut.close();

    swapFile( "index." + extension, outputPath );
    swapFile( "descriptions." + extension, outputPath );
  }
  catch( IOException& e )
  { 
    std::cerr << "Caught I/O exception: " << e.message() << std::endl;
    return 1;
  }
  catch(const XMLException &toCatch)
  {
    StrX message( toCatch.getMessage() );
    std::cerr << "An error occurred during parsing." << std::endl
       << "  Exception message:"
       << message << std::endl;
    return 1;
  }
  catch (const DOMException& e)
  {
    const unsigned int maxChars = 2047;
    XMLCh errText[maxChars + 1];

    std::cerr << "\nDOM Error during parsing: '" << coursesFile << "'\n"
           << "DOMException code is:  " << e.code << std::endl;

    StrX errorMessage( errText );
    if (DOMImplementation::loadDOMExceptionMsg(e.code, errText, maxChars))
       std::cerr << "Message is: " << errorMessage << std::endl
;

    return 1;
  }
  catch( ParserException& e )
  {
    std::cout << "Caught parser exception: " << e.message() << std::endl;
    return 1;
  }
}

