// (c) 2008 Sebastian Redl
// Released under the GPLv2.

#define BOOST_DISABLE_THREADS

#include <ostream>
#include <iostream>
#include <string>
#include <algorithm>
#include <utility>
#include <tr1/unordered_map>
#include <boost/program_options.hpp>
#include <boost/interprocess/file_mapping.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <boost/filesystem.hpp>

namespace po = boost::program_options;
namespace ip = boost::interprocess;
namespace fs = boost::filesystem;

// The possible modes of sorting footnotes.
enum sort_mode
{
       sort_by_text,
       sort_by_footnote
};

// Input operator for the enum.
std::istream &operator >>(std::istream &is, sort_mode &sm)
{
       std::string s;
       is >> s;
       if(s == "text") {
               sm = sort_by_text;
       } else if(s == "footnote") {
               sm = sort_by_footnote;
       } else {
               is.setstate(std::ios_base::failbit);
       }
       return is;
}

// Output operator for the enum.
std::ostream &operator <<(std::ostream &os, sort_mode sm)
{
       return os << (sm == sort_by_text ? "text" : "footnote");
}

bool parse_command_line(int argc, char **argv,
       sort_mode &sm, std::string &input_file);

std::tr1::unordered_map< int, int>
       g_footnote_number_translations(10000);
int g_footnote_count = 0;

inline int lookup_footnote_translation(int number)
{
       int &r = g_footnote_number_translations[number];
       if(r == 0) {
               r = ++g_footnote_count;
       }
       return r;
}

inline bool isdig(char c)
{
       return c >= '0' && c <= '9';
}

void load_footnote_order(const char *first, const char *last);
void copy_text(const char *first, const char *last, std::ostream &os);
void sort_footnotes(const char *first, const char *last, std::ostream &os);

int main(int argc, char **argv)
{
       std::ios::sync_with_stdio(false);

       sort_mode sm = sort_by_text;
       std::string input_file;

       if(!parse_command_line(argc, argv, sm, input_file)) {
               return 1;
       }

       // Memory-map the input file.
       ip::file_mapping input_map(input_file.c_str(), ip::read_only);
       std::size_t input_size = fs::file_size(fs::path(input_file));
       ip::mapped_region mapping(input_map, ip::read_only, 0, input_size);

       const char *text_start = static_cast<const char*>(mapping.get_address());
       const char *footnote_end = text_start + input_size;

       // Locate the footnote marker. Search in reverse, since it's probably close
       // to the end.
       const char * const MARKER_START = "\n@footnote:\n";
       const char * const MARKER_END = MARKER_START + 12;

       const char* loc = std::find_end(text_start, footnote_end,
               MARKER_START, MARKER_END);
       if(loc == footnote_end) {
               throw std::runtime_error("No footnote section.");
       }

       // Include the last newline.
       const char *text_end = loc + 1;
       const char *footnote_start = loc + 12;

       if(sm == sort_by_footnote) {
               load_footnote_order(footnote_start, footnote_end);
       }

       copy_text(text_start, text_end, std::cout);
       std::cout << "@footnote:\n";
       sort_footnotes(footnote_start, footnote_end, std::cout);
}

bool parse_command_line(int argc, char **argv,
       sort_mode &sm, std::string &input_file)
{
       po::options_description visible_desc("Allowed options");
       visible_desc.add_options()
               ("help,h", "produce help message")
               ("sort-by,s", po::value<sort_mode>(&sm)->default_value(sort_by_text),
                       "sort by order of in-text appearance ('text') or in-footnotes "
                       "appearance ('footnote')")
               ;

       po::options_description hidden_desc("positional options");
       hidden_desc.add_options()("input", po::value<std::string>(&input_file),
               "input file (must be real file)");

       po::positional_options_description positional;
       positional.add("input", 1);

       po::options_description merged("all options");
       merged.add(visible_desc).add(hidden_desc);

       po::variables_map vm;
       try {
               po::store(
                       po::command_line_parser(argc, argv)
                               .options(merged).positional(positional).run(),
                       vm);
               po::notify(vm);
       } catch(po::error &e) {
               std::cerr
                       << "Command line error: " << e.what() << "\n\n"
                       << "Usage: babylon [<options>] <input-file>\n"
                       << visible_desc
                       << '\n';
               return false;
       }

       if(vm.count("help")) {
               std::cerr
                       << "Babylon Challenge in C++, Sebastian Redl "
                          "<[email protected]>\n"
                       << "Usage: babylon [<options>] <input-file>\n"
                       << visible_desc
                       << '\n';
               return false;
       }
       return true;
}

void load_footnote_order(const char *first, const char *last)
{
       for( ; first != last; ++first) {
               if(*first == '[') {
                       int num = 0;
                       for(++first; first != last && isdig(*first); ++first) {
                               num *= 10;
                               num += *first - '0';
                       }
                       if(first == last) {
                               break;
                       }
                       if(*first == ']' && num != 0) {
                               lookup_footnote_translation(num);
                       }
               }
       }
}

void copy_text(const char *first, const char *last, std::ostream &os)
{
       const char *last_end = first;
       for( ; first != last; ++first) {
               if(*first == '[') {
                       int num = 0;
                       const char *f = first + 1;
                       for( ; f != last && isdig(*f); ++f) {
                               num *= 10;
                               num += *f - '0';
                       }
                       if(f == last) {
                               break;
                       }
                       if(*f == ']' && num != 0) {
                               os.write(last_end, first - last_end + 1);
                               os << lookup_footnote_translation(num);
                               last_end = first = f;
                       }
               }
       }
       os.write(last_end, last - last_end);
}

void sort_footnotes(const char *first, const char *last, std::ostream &os)
{
       typedef std::pair<const char*, const char*> txtrange;
       typedef std::vector<txtrange> txtmap;
       txtmap order(g_footnote_count, txtrange(0, 0));

       for( ; first != last ; ++first) {
               if(*first == '[') {
                       int num = 0;
                       ++first;
                       for( ; first != last && isdig(*first); ++first) {
                               num *= 10;
                               num += *first - '0';
                       }
                       if(first == last) {
                               break;
                       }
                       const char *fnbegin = first;
                       for( ; first != last && *first != '\n'; ++first) {
                       }
                       int idx = lookup_footnote_translation(num);
                       order[idx-1] = txtrange(fnbegin, first);
                       if(first == last) {
                               std::cerr << "Warning: File doesn't end with a newline.\n";
                               break;
                       }
               } else {
                       //std::cerr << "Warning: Bad '" << *first << "' in footnotes.\n";
               }
       }

       for(std::size_t i = 0, len = order.size(); i < len; ++i) {
               const txtrange &r = order[i];
               if(r.first == 0) {
                       os << '[' << (i+1) << "] Error: No footnote text found.\n";
                       std::cerr << "Warning: No footnote for reference " << (i+1) << '\n';
                       continue;
               }
               os << '[' << (i+1);
               os.write(r.first, r.second - r.first);
               os << '\n';
       }
}