// @(#)root/test:$name:  $:$id: filter.cxx,v 1.0 exp $
// Author: O.Couet

/// The ROOT doxygen filter implements ROOT's specific directives used to generate
/// the ROOT reference guide.
///
/// ## In the ROOT classes
///
/// ### `Begin_Macro` and `End_Macro`
/// The two tags where used the THtml version to generate images from ROOT code.
/// The generated picture is inlined exactly at the place where the macro is
/// defined. The Macro can be defined in two way:
///  - by direct in-lining of the C++ code
///  - by a reference to a C++ file
/// The tag `Begin_Macro` can have the parameter `(source)`. The directive becomes:
/// `Begin_Macro(source)`. This parameter allows to show the macro's code in addition.
/// `Begin_Macro` also accept the image file type as option. "png" or "svg".
/// "png" is the default value. For example: `Begin_Macro(source, svg)` will show
/// the code of the macro and the image will be is svg format. The "width" keyword
/// can be added to define the width of the picture in pixel: "width=400" will
/// scale a picture to 400 pixel width. This allow to define large pictures which
/// can then be scaled down to have a better definition.
///
/// ## In the ROOT tutorials
///
/// ROOT tutorials are also included in the ROOT documentation. The tutorials'
/// macros headers should look like:
///
/// ~~~ {.cpp}
/// \file
/// \ingroup tutorial_hist
/// \notebook
/// Getting Contours From TH2D.
///
/// #### Image produced by `.x ContourList.C`
/// The contours values are drawn next to each contour.
/// \macro_image
///
/// #### Output produced by `.x ContourList.C`
/// It shows that 6 contours and 12 graphs were found.
/// \macro_output
///
/// #### `ContourList.C`
/// \macro_code
///
/// \authors  Josh de Bever, Olivier Couet
/// ~~~
///
/// This example shows that four new directives have been implemented:
///
///  1. `\macro_image`
///  The images produced by this macro are shown. A caption can be added to document
///  the pictures: `\macro_image This is a picture`. When the option `(nobatch)`
///  is passed, the macro is executed without the batch option.
///  Some tutorials generate pictures (png or pdf) with `Print` or `SaveAs`.
///  Such pictures can be displayed with `\macro_image (picture_name.png[.pdf])`
///  When the option (tcanvas_js) is used the image is displayed as JavaScript.
///  For ROOT 7 tutorials, when the option (rcanvas_js) is used the image is displayed as json file.
///
///  2. `\macro_code`
///  The macro code is shown.  A caption can be added: `\macro_code This is code`
///
///  3. `\macro_output`
///  The output produced by this macro is shown. A caption can be added:
///  `\macro_output This the macro output`
///
///  4. `\notebook`
///    To generate the corresponding jupyter notebook. In case the tutorial does
///    not generate any graphics output, the option `-nodraw` should be added.
///
/// Note that the doxygen directive `\authors` or `\author` must be the last one
/// of the macro header.

#include <unistd.h>
#include <stdio.h>
#include <string>
#include <cstring>
#include <iostream>
#include <fstream>
#include <stdlib.h>
#include <stdarg.h>
#include <memory>

using std::string, std::ios_base, std::unique_ptr;

// Auxiliary functions
void   FilterClass();
void   FilterTutorial();
void   GetClassName();
int    NumberOfImages();
string ImagesList(string&);
void   ExecuteMacro();
void   ExecuteCommand(string);
void   ReplaceAll(string&, const string&, const string&);
string StringFormat(const string fmt_str, ...);
bool   EndsWith(string const &, string const &);
bool   BeginsWith(const string&, const string&);

// Global variables.
FILE  *f;              // Pointer to the file being parsed.
char   gLine[255];     // Current line in the current input file
string gFileName;      // Input file name
string gLineString;    // Current line (as a string) in the current input file
string gClassName;     // Current class name
string gImageName;     // Current image name
string gMacroName;     // Current macro name
string gImageType;     // Type of image used to produce pictures (png, svg ...)
string gImageWidth;    // Width of image
string gCwd;           // Current working directory
string gOutDir;        // Output directory
string gSourceDir;     // Source directory
string gPythonExec;    // Python executable
string gOutputName;    // File containing a macro std::out
bool   gHeader;        // True if the input file is a header
bool   gSource;        // True if the input file is a source file
bool   gPython;        // True if the input file is a Python script.
bool   gImageSource;   // True the source of the current macro should be shown
int    gInMacro;       // >0 if parsing a macro in a class documentation.
int    gImageID;       // Image Identifier.
int    gMacroID;       // Macro identifier in class documentation.


////////////////////////////////////////////////////////////////////////////////
/// Filter ROOT files for Doxygen.

int main(int argc, char *argv[])
{
   // Initialisation
   gFileName      = argv[1];
   gHeader        = false;
   gSource        = false;
   gPython        = false;
   gImageSource   = false;
   gInMacro       = 0;
   gImageID       = 0;
   gMacroID       = 0;
   gOutputName    = "stdout.dat";
   gImageType     = "png";
   gImageWidth    = "";
   if (EndsWith(gFileName,".cxx")) gSource = true;
   if (EndsWith(gFileName,".h"))   gHeader = true;
   if (EndsWith(gFileName,".py"))  gPython = true;
   GetClassName();

   // Retrieve the current working directory
   int last = gFileName.rfind("/");
   gCwd     = gFileName.substr(0,last);

   // Retrieve the output directory
   gOutDir = getenv("DOXYGEN_OUTPUT_DIRECTORY");
   ReplaceAll(gOutDir,"\"","");

   // Retrieve the source directory
   gSourceDir = getenv("DOXYGEN_SOURCE_DIRECTORY");
   ReplaceAll(gSourceDir,"\"","");

   // Retrieve the python executable
   gPythonExec = getenv("Python3_EXECUTABLE");
   ReplaceAll(gPythonExec,"\"","");

   // Open the input file name.
   f = fopen(gFileName.c_str(),"r");
   if (!f) return 1;

   if (gFileName.find("tutorials") != string::npos) FilterTutorial();
   else                                             FilterClass();
}

////////////////////////////////////////////////////////////////////////////////
/// Filter ROOT class for Doxygen.

void FilterClass()
{
   // File for inline macros.
   FILE *m = 0;

   // File header.
   if (gHeader) {
      while (fgets(gLine,255,f)) {
         gLineString = gLine;
         printf("%s",gLineString.c_str());
      }
      fclose(f);
      return;
   }

   // Source file.
   if (gSource) {
      size_t spos = 0;
      while (fgets(gLine,255,f)) {
         gLineString = gLine;

         if (gInMacro && gLineString.find("End_Macro") != string::npos) {
            gImageSource = false;
            gInMacro = 0;
            spos = 0;
            if (m) {
               fclose(m);
               m = 0;
               ExecuteCommand(
                  StringFormat("root -l -b -q \"makeimage.C+O(\\\"%s\\\",\\\"%s\\\",\\\"%s\\\",true,false)\"",
                               StringFormat("%s_%3.3d.C", gClassName.c_str(), gMacroID).c_str(),
                               StringFormat("%s_%3.3d.%s", gClassName.c_str(), gImageID, gImageType.c_str()).c_str(),
                               gOutDir.c_str()));
               ExecuteCommand(StringFormat("rm %s_%3.3d.C", gClassName.c_str(), gMacroID));
            }
            int ImageSize = 300;
            FILE *f = fopen("ImagesSizes.dat", "r");
            if (!f) return;
            if (fscanf(f, "%d", &ImageSize) == 1) {
               ReplaceAll(gImageWidth, "IMAGESIZE", StringFormat("%d", ImageSize));
               ReplaceAll(gLineString, "End_Macro",
                          StringFormat("\\image html pict1_%s_%3.3d.%s %s", gClassName.c_str(), gImageID,
                                       gImageType.c_str(), gImageWidth.c_str()));
            }
            fclose(f);
            remove("ImagesSizes.dat");
         }

         if (gInMacro) {
            if (spos) gLineString = gLineString.substr(spos);
            if (gInMacro == 1) {
               if (EndsWith(gLineString,".C\n") || (gLineString.find(".C(") != string::npos)) {
                  ExecuteMacro();
                  gInMacro++;
               } else {
                  gMacroID++;
                  m = fopen(StringFormat("%s_%3.3d.C", gClassName.c_str(), gMacroID).c_str(), "w");
                  if (m) fprintf(m,"%s",gLineString.c_str());
                  if (BeginsWith(gLineString,"{")) {
                     if (gImageSource) {
                        ReplaceAll(gLineString,"{"
                                                , StringFormat("\\include %s_%3.3d.C"
                                                , gClassName.c_str()
                                                , gMacroID));
                     } else {
                        gLineString = "\n";
                     }
                  }
                  gInMacro++;
               }
            } else {
               if (m) fprintf(m,"%s",gLineString.c_str());
               if (BeginsWith(gLineString,"}")) {
                  ReplaceAll(gLineString,"}","");
               } else {
                  gLineString = "\n";
               }
               gInMacro++;
            }
         }

         if (gLineString.find("Begin_Macro") != string::npos &&
             gLineString.find("End_Macro") == string::npos) {
            if (BeginsWith(gLineString, "///")) {
               spos = gLineString.find_first_not_of(' ', 3);
            }
            if (gLineString.find("source") != string::npos) gImageSource = true;
            if (gLineString.find("png") != string::npos) {
               gImageType = "png";
            } else if (gLineString.find("svg") != string::npos) {
               gImageType = "svg";
            } else {
               gImageType = "png";
            }
            gImageWidth = "";
            int wpos1 = gLineString.find("\"width=");
            if (wpos1 != string::npos) {
               int wpos2 = gLineString.find_first_of("\"", wpos1+1);
               gImageWidth = gLineString.substr(wpos1+1, wpos2-wpos1-1);
             } else {
                gImageWidth = "width=IMAGESIZE";
            }
            gImageID++;
            gInMacro++;
            gLineString = "\n";
         }

         size_t l = gLineString.length();
         size_t b = 0;
         do {
            size_t e = gLineString.find('\n', b);
            if (e != string::npos) e++;
            if (spos) printf("%-*s%s", (int)spos, "///",
                              gLineString.substr(b, e - b).c_str());
            else printf("%s", gLineString.substr(b, e - b).c_str());
            b = e;
         } while (b < l);
      }
      fclose(f);
      return;
   }

   // Output anything not header nor source
   while (fgets(gLine,255,f)) {
      gLineString = gLine;
      printf("%s",gLineString.c_str());
   }
   fclose(f);
}

////////////////////////////////////////////////////////////////////////////////
/// Filter ROOT tutorials for Doxygen.

void FilterTutorial()
{
   // Use these to write out work that should be run in parallel after doxygen is done:
   // This executes python <work>
   std::ofstream worklist_py("tutorialWorklist_py", ios_base::app);
   // This executes root <work>
   std::ofstream worklist_root("tutorialWorklist_root", ios_base::app);

   // File for inline macros.
   FILE *m = 0;

   int showTutSource = 0;
   int incond = 0;

   // Extract the macro name
   int i1 = gFileName.rfind('/')+1;
   int i2;
   if (gPython) {
      i2 = gFileName.rfind('y');
   } else {
      i2 = gFileName.rfind('C');
   }
   gMacroName  = gFileName.substr(i1,i2-i1+1);
   gImageName  = StringFormat("%s.%s", gMacroName.c_str(), gImageType.c_str()); // Image name
   gOutputName = StringFormat("%s.out", gMacroName.c_str()); // output name
   if (gPython) {
      FILE *cn = fopen("CleanNamespaces.sh", "a");
      string name = gMacroName;
      ReplaceAll(name,".py","");
      ReplaceAll(name,"_","__");
      if (cn)
         fprintf(cn,"./modifyNamespacesWebpage.sh %s\n",name.c_str());
      fclose(cn);
   }

   // Parse the source and generate the image if needed
   while (fgets(gLine,255,f)) {
      gLineString = gLine;

      // \macro_image found
      if (gLineString.find("\\macro_image") != string::npos) {
         bool nobatch = (gLineString.find("(nobatch)") != string::npos);
         ReplaceAll(gLineString,"(nobatch)","");
         bool tcanvas_js = false, rcanvas_js = false;
         int tcanvas_aclic = 0;

         if (gLineString.find("(tcanvas_js)") != string::npos) {
            tcanvas_js = true;
            tcanvas_aclic = 0;
            ReplaceAll(gLineString,"(tcanvas_js)", "");
         }

         if (gLineString.find("(tcanvas_jsp)") != string::npos) {
            tcanvas_js = true;
            tcanvas_aclic = 1;
            ReplaceAll(gLineString,"(tcanvas_jsp)", "");
         }

         if (gLineString.find("(rcanvas_js)") != string::npos) {
            rcanvas_js = true;
            ReplaceAll(gLineString,"(rcanvas_js)", "");
         }

         bool image_created_by_macro = (gLineString.find(".png)") != string::npos) ||
                                       (gLineString.find(".svg)") != string::npos) ||
                                       (gLineString.find(".pdf)") != string::npos);
         if (image_created_by_macro) {
            string image_name = gLineString;
            ReplaceAll(image_name, " ", "");
            ReplaceAll(image_name, "///\\macro_image(", "");
            ReplaceAll(image_name, ")\n", "");
            if (gPython) {
               ExecuteCommand(StringFormat("%s %s", gPythonExec.c_str(), gFileName.c_str()));
            } else {
               ExecuteCommand(StringFormat("root -l -b -q %s", gFileName.c_str()));
            }
            ExecuteCommand(StringFormat("mv %s %s/html", image_name.c_str(), gOutDir.c_str()));
            ReplaceAll(gLineString, "macro_image (", "image html ");
            ReplaceAll(gLineString, ")", "");
         } else if (tcanvas_js) {
            string IN;
            IN = gImageName;
            int i = IN.find(".");
            IN.erase(i,IN.length());
            ExecuteCommand(StringFormat("root -l -b -q \"MakeTCanvasJS.C(\\\"%s\\\",\\\"%s\\\",\\\"%s\\\",false,%d,%d)\"",
                                         gFileName.c_str(), IN.c_str(), gOutDir.c_str(), gPython, tcanvas_aclic));
            ReplaceAll(gLineString, "macro_image", StringFormat("htmlinclude %s.html",IN.c_str()));
         } else if (rcanvas_js) {
            string IN;
            IN = gImageName;
            int i = IN.find(".");
            IN.erase(i,IN.length());
            ExecuteCommand(StringFormat(
               "root -l -b -q --web=batch \"MakeRCanvasJS.C+O(\\\"%s\\\",\\\"%s\\\",\\\"%s\\\",false,%d)\"",
               gFileName.c_str(), IN.c_str(), gOutDir.c_str(), gPython));
            ReplaceAll(gLineString, "macro_image", StringFormat("htmlinclude %s.html",IN.c_str()));
         } else {
            if (gPython) {
               if (nobatch) {
                  ExecuteCommand(StringFormat("%s makeimage.py %s %s %s 0 1 0",
                                             gPythonExec.c_str(),
                                             gFileName.c_str(), gImageName.c_str(), gOutDir.c_str()));
               } else {
                  ExecuteCommand(StringFormat("%s makeimage.py %s %s %s 0 1 1",
                                             gPythonExec.c_str(),
                                             gFileName.c_str(), gImageName.c_str(), gOutDir.c_str()));
               }
            } else {
               if (nobatch) {
                  ExecuteCommand(
                     StringFormat("root -l -q \"makeimage.C+O(\\\"%s\\\",\\\"%s\\\",\\\"%s\\\",false,false)\"",
                                  gFileName.c_str(), gImageName.c_str(), gOutDir.c_str()));
               } else {
                  ExecuteCommand(
                     StringFormat("root -l -b -q \"makeimage.C+O(\\\"%s\\\",\\\"%s\\\",\\\"%s\\\",false,false)\"",
                                  gFileName.c_str(), gImageName.c_str(), gOutDir.c_str()));
               }
            }
            ReplaceAll(gLineString, "\\macro_image", ImagesList(gImageName));
            remove(gOutputName.c_str());
         }
      }

      // \macro_code found
      if (gLineString.find("\\macro_code") != string::npos) {
         showTutSource = 1;
         m = fopen(StringFormat("%s/macros/%s",gOutDir.c_str(),gMacroName.c_str()).c_str(), "w");
         ReplaceAll(gLineString, "\\macro_code", StringFormat("\\include %s",gMacroName.c_str()));
      }

      // notebook found
      if (gLineString.find("\\notebook") != string::npos) {
         // Notebooks are generated in dedicated step:
         worklist_py << "converttonotebook.py " << gFileName << " " << gOutDir << "/notebooks/" << std::endl;

         if (gPython){
             gLineString = "## ";
         }
         else{
             gLineString = "/// ";
         }
         gLineString += StringFormat( "\\htmlonly <a href=\"https://nbviewer.jupyter.org/url/root.cern/doc/master/notebooks/%s.nbconvert.ipynb\" target=\"_blank\"><img src= notebook.gif alt=\"View in nbviewer\" style=\"height:1.5em\" ></a> <a href=\"https://cern.ch/swanserver/cgi-bin/go?projurl=https://root.cern/doc/master/notebooks/%s.nbconvert.ipynb\" target=\"_blank\"><img src=\"https://swanserver.web.cern.ch/swanserver/images/badge_swan_white_150.png\"  alt=\"Open in SWAN\" style=\"height:1.5em\" ></a> <br/>\\endhtmlonly \n", gMacroName.c_str() , gMacroName.c_str());
      }

      // \macro_output found
      if (gLineString.find("\\macro_output") != string::npos) {
         remove(gOutputName.c_str());
         if (!gPython) ExecuteCommand(StringFormat("root -l -b -q %s", gFileName.c_str()).c_str());
         else          ExecuteCommand(StringFormat("%s %s", gPythonExec.c_str(), gFileName.c_str()).c_str());
         ExecuteCommand(StringFormat("sed -i '/Processing/d' %s", gOutputName.c_str()).c_str());
         rename(gOutputName.c_str(), StringFormat("%s/macros/%s",gOutDir.c_str(), gOutputName.c_str()).c_str());
         ReplaceAll(gLineString, "\\macro_output", StringFormat("\\include %s",gOutputName.c_str()));
      }

      // \author is the last comment line.
      if (gLineString.find("\\author") != string::npos) {
         if (gPython) printf("%s",StringFormat("%s \n## \\cond \n",gLineString.c_str()).c_str());
         else         printf("%s",StringFormat("%s \n/// \\cond \n",gLineString.c_str()).c_str());
         if (showTutSource == 1) {
            showTutSource = 2;
            m = fopen(StringFormat("%s/macros/%s",gOutDir.c_str(),gMacroName.c_str()).c_str(), "w");
         }
         incond = 1;
      } else {
         printf("%s",gLineString.c_str());
         if (m && showTutSource == 2) fprintf(m,"%s",gLineString.c_str());
      }
   }

   if (incond) {
      if (gPython) printf("## \\endcond \n");
      else         printf("/// \\endcond \n");
   }

   if (m) {
      fclose(m);
   }
}

////////////////////////////////////////////////////////////////////////////////
/// Retrieve the class name.

void GetClassName()
{
   int i1 = 0;
   int i2 = 0;

   // File header.
   if (gHeader) {
      i1         = gFileName.find_last_of("/")+1;
      i2         = gFileName.find(".h")-1;
      gClassName = gFileName.substr(i1,i2-i1+1);
   }

   // Source file.
   if (gSource) {
      i1         = gFileName.find_last_of("/")+1;
      i2         = gFileName.find(".cxx")-1;
      gClassName = gFileName.substr(i1,i2-i1+1);
   }

   return;
}

////////////////////////////////////////////////////////////////////////////////
/// Execute the macro in gLineString and produce the corresponding picture.

void ExecuteMacro()
{
   // Name of the next Image to be generated
   gImageName = StringFormat("%s_%3.3d.%s", gClassName.c_str(), gImageID, gImageType.c_str());

   // Retrieve the macro to be executed.
   if (gLineString.find("../../..") != string::npos) {
      ReplaceAll(gLineString,"../../..", gSourceDir.c_str());
   } else {
      gLineString.insert(0, StringFormat("%s/../doc/macros/",gCwd.c_str()));
   }
   int i1     = gLineString.rfind('/')+1;
   int i2     = gLineString.rfind('C');
   gMacroName = gLineString.substr(i1,i2-i1+1);

   // Build the ROOT command to be executed.
   gLineString.insert(0, StringFormat("root -l -b -q \"makeimage.C+O(\\\""));
   size_t l = gLineString.length();
   gLineString.replace(l-1,1,StringFormat("\\\",\\\"%s\\\",\\\"%s\\\",true,false)\"", gImageName.c_str(), gOutDir.c_str()));

   // Execute the macro
   ExecuteCommand(gLineString);

   // Inline the directives to show the code
   if (gImageSource) gLineString = StringFormat("\\include %s\n", gMacroName.c_str());
   else gLineString = "";
}

////////////////////////////////////////////////////////////////////////////////
/// Execute a command making sure stdout will not go in the doxygen file.

void ExecuteCommand(string command)
{
   int o = dup(fileno(stdout));
   if (freopen(gOutputName.c_str(), "a", stdout) != nullptr) {
      int i = system(command.c_str());
      dup2(o, fileno(stdout));
      close(o);
   }
}

////////////////////////////////////////////////////////////////////////////////
/// Get the number of images in NumberOfImages.dat after makeimage.C is executed.

int NumberOfImages()
{
   int ImageNum;
   FILE *f = fopen("NumberOfImages.dat", "r");
   if (!f) return 0;
   if (fscanf(f, "%d", &ImageNum) != 1)
      ImageNum = 0;
   fclose(f);
   remove("NumberOfImages.dat");
   return ImageNum;
}

////////////////////////////////////////////////////////////////////////////////
/// Replace all instances of a string with another string.

void ReplaceAll(string& str, const string& from, const string& to) {
   if (from.empty()) return;
   string wsRet;
   wsRet.reserve(str.length());
   size_t start_pos = 0, pos;
   while ((pos = str.find(from, start_pos)) != string::npos) {
      wsRet += str.substr(start_pos, pos - start_pos);
      wsRet += to;
      pos += from.length();
      start_pos = pos;
   }
   wsRet += str.substr(start_pos);
   str.swap(wsRet);
}

////////////////////////////////////////////////////////////////////////////////
/// std::string formatting like sprintf.

string StringFormat(const string fmt_str, ...) {
   int final_n, n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */
   string str;
   unique_ptr<char[]> formatted;
   va_list ap;
   while (1) {
      formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */
      strcpy(&formatted[0], fmt_str.c_str());
      va_start(ap, fmt_str);
      final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap);
      va_end(ap);
      if (final_n < 0 || final_n >= n) n += abs(final_n - n + 1);
      else break;
   }
   return string(formatted.get());
}

////////////////////////////////////////////////////////////////////////////////
/// Return the image list after a tutorial macro execution.

string ImagesList(string& name) {

   int N = NumberOfImages();

   // evaluate the size of the output string
   char evalstring[300];
   snprintf(&evalstring[0], 300, " \n/// \\image html pict%d_%s width=%d", N, name.c_str(), 10000);
   int evallen = (int)strlen(evalstring);

   // allocate the output string
   int vallen = sizeof(char) * evallen * N;
   char *val = (char *)malloc(vallen);
   int len = 0;

   int ImageSize = 300;
   FILE *f = fopen("ImagesSizes.dat", "r");
   if (!f) return "";

   for (int i = 1; i <= N; i++){
      if (fscanf(f, "%d", &ImageSize) == 1) {
         if (i > 1) {
            if (gPython)
               snprintf(&val[len], vallen, " \n## \\image html pict%d_%s width=%d", i, name.c_str(), ImageSize);
            else
               snprintf(&val[len], vallen, " \n/// \\image html pict%d_%s width=%d", i, name.c_str(), ImageSize);
         } else {
            snprintf(&val[len], vallen, "\\image html pict%d_%s width=%d", i, name.c_str(), ImageSize);
         }
         len = (int)strlen(val);
      }
   }

   fclose(f);
   remove("ImagesSizes.dat");

   return (string)val;
}

////////////////////////////////////////////////////////////////////////////////
/// Find if a string ends with another string.

bool EndsWith(string const &fullString, string const &ending) {
   if (fullString.length() >= ending.length()) {
      return (0 == fullString.compare (fullString.length() - ending.length(), ending.length(), ending));
   } else {
      return false;
   }
}

////////////////////////////////////////////////////////////////////////////////
/// Find if a string begins with another string.

bool BeginsWith(const string& haystack, const string& needle) {
   return needle.length() <= haystack.length() && equal(needle.begin(), needle.end(), haystack.begin());
}
