/*****
* util.cc
* John Bowman
*
* A place for useful utility functions.
*****/

#include <cassert>
#include <iostream>
#include <cstdio>
#include <cfloat>
#include <sstream>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <algorithm>


#if !defined(_WIN32)
#include <sys/wait.h>
#include <sys/param.h>
#include <unistd.h>
#include <dirent.h>
#else
#include <Windows.h>
#include <Shlwapi.h>
#include <Shellapi.h>
#include <direct.h>

#define getcwd _getcwd
#define chdir _chdir
#endif

#include "util.h"
#include "settings.h"
#include "errormsg.h"
#include "camperror.h"
#include "interact.h"
#include "locate.h"

using namespace settings;
using camp::reportError;

bool False=false;

namespace vm {
void error(const char* message);
}

#if __GNUC__
#include <cxxabi.h>
string demangle(const char *s)
{
 int status;
 char *demangled = abi::__cxa_demangle(s,NULL,NULL,&status);
 if (status == 0 && demangled) {
   string str(demangled);
   free(demangled);
   return str;
 } else if (status == -2) {
   free(demangled);
   return s;
 } else {
   free(demangled);
   return string("Unknown(") + s + ")";
 }
}
#else
string demangle(const char* s)
{
 return s;
}
#endif

void showCommand(const mem::vector<string>& s)
{
 if(settings::verbose > 1) {
   cerr << s[0];
   size_t count=s.size();
   for(size_t i=1; i < count; ++i)
     cerr << " " << s[i];
   cerr << endl;
 }
}

// windows specific unnamed spaces
#if defined(_WIN32)

int setenv(const char *name, const char *value, bool overwrite) {
 assert(overwrite);
 return SetEnvironmentVariableA(name,value);
}

int unsetenv(const char *name) {
 return setenv(name,NULL,true);
}

namespace w32 = camp::w32;

namespace
{
/** @brief System, but for Windows.
*         Any handle placed in outHandle must be properly closed */
int SystemWin32(const mem::vector<string>& command, int quiet, bool wait,
               const char* hint, const char* application, int* ppid);
}

namespace
{
int SystemWin32(const mem::vector<string>& command, int quiet, bool wait,
               const char* hint, const char* application, int* ppid)
{
 cout.flush();
 if (command.empty())
 {
   camp::reportError("Command cannot be empty");
   return -1;
 }
 if(!quiet)
   showCommand(command);

 string cmdlineStr=camp::w32::buildWindowsCmd(command);

 STARTUPINFOA startInfo={};
 startInfo.cb=sizeof(startInfo);
 startInfo.dwFlags=STARTF_USESTDHANDLES;

 SECURITY_ATTRIBUTES sa;
 sa.nLength= sizeof(sa);
 sa.lpSecurityDescriptor=nullptr;
 sa.bInheritHandle=true;

 PROCESS_INFORMATION procInfo= {};

 // windows' "/dev/null" file (a.k.a. "NUL")
 {
   w32::HandleRaiiWrapper const nulFileHandle=CreateFileA(
     "NUL", GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ,
     &sa,
     CREATE_ALWAYS,
     FILE_ATTRIBUTE_NORMAL,
     nullptr);

   // set quiet info
   startInfo.hStdInput= GetStdHandle(STD_INPUT_HANDLE);
   startInfo.hStdOutput= quiet >= 1 ? nulFileHandle.getHandle() : GetStdHandle(STD_OUTPUT_HANDLE);
   startInfo.hStdError= quiet >= 2 ? nulFileHandle.getHandle() : GetStdHandle(STD_ERROR_HANDLE);

   ostringstream errorMessage;
   errorMessage << "Cannot open " << application << "\n";
   string const errorMessageOut=errorMessage.str();
   w32::checkResult(CreateProcessA(
                      nullptr,
                      cmdlineStr.data(),
                      nullptr, nullptr, true,
                      0,
                      nullptr, nullptr,
                      &startInfo,
                      &procInfo),
                    errorMessageOut.c_str());
 }
 if (ppid)
 {
   *ppid=static_cast<int>(procInfo.dwProcessId);
 }

 w32::HandleRaiiWrapper const procHandle(procInfo.hProcess);
 w32::HandleRaiiWrapper const threadHandle(procInfo.hThread);


 if (!wait)
 {
   return 0;
 }

 DWORD retcode=-1;
 // else, wait
 switch (WaitForSingleObject(procHandle.getHandle(), INFINITE))
 {
   case WAIT_OBJECT_0: {
     w32::checkResult(GetExitCodeProcess(
       procHandle.getHandle(), &retcode
     ),"Cannot get exit code of process");
     break;
   }
   case WAIT_ABANDONED:// also impossible, we are waiting for a process
   case WAIT_TIMEOUT:  // impossible, since we set timeout to infinite
   case WAIT_FAILED:
   default:
     camp::reportError("Waiting for process failed");
     break;
 }
 return static_cast<int>(retcode);
}
}

#endif

char *Strdup(string s)
{
 size_t size=s.size()+1;
 char *dest=new(UseGC) char[size];
 std::memcpy(dest,s.c_str(),size*sizeof(char));
 return dest;
}

char *StrdupNoGC(string s)
{
 size_t size=s.size()+1;
 char *dest=new char[size];
 std::memcpy(dest,s.c_str(),size*sizeof(char));
 return dest;
}

char *StrdupMalloc(string s)
{
 size_t size=s.size()+1;
 char *dest=(char *) std::malloc(size);
 std::memcpy(dest,s.c_str(),size*sizeof(char));
 return dest;
}

string stripDir(string name)
{
#if defined(_WIN32)
 char constexpr separator= '\\';
 std::replace(name.begin(), name.end(), '/', separator);
#else
 char constexpr separator= '/';
#endif

 size_t p=name.rfind(separator);
 if(p < string::npos) name.erase(0,p+1);
 return name;
}

string stripFile(string name)
{
#if defined(_WIN32)
 char constexpr separator = '\\';
 std::replace(name.begin(), name.end(), '/', separator);
#else
 char constexpr separator= '/';
#endif

 bool dir=false;
 size_t p=name.rfind(separator);
 if(p < string::npos) {
   dir=true;
   while(p > 0 && name[p-1] == separator) --p;
   name.erase(p+1);
 }

 return dir ? name : "";
}

string stripExt(string name, const string& ext)
{
 string suffix="."+ext;
 size_t p=name.rfind(suffix);
 size_t n=suffix.length();
 if(n == 1 || p == name.length()-n)
   return name.substr(0,p);
 else return name;
}

void backslashToSlash(string& s)
{
 size_t p;
 while((p=s.find('\\')) < string::npos)
   s[p]='/';
}

void spaceToUnderscore(string& s)
{
 size_t p;
 while((p=s.find(' ')) < string::npos)
   s[p]='_';
}

string escapeCharacters(string const& inText, std::unordered_set<char> const& charactersToEscape)
{
 mem::vector<char> retBuffer;
 retBuffer.reserve(inText.length() + 1);

 for (const char textChar : inText)
 {
   if (charactersToEscape.find(textChar) != charactersToEscape.end())
   {
     retBuffer.emplace_back('\\');
   }
   retBuffer.emplace_back(textChar);
 }
 retBuffer.emplace_back(0);

 return retBuffer.data();
}

string Getenv(const char *name, bool msdos)
{
#if defined(_WIN32)
 size_t envSize=0;
 getenv_s(&envSize,nullptr,0,name);
 if (envSize == 0)
 {
   return "";
 }

 mem::vector<char> resultingData(envSize);
 if (getenv_s(&envSize, resultingData.data(), resultingData.size(), name) != 0)
 {
   camp::reportError("Cannot retrieve environment variable");
 }

 return resultingData.data();

#else
 char *s=getenv(name);
 if(!s) return "";
 string S=string(s);
 if(msdos) backslashToSlash(S);
 return S;
#endif
}

void readDisabled()
{
 camp::reportError("Read from other directories disabled; override with option -globalread");
}

void writeDisabled()
{
 camp::reportError("Write to other directories disabled; override with option -globalwrite");
}

string cleanpath(string name)
{
 string dir=stripFile(name);
 name=stripDir(name);
 spaceToUnderscore(name);
 return dir+name;
}

string inpath(string name)
{
 bool global=globalread();
 string dir=stripFile(name);
 if(global && !dir.empty()) return name;
 string indir=stripFile(outname());
 if(!(global || dir.empty() || dir == indir)) readDisabled();
 return stripDir(name);
}

string outpath(string name)
{
 bool global=globalwrite();
 string dir=stripFile(name);
 if(global && !dir.empty()) return name;
 string outdir=stripFile(outname());
 if(!(global || dir.empty() || dir == outdir)) writeDisabled();
 return outdir+stripDir(name);
}

string buildname(string prefix, string suffix, string aux)
{
 string name=outpath(prefix)+aux;
 if(!suffix.empty()) name += "."+suffix;
 return name;
}

string auxname(string filename, string suffix)
{
 return buildname(filename,suffix,"_");
}

sighandler_t Signal(int signum, sighandler_t handler)
{
#if !defined(_WIN32)
 struct sigaction action,oldaction;
 action.sa_handler=handler;
 sigemptyset(&action.sa_mask);
 action.sa_flags=0;
 return sigaction(signum,&action,&oldaction) == 0 ? oldaction.sa_handler :
   SIG_ERR;
#else
 return signal(signum, handler);
#endif
}

void push_split(mem::vector<string>& a, const string& S)
{
 const char *p=S.c_str();
 string s;
 char c;
 while((c=*(p++))) {
   if(c == ' ') {
     if(s.size() > 0) {
       a.push_back(s);
       s.clear();
     }
   } else s += c;
 }
 if(s.size() > 0)
   a.push_back(s);
}

char **args(const mem::vector<string>& s, bool quiet)
{
 size_t count=s.size();

 if(!quiet)
   showCommand(s);

 char **argv=NULL;
 argv=new char*[count+1];
 for(size_t i=0; i < count; ++i)
   argv[i]=StrdupNoGC(s[i]);

 argv[count]=NULL;
 return argv;
}

void execError(const char *command, const char *hint, const char *application)
{
 cerr << "Cannot execute " << command << endl;
 if(*application == 0) application=hint;
 if(hint) {
   string s=string(hint);
   transform(s.begin(), s.end(), s.begin(), toupper);
   cerr << "Please put in a file " << getSetting<string>("config")
        << ": " << endl << endl
        << "import settings;" << endl
        << hint << "=\"LOCATION\";" << endl << endl
        << "where LOCATION specifies the location of "
        << application << "." << endl << endl
        << "Alternatively, set the environment variable ASYMPTOTE_" << s
        << endl << "or use the command line option -" << hint
        << "=\"LOCATION\". For further details, see" << endl
        << "https://asymptote.sourceforge.io/doc/Configuring.html" << endl
        << "https://asymptote.sourceforge.io/doc/Search-paths.html" << endl;
 }
}

// quiet: 0=none; 1=suppress stdout; 2=suppress stdout+stderr.
int System(const mem::vector<string> &command, int quiet, bool wait,
          const char *hint, const char *application, int *ppid)
{
#if _WIN32
   return SystemWin32(command, quiet, wait, hint, application, ppid);
#else
 int status;

 cout.flush(); // Flush stdout to avoid duplicate output.

 char **argv=args(command);

 int pid=fork();
 if(pid == -1)
   camp::reportError("Cannot fork process");

 if(pid == 0) {
   if(interact::interactive) signal(SIGINT,SIG_IGN);
   if(quiet) {
     static int null=creat("/dev/null",O_WRONLY);
     close(STDOUT_FILENO);
     dup2(null,STDOUT_FILENO);
     if(quiet == 2) {
       close(STDERR_FILENO);
       dup2(null,STDERR_FILENO);
     }
   }
   if(argv) {
     execvp(argv[0],argv);
     execError(argv[0],hint,application);
     _exit(-1);
   }
 }

 if(ppid) *ppid=pid;
 for(;;) {
   if(waitpid(pid, &status, wait ? 0 : WNOHANG) == -1) {
     if(errno == ECHILD) return 0;
     if(errno != EINTR) {
       if(quiet < 2) {
         ostringstream msg;
         msg << "Command failed: ";
         for(size_t i=0; i < command.size(); ++i) msg << command[i] << " ";
         camp::reportError(msg);
       }
     }
   } else {
     if(!wait) return 0;
     if(WIFEXITED(status)) {
       if(argv) {
         char **p=argv;
         char *s;
         while((s=*(p++)) != NULL)
           delete [] s;
         delete [] argv;
       }
       return WEXITSTATUS(status);
     } else {
       if(quiet < 2) {
         ostringstream msg;
         msg << "Command exited abnormally: ";
         for(size_t i=0; i < command.size(); ++i) msg << command[i] << " ";
         camp::reportError(msg);
       }
     }
   }
 }
#endif
}

string stripblanklines(const string& s)
{
 string S=string(s);
 bool blank=true;
 const char *t=S.c_str();
 size_t len=S.length();

 for(size_t i=0; i < len; i++) {
   if(t[i] == '\n') {
     if(blank) S[i]=' ';
     else blank=true;
   } else if(t[i] != '\t' && t[i] != ' ') blank=false;
 }
 return S;
}

char *startpath=NULL;

void noPath()
{
 camp::reportError("Cannot get current path");
}

char *getPath(char *p)
{
#ifdef MAXPATHLEN
 static size_t size = MAXPATHLEN;
#else
 static size_t size = 1024;
#endif
 if(!p) p=new(UseGC) char[size];
 if(!p) noPath();
 else while(getcwd(p,size) == NULL) {
     if(errno == ERANGE) {
       size *= 2;
       p=new(UseGC) char[size];
     } else {noPath(); p=NULL;}
   }
 return p;
}

const char *setPath(const char *s, bool quiet)
{
 if(startpath == NULL) startpath=getPath(startpath);
 if(s == NULL || *s == 0) s=startpath;
 int rc=chdir(s);
 if(rc != 0) {
   ostringstream buf;
   buf << "Cannot change to directory '" << s << "'";
   camp::reportError(buf);
 }
 char *p=getPath();
 if(p && (!interact::interactive || quiet) && verbose > 1)
   cout << "cd " << p << endl;
 return p;
}

void push_command(mem::vector<string>& a, const string& s)
{
 a.push_back(s);
#ifdef __MSDOS__
 if(s == "cmd") {
   a.push_back("/c");
   a.push_back("start");
   a.push_back("\"\"");
 }
#endif
}

void popupHelp() {
#if defined(_WIN32)
 auto const pdfviewer= getSetting<string>("pdfviewer");
 string const docPath= docdir + dirsep + "asymptote.pdf";
 if (!pdfviewer.empty())
 {
   mem::vector<string> cmd;
   push_command(cmd, pdfviewer);

   if (auto const viewerOpts= getSetting<string>("pdfviewerOptions"); !viewerOpts.empty())
   {
     istringstream viewerOptStream(viewerOpts);
     string tmp;
     while (viewerOptStream >> tmp)
     {
       if (!tmp.empty())
       {
         cmd.push_back(tmp);
       }
     }
   }
   cmd.push_back(docPath);
   System(cmd, 0, false, "pdfviewer", "your PDF viewer");
 }
 else
 {
   // pdf viewer not given, let windows decide which program to use
   camp::w32::checkShellExecuteResult(
           reinterpret_cast<INT_PTR>(ShellExecuteA(nullptr, "open", docPath.c_str(), nullptr, nullptr, SW_SHOWNORMAL)),
           false
   );
 }
#else
 // If the popped-up help is already running, pid stores the pid of the viewer.
 static int pid=0;

 // Status is ignored.
 static int status=0;

 // If the help viewer isn't running (or its last run has termined), launch the
 // viewer again.
 if (pid==0 || (waitpid(pid, &status, WNOHANG) == pid)) {
   mem::vector<string> cmd;
   push_command(cmd,getSetting<string>("pdfviewer"));
   string viewerOptions=getSetting<string>("pdfviewerOptions");
   if(!viewerOptions.empty())
     cmd.push_back(viewerOptions);
   cmd.push_back(docdir+dirsep+"asymptote.pdf");
   status=System(cmd,0,false,"pdfviewer","your PDF viewer",&pid);
 }
#endif
}

const char *intrange="integer argument is outside valid range";
const char *uintrange="integer argument is outside valid unsigned range";

unsigned unsignedcast(Int n)
{
 if(n < 0 || n/2 > INT_MAX)
   vm::error(uintrange);
 return (unsigned) n;
}

unsignedInt unsignedIntcast(Int n)
{
 if(n < 0)
   vm::error(uintrange);
 return (unsignedInt) n;
}

int intcast(Int n)
{
 if(Abs(n) > INT_MAX)
   vm::error(intrange);
 return (int) n;
}

Int Intcast(unsignedInt n)
{
 if(n > (unsignedInt) Int_MAX)
   vm::error(intrange);
 return (Int) n;
}

bool fileExists(string const& path)
{
#if defined(_WIN32)
 return PathFileExistsA(path.c_str());
#else
 return access(path.c_str(), R_OK) == 0;
#endif
}