//  Please refer to the COPYRIGHT file of the profiling package for details.
//  SPDX-License-Identifier: MIT
/*
 * profiling: Line by line profiling and code coverage for GAP
 */

#include "gap_cpp_headers/gap_cpp_mapping.hpp"
#include <sstream>
#include <vector>
#include <set>
#include <algorithm>

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>


#include "md5.h"


enum ProfType { Read = 1, Exec = 2, IntoFun = 3, OutFun = 4, StringId = 5, Info = 6, InvalidType = -1};

ProfType CharToProf(char c)
{
  if(c == 'R') return Read;
  if(c == 'E' || c == 'X') return Exec;
  if(c == 'I') return IntoFun;
  if(c == 'O') return OutFun;
  if(c == 'S') return StringId;
  if(c == '_') return Info;
  throw GAPException("Invalid 'Type' in profile");
}

struct JsonParse
{
  // Type of line
  ProfType Type;
  // Name of function (for function start/end)
  std::string Fun;
  // Time spent on line
  int Ticks;
  // Number of line executed (1 is ommitted from JSON)
  int Execs;
  // Line executed
  int Line;
  // End line of function (for function start)
  int EndLine;
  std::string File;
  int FileId;

  bool IsCover;
  std::string TimeType;
  JsonParse() : Type(InvalidType), Ticks(0), Execs(1), Line(-1), EndLine(-1), FileId(-1),
                IsCover(false)
    { }
};

#include "json_parse_rapidjson.h"


struct FullFunction
{
  std::string name;
  std::string filename;
  Int line;
  Int endline;

  FullFunction() {}
  FullFunction(const std::string& _name, const std::string _file, Int _line, Int _endline)
    : name(_name), filename(_file), line(_line), endline(_endline)
  { }

  friend bool operator<(const FullFunction& lhs, const FullFunction& rhs)
  {
    if(lhs.line < rhs.line) return true;
    if(lhs.line > rhs.line) return false;
    if(lhs.endline < rhs.endline) return true;
    if(lhs.endline > rhs.endline) return false;
    if(lhs.name < rhs.name) return true;
    if(lhs.name > rhs.name) return false;
    if(lhs.filename < rhs.filename) return true;
    if(lhs.filename > rhs.filename) return false;

    return false;
  }
};

namespace GAPdetail {
template<>
struct GAP_maker<FullFunction>
{
  Obj operator()(const FullFunction& f)
  {
    GAPRecord r;
    r.set("line", f.line);
    r.set("endline", f.endline);
    r.set("name", f.name);
    r.set("filename", f.filename);
    return r.raw_obj();
  }
};
}

struct Location
{
  std::string filename;
  std::string name;
  Int line;

  Location() {}
  Location(const std::string _name, const std::string _file, Int _line)
    :  name(_name), filename(_file), line(_line)
  { }

  friend bool operator<(const Location& lhs, const Location& rhs)
  {
    if(lhs.line < rhs.line) return true;
    if(lhs.line > rhs.line) return false;
    if(lhs.name < rhs.name) return true;
    if(lhs.name > rhs.name) return false;
    if(lhs.filename < rhs.filename) return true;
    if(lhs.filename > rhs.filename) return false;

    return false;
  }
};

namespace GAPdetail {
template<>
struct GAP_maker<Location>
{
  Obj operator()(const Location& f)
  {
    GAPRecord r;
    r.set("line", f.line);
    r.set("filename", f.filename);
    return r.raw_obj();
  }
};
}

FullFunction buildFunctionName(const JsonParse& jp)
{ return FullFunction(jp.Fun, jp.File, jp.Line, jp.EndLine); }

// We lazily set up 'children' because we can't be bothered using
// a shared_ptr
struct StackTrace
{
    int runtime;
    int calls;
    std::map<FullFunction, StackTrace>* children;
    StackTrace* parent;

    StackTrace() : runtime(0), calls(0),
    children(NULL), parent(NULL)
    { }

    StackTrace(StackTrace* p) : runtime(0), calls(0),
    children(NULL), parent(p)
    { }

    void setupChildren()
    {
      if(!children)
        children = new std::map<FullFunction, StackTrace>;
    }

    ~StackTrace()
    {
      if(children)
        delete children;
    }

    StackTrace(const StackTrace& st) :
    runtime(st.runtime), calls(st.calls), children(st.children), parent(st.parent)
    { assert(!children); }

};


void dumpRuntimes_in(StackTrace* st,
                     std::vector<std::pair<std::vector<FullFunction>, Int > >& ret,
                     std::vector<FullFunction>& stack)
{
    ret.push_back(std::make_pair(stack, st->runtime));
    for(std::map<FullFunction, StackTrace>::iterator it = st->children->begin();
        it != st->children->end();
        ++it)
    {
        stack.push_back(it->first);
        dumpRuntimes_in(&(it->second), ret, stack);
        stack.pop_back();
    }
}

std::vector<std::pair<std::vector<FullFunction>, Int > > dumpRuntimes(StackTrace* st)
{
    std::vector<std::pair<std::vector<FullFunction>, Int > > ret;
    std::vector<FullFunction> stack;
    dumpRuntimes_in(st, ret, stack);
    return ret;
}

struct TimeStash
{
  Int runtime;
  Int runtime_with_children;
  Int total_ticks;

  TimeStash(Int _l, Int _cl, Int _tt) :
  runtime(_l), runtime_with_children(_cl),
  total_ticks(_tt) { }
};

static int endsWithgz(char* s)
{
  s = strrchr(s, '.');
  if(s)
    return strcmp(s, ".gz") == 0;
  else
    return 0;
}

struct Stream {
  FILE* stream;
  bool wasPopened;
  Stream(char* name) {
    char popen_buf[4096];
    if(endsWithgz(name) && strlen(name) < 3000)
    {
      strcpy(popen_buf, "gzip -d -c < ");
      strcat(popen_buf, name);
      stream = popen(popen_buf, "r");
      wasPopened = true;
    }
    else
    {
      stream = fopen(name, "r");
      wasPopened = false;
    }
  }

  bool fail()
  { return stream == 0; }

  ~Stream() {
      if(wasPopened)
        pclose(stream);
      else
        fclose(stream);
    }
};



Obj FuncREAD_PROFILE_FROM_STREAM(Obj self, Obj filename, Obj param2)
{
try{
    bool isCover = false;
    std::string timeType;
    int failedparse = 0;
    bool firstExec = true;

    std::map<Int, std::string> filename_map;
    std::map<std::string, Int> filename_map_inverse;

    std::map<Int, std::set<Int> > read_lines;
    std::map<Int, std::map<Int, Int> > exec_lines;
    std::map<Int, std::map<Int, Int> > runtime_lines;
    std::map<Int, std::map<Int, Int> > runtime_with_children_lines;

    std::map<Int, std::map<Int, std::set<FullFunction> > > called_functions;
    std::map<Int, std::map<Int, std::set<Location> > > calling_functions;
    StackTrace stacktrace;
    stacktrace.setupChildren();
    StackTrace* current_stack = &stacktrace;

    // prev_exec is the last function executed, calling_exec is the statement which
    // we would currently say called a function. The only time when there differ
    // is immediately after returning from a function.
    JsonParse prev_exec;
    JsonParse calling_exec;

    // These keeps track of us going down our function stack
    std::vector<FullFunction> function_stack;
    std::vector<JsonParse> line_stack;
    // this stores various time values
    // when we call a function, so we can correct everything on return.
    std::vector<TimeStash> line_times_stack;

    long long total_ticks = 0;

    if(!(IS_STRING(filename))) {
      ErrorMayQuit("Filename must be a string", 0, 0);
    }
    Obj filenamestr = CopyToStringRep(filename);
    Stream infile(CSTR_STRING(filenamestr));
    if(infile.fail()) {
      ErrorMayQuit("Unable to open file %s", (Int)CSTR_STRING(filenamestr), 0);
      return Fail;
    }

    long line_number = 0;

    while(true)
    {
      if(feof(infile.stream)) {
        break;
      }
      line_number++;
      char str[10000];
      if(fgets(str, 10000, infile.stream) == 0)
      {
        if(feof(infile.stream)) {
          break;
        }
        else {
          return Fail;
        }
      }
      JsonParse ret;
      if(ReadJson(str, ret))
      {
        switch(ret.Type)
        {
          case InvalidType: ErrorReturnVoid("Internal Error",0,0,""); break;
          case StringId:
            if(filename_map.count(ret.FileId) > 0) {
              ErrorMayQuit("Invalid input - Reused fileId. Did you try concatenating files?"
                           "Use ConcatenateLineByLineProfiles",0,0); break;
            }
            filename_map[ret.FileId] = ret.File;
            filename_map_inverse[ret.File] = ret.FileId;
          break;
          case IntoFun:
          {
            FullFunction retfunc = buildFunctionName(ret);
            // Record which line called this function
            called_functions[calling_exec.FileId][calling_exec.Line].insert(retfunc);
            // Record we called this function from here
            if(!function_stack.empty()) {
              // This '!= 0' is to support older GAP's which don't provide this field
              if(ret.FileId != 0) {
                std::string filename = function_stack.back().filename;
                calling_functions[ret.FileId][ret.Line].insert(Location(filename, filename_map[calling_exec.FileId], calling_exec.Line));
              }
            }
            // Add this function to the stack of executing functions
            function_stack.push_back(retfunc);

            static int deepest_depth = 0;
            if(function_stack.size() > deepest_depth) {
              deepest_depth = function_stack.size();
            }
            // And to stack of executed files/line numbers
            line_stack.push_back(calling_exec);
            // We also store the amount of time spent in this stack as well.
            line_times_stack.push_back(
              TimeStash(runtime_lines[calling_exec.FileId][calling_exec.Line],
                        runtime_with_children_lines[calling_exec.FileId][calling_exec.Line],
                        total_ticks));

            StackTrace* next_stack = &((*(current_stack->children))[function_stack.back()]);
            next_stack->setupChildren();

            if(!next_stack->parent)
                next_stack->parent = current_stack;
            assert(next_stack->parent == current_stack);
            current_stack = next_stack;
            (current_stack->calls)++;
          }
          break;
          case OutFun:
          {
            if(current_stack->parent)
            {
                current_stack = current_stack->parent;
                calling_exec = line_stack.back();
                TimeStash ts = line_times_stack.back();
                runtime_with_children_lines[calling_exec.FileId][calling_exec.Line] =
                  ts.runtime_with_children + (total_ticks - ts.total_ticks) -
                    (runtime_lines[calling_exec.FileId][calling_exec.Line] - ts.runtime);
                function_stack.pop_back();
                line_stack.pop_back();
                line_times_stack.pop_back();
            }
          }
          break;

          case Read:
          case Exec:

          if(ret.Type == Read)
          {
            read_lines[ret.FileId].insert(ret.Line);
          }
          else
          {
            exec_lines[ret.FileId][ret.Line]+=ret.Execs;
            if(firstExec)
              firstExec = false;
            else
            {
              // The ticks are since the last executed line
              if(ret.Ticks > 0) {
                runtime_lines[prev_exec.FileId][prev_exec.Line]+=ret.Ticks;
                // Hard to know exactly where to charge these to --
                // this is easiest
                (current_stack->runtime) += ret.Ticks;
                total_ticks += ret.Ticks;
              }
            }
          }
          break;
          case Info:
            isCover = ret.IsCover;
            timeType = ret.TimeType;
          break;
        }
      }
      else
      {
        // We allow a few failed parses to deal with truncated files
        failedparse++;
        Pr("Warning: damaged profile at %g:%d",  (Int)filenamestr, (Int)line_number);
        if(failedparse > 4) {
          throw GAPException("Malformed profile");
        }
      }


      if(ret.Type == Exec) { prev_exec = ret; calling_exec = ret; }
      if(ret.Type == Info) { calling_exec = ret; }
    }


    // Now lets build a bunch of stuff which GAP will want back.
    // This stores the read, exec and runtime data.
    // vector of [filename, [ [read,exec,runtime] of line 1, [read,exec,runtime] of line 2, ... ] ]

    std::vector<std::pair<std::string, std::vector<std::vector<Int> > > > read_exec_data;

    std::vector<std::pair<std::string, std::vector<std::set<FullFunction> > > > called_functions_ret;
    std::vector<std::pair<std::string, std::vector<std::set<Location> > > > calling_functions_ret;

    // First gather all used filenames
    std::set<Int> filenameids;

    for(std::map<Int, std::set<Int> >::iterator it = read_lines.begin(); it != read_lines.end(); ++it)
      filenameids.insert(it->first);

    for(std::map<Int, std::map<Int,Int> >::iterator it = exec_lines.begin(); it != exec_lines.end(); ++it)
      filenameids.insert(it->first);

    for(std::map<Int, std::map<Int,Int> >::iterator it = runtime_lines.begin(); it != runtime_lines.end(); ++it)
      filenameids.insert(it->first);

    // Now we have all our filenames!
    // clear out a marker we use for start of file
    filenameids.erase(-1);

    for(std::set<Int>::iterator it = filenameids.begin(); it != filenameids.end(); ++it)
    {
      const std::set<Int>& read_set = read_lines[*it];
      std::map<Int,Int>& exec_set = exec_lines[*it];
      std::map<Int,Int>& runtime = runtime_lines[*it];
      std::map<Int,Int>& runtime_children = runtime_with_children_lines[*it];
      std::map<Int, std::set<FullFunction> >& functions = called_functions[*it];
      std::map<Int, std::set<Location> >& functions_calling = calling_functions[*it];

      Int max_line = 0;
      if(!read_set.empty())
        max_line = std::max(max_line, *(read_set.rbegin()));

      if(!exec_set.empty())
        max_line = std::max(max_line, exec_set.rbegin()->first);

      if(!runtime.empty())
        max_line = std::max(max_line, runtime.rbegin()->first);

      if(!runtime_children.empty())
        max_line = std::max(max_line, runtime_children.rbegin()->first);

      if(!functions.empty())
        max_line = std::max(max_line, functions.rbegin()->first);

      if(!functions_calling.empty())
        max_line = std::max(max_line, functions_calling.rbegin()->first);

      std::vector<std::vector<Int> > line_data;
      std::vector<std::set<FullFunction> > called_data;
      std::vector<std::set<Location> > calling_data;
      for(int i = 1; i <= max_line; ++i)
      {
        std::vector<Int> data;
        data.push_back(read_set.count(i));
        data.push_back(exec_set[i]);
        data.push_back(runtime[i]);
        data.push_back(runtime_children[i]);
        line_data.push_back(data);

        called_data.push_back(functions[i]);
        calling_data.push_back(functions_calling[i]);
      }

      if(filename_map.count(*it) == 0)
      {
        Pr("Warning: damaged profile, cannot find a filename to match id %d", *it, 0L);
      }
      else
      {
        read_exec_data.push_back(std::make_pair(filename_map[*it], line_data));
        called_functions_ret.push_back(std::make_pair(filename_map[*it], called_data));
        calling_functions_ret.push_back(std::make_pair(filename_map[*it], calling_data));
      }
    }

    std::vector<std::pair<std::vector<FullFunction>, Int> > function_stack_runtimes = dumpRuntimes(&stacktrace);

    GAPRecord info;
    info.set("is_cover", isCover);
    info.set("time_type", timeType);

    GAPRecord r;

    r.set("line_info", read_exec_data);
    r.set("stack_runtimes", function_stack_runtimes);
    r.set("line_function_calls", called_functions_ret);
    r.set("line_calling_function_calls", calling_functions_ret);
    r.set("info", info);

    return GAP_make(r);
} catch (const GAPException& exp) {
  ErrorMayQuit(exp.what(), 0, 0);
}
return Fail;
}

Obj FuncHTMLEncodeString(Obj self, Obj param)
{
  if(!IS_STRING_REP(param))
  {
    ErrorMayQuit("<arg> must satisfy IsStringRep",0L,0L);
  }

  Int len = GET_LEN_STRING(param);
  // Make long enough there is no chance a
  // resize will be needed. - 8 * 6 to allow for tabs
  Obj outstring = NEW_STRING(len * 8 * 6);
  char* ptr = CSTR_STRING(param);
  char* outptr = CSTR_STRING(outstring);
  Int outpos = 0;
  for(Int i = 0; i < len; ++i)
  {
    switch(ptr[i]) {
      case '&':
        outptr[outpos++] = '&';
        outptr[outpos++] = 'a';
        outptr[outpos++] = 'm';
        outptr[outpos++] = 'p';
        outptr[outpos++] = ';';
        break;
      case '<':
        outptr[outpos++] = '&';
        outptr[outpos++] = 'l';
        outptr[outpos++] = 't';
        outptr[outpos++] = ';';
        break;
      case ' ':
        outptr[outpos++] = '&';
        outptr[outpos++] = 'n';
        outptr[outpos++] = 'b';
        outptr[outpos++] = 's';
        outptr[outpos++] = 'p';
        outptr[outpos++] = ';';
        break;
      case '\t':
        for(int i = 0; i < 8; ++i) {
          outptr[outpos++] = '&';
          outptr[outpos++] = 'n';
          outptr[outpos++] = 'b';
          outptr[outpos++] = 's';
          outptr[outpos++] = 'p';
          outptr[outpos++] = ';';
        }
        break;
      default:
        outptr[outpos++] = ptr[i];
    }
  }
  SET_LEN_STRING(outstring, outpos);
  SHRINK_STRING(outstring);
  return outstring;
}

Obj FuncMD5File(Obj self, Obj filename)
{
  if (!IsStringConv(filename))
  {
    ErrorQuit("MD5File: <filename> must be a string", 0, 0);
  }

  MD5Context ctx;

  MD5Init(&ctx);

  int fd = open(CSTR_STRING(filename), O_RDONLY);
  if (fd < 0)
  {
    ErrorQuit("MD5File: failed to open file %g", (Int)filename, 0);
  }

  // get the file length
  struct stat sb;
  if (fstat(fd, &sb) == -1 || sb.st_size < 0)
  {
    close(fd);
    ErrorQuit("MD5File: failed to determine size of file %g", (Int)filename, 0);
  }
  off_t len = sb.st_size;

  while (len > 0)
  {
    uint8_t buffer[4096];
    size_t amount = (len < 4096 ? len : 4096);
    ssize_t bytesRead = read(fd, buffer, amount);
    if (bytesRead < 0) {
      close(fd);
      ErrorQuit("MD5File: error reading from file %g", (Int)filename, 0);
    }
    MD5Update(&ctx, buffer, (size_t)bytesRead);
    len -= bytesRead;
  }

  close(fd);

  uint8_t digest[16];
  MD5Final(digest, &ctx);

  char str[33];
  static const char hex[] = "0123456789abcdef";
  for (int i = 0; i < 16; i++)
  {
    str[2*i] = hex[digest[i] >> 4];
    str[2*i + 1] = hex[digest[i] & 0x0f];
  }
  str[32] = '\0';

  return MakeImmString(str);
}

// Table of functions to export
static StructGVarFunc GVarFuncs [] = {
    GVAR_FUNC_2ARGS(READ_PROFILE_FROM_STREAM, param, param2),
    GVAR_FUNC_1ARGS(HTMLEncodeString, param),
    GVAR_FUNC_1ARGS(MD5File, filename),

	{ 0 } /* Finish with an empty entry */
};

/****************************************************************************
**
*F  InitKernel( <module> )  . . . . . . . . initialise kernel data structures
*/
static Int InitKernel( StructInitInfo *module )
{
    /* init filters and functions                                          */
    InitHdlrFuncsFromTable( GVarFuncs );

    /* return success                                                      */
    return 0;
}

/****************************************************************************
**
*F  InitLibrary( <module> ) . . . . . . .  initialise library data structures
*/
static Int InitLibrary( StructInitInfo *module )
{
    /* init filters and functions */
    InitGVarFuncsFromTable( GVarFuncs );

    /* return success                                                      */
    return 0;
}

/****************************************************************************
**
*F  InitInfopl()  . . . . . . . . . . . . . . . . . table of init functions
*/
static StructInitInfo module = {
 /* type        = */ MODULE_DYNAMIC,
 /* name        = */ "profiling",
 /* revision_c  = */ 0,
 /* revision_h  = */ 0,
 /* version     = */ 0,
 /* crc         = */ 0,
 /* initKernel  = */ InitKernel,
 /* initLibrary = */ InitLibrary,
 /* checkInit   = */ 0,
 /* preSave     = */ 0,
 /* postSave    = */ 0,
 /* postRestore = */ 0
};

extern "C"
StructInitInfo * Init__Dynamic ( void )
{
  return &module;
}
