/*****
 * process.cc
 * Andy Hammerlindl 2006/08/19
 *
 * Handles processing blocks of code (including files, strings, and the
 * interactive prompt, for listing and parse-only modes as well as actually
 * running it.
 *****/

#include "types.h"
#include "errormsg.h"
#include "genv.h"
#include "coenv.h"
#include "stm.h"
#include "settings.h"
#include "vm.h"
#include "program.h"
#include "interact.h"
#include "envcompleter.h"

#include "fileio.h"

#include "stack.h"
#include "runtime.h"
#include "texfile.h"

#include "asyprocess.h"

namespace camp {
pen& defaultpen() {
  return processData().defaultpen;
}
}

unsigned int count=0;

namespace run {
void cleanup();
void exitFunction(vm::stack *Stack);
void updateFunction(vm::stack *Stack);
void purge(Int divisor=0);
}

namespace vm {
bool indebugger;
}

using namespace settings;

using absyntax::file;
using trans::genv;
using trans::coenv;
using trans::env;
using trans::coder;
using types::record;

using interact::interactive;
using interact::uptodate;

using absyntax::runnable;
using absyntax::block;

mem::stack<processDataStruct *> processDataStack;

// Exception-safe way to push and pop the process data.
class withProcessData {
  // Do not let this object be dynamically allocated.
  void *operator new(size_t);

  processDataStruct *pd_ptr;
public:

  withProcessData(processDataStruct *pd_ptr) : pd_ptr(pd_ptr)
  {
    processDataStack.push(pd_ptr);
  }

  ~withProcessData()
  {
    assert(processDataStack.top() == pd_ptr);
    processDataStack.pop();
  }
};

processDataStruct &processData() {
  assert(!processDataStack.empty());
  return *processDataStack.top();
}

// A process environment.  Basically this just serves as short-hand to start a
// new global environment (genv) and new process data at the same time.  When
// it goes out of scope, the process data is popped off the stack.  This also
// ensures that the data is popped even if an exception is thrown.
class penv {
  genv *_ge;
  processDataStruct _pd;

  // Do not let this object be dynamically allocated.
  void *operator new(size_t);

public:
  penv() : _ge(0), _pd() {
    // Push the processData first, as it needs to be on the stack before genv
    // is initialized.
    _pd.topPos = nullPos;
    processDataStack.push(&_pd);
    _ge = new genv;
  }

  virtual ~penv() {
    processDataStack.pop();
    delete _ge;
  }

  genv &ge() { return *_ge; }

  processDataStruct *pd() { return &_pd; }
};


void init(bool resetpath=true)
{
  vm::indebugger=false;
  uptodate=false;
  if(resetpath)
    setPath("");  /* On second and subsequent calls, sets the path
                     to what it was when the program started. */
}

// This helper class does nothing but call the interactiveTrans method of the
// base object in place of trans, so that the runnable can exhibit special
// behaviour when run at the interactive prompt.
class interactiveRunnable : public runnable {
  runnable *base;

public:
  interactiveRunnable(runnable *base)
    : runnable(base->getPos()), base(base) {}

  void prettyprint(ostream &out, Int indent) {
    absyntax::prettyname(out, "interactiveRunnable", indent, base->getPos());
    base->prettyprint(out, indent+1);
  }

  void trans(coenv &e) {
    base->interactiveTrans(e);
  }

  void transAsField(coenv &e, types::record *r) {
    // There is no interactiveTransAsField, as fields aren't declared at the top
    // level of the interactive prompt.
    base->transAsField(e, r);
  }
};




// How to run a runnable in runnable-at-a-time mode.
bool runRunnable(runnable *r, coenv &e, istack &s, transMode tm=TRANS_NORMAL) {
  e.e.beginScope();

  lambda *codelet= tm==TRANS_INTERACTIVE ?
    interactiveRunnable(r).transAsCodelet(e) :
    r->transAsCodelet(e);
  em.sync();
  if(!em.errors()) {
    if(getSetting<bool>("translate")) print(cout,codelet->code);
    s.run(codelet);

    // Commits the changes made to the environment.
    e.e.collapseScope();
  } else {
    e.e.endScope(); // Remove any changes to the environment.

    // Should an interactive error hurt the status?
    em.statusError();

    return false;
  }
  return true;
}

void runAutoplain(coenv &e, istack &s) {
  absyntax::runnable *r=absyntax::autoplainRunnable();
  runRunnable(r,e,s);
}

// icore


// preRun and postRun are the optional activities that take place before and
// after running the code specified.  They can be overridden by a derived
// class that wishes different behaviour.
void icore::preRun(coenv &e, istack &s) {
  if(getSetting<bool>("autoplain"))
    runAutoplain(e,s);
}

void icore::postRun(coenv &, istack &s) {
  run::exitFunction(&s);
}

void icore::doRun(bool purge, transMode tm) {
  em.sync();
  if(em.errors())
    return;

  try {
    if(purge) run::purge();

    penv pe;
    env base_env(pe.ge());
    coder base_coder(nullPos, "icore::doRun");
    coenv e(base_coder,base_env);

    vm::interactiveStack s;
    s.setInitMap(pe.ge().getInitMap());
    s.setEnvironment(&e);

    preRun(e,s);

    if(purge) run::purge();

    // Now that everything is set up, run the core.
    run(e,s,tm);

    postRun(e,s);

  } catch(std::bad_alloc&) {
    outOfMemory();
  } catch(quit const&) {
    // Exception to quit running the current code. Nothing more to do.
  } catch(handled_error const&) {
    em.statusError();
  }

  run::cleanup();

  em.clear();
}


void icore::process(bool purge) {
  if (!interactive && getSetting<bool>("parseonly"))
    doParse();
  else if (getSetting<bool>("listvariables"))
    doList();
  else
    doRun(purge);
}

itree::itree(string name) : name(name), cachedTree(0) { }


block *itree::getTree() {
  if (cachedTree==0) {
    try {
      cachedTree=buildTree();
    } catch(handled_error const&) {
      em.statusError();
      return 0;
    }
  }
  return cachedTree;
}

string itree::getName() {
  return name;
}

void itree::doParse() {
  block *tree=getTree();
  em.sync();
  if(tree && !em.errors())
    tree->prettyprint(cout, 0);
}

void itree::doList() {
  block *tree=getTree();
  if (tree) {
    penv pe;
    record *r=tree->transAsFile(pe.ge(), symbol::trans(getName()));
    if(r)
      r->e.list(r);
  }
}

void itree::run(coenv &e, istack &s, transMode tm) {
  block *tree=getTree();
  if (tree) {
    for(mem::list<runnable *>::iterator r=tree->stms.begin();
        r != tree->stms.end(); ++r) {
      processData().fileName=(*r)->getPos().filename();
      if(!em.errors() || debug)
        runRunnable(*r,e,s,tm);
    }
  }
}

void itree::doExec(transMode tm) {
  // Don't prepare an environment to run the code if there isn't any code.
  if (getTree())
    icore::doRun(false,tm);
}

void printGreeting(bool interactive) {
  if(!getSetting<bool>("quiet")) {
    cout << "Welcome to " << PACKAGE_NAME << " version " << REVISION;
    if(interactive)
      cout << " (to view the manual, type help)";
    cout << endl;
  }
}

ifile::ifile(const string& filename)
  : itree(filename),
    filename(filename),
    outname(stripDir(stripExt(string(filename == "-" ? settings::outname() : filename), suffix))) {}

block *ifile::buildTree() {
  return !filename.empty() ? parser::parseFile(filename,"Loading") : 0;
}

void ifile::preRun(coenv& e, istack& s) {
  outname_save=getSetting<string>("outname");
  if(stripDir(outname_save).empty())
    Setting("outname")=outname_save+outname;

  itree::preRun(e, s);
}

void ifile::postRun(coenv &e, istack& s) {
  itree::postRun(e, s);

  Setting("outname")=outname_save;
}

void ifile::process(bool purge) {
  if(verbose > 1) printGreeting(false);
  try {
    init();
  } catch(handled_error const&) {
  }

  if (verbose >= 1)
    cout << "Processing " << outname << endl;

  try {
    icore::process(purge);
  }
  catch(handled_error const&) {
    em.statusError();
  }
}

// Add a semi-colon terminator, if one is not there.
string terminateLine(const string line) {
  return (!line.empty() && *(line.rbegin())!=';') ? (line+";") : line;
}

// cleanLine changes a C++ style comment (//) into a C-style comment (/* */) so
// that comments don't absorb subsequent lines of code when multiline input is
// collapsed to a single line in the history.
//
// ex.  if (x==1) // test x
//        x=2;
// becomes
//      if (x==1) /* test x */ x=2    (all on one line)
//
// cleanLine is a mess because we have to check that the // is not in a string
// or c-style comment, which entails re-inventing much of the lexer.  The
// routine handles most cases, but multiline strings lose their newlines when
// recorded in the history.
typedef string::size_type size_type;
const size_type npos=string::npos;

inline size_type min(size_type a, size_type b) {
  return a < b ? a : b;
}

// If start is an offset somewhere within a string, this returns the first
// offset outside of the string. sym is the character used to start the string,
// ' or ".
size_type endOfString(const char sym, const string line, size_type start) {
  size_type endString=line.find(sym, start);
  if (endString == npos)
    return npos;

  size_type escapeInString=min(line.find(string("\\")+sym, start),
                               line.find("\\\\", start));
  if (endString < escapeInString)
    return endString+1;
  else
    return endOfString(sym, line, escapeInString+2);
}

// If start is an offset somewhere within a C-style comment, this returns the
// first offset outside of the comment.
size_type endOfComment(const string line, size_type start) {
  size_type endComment=line.find("*/", start);
  if (endComment == npos)
    return npos;
  else
    return endComment+2;
}

// Find the start of a string literal in the line.
size_type stringPos(const string line, size_type start) {
  if (start == npos)
    return npos;

  size_type pos=line.find_first_of("\'\"", start);
  if (pos == npos)
    return npos;

  // Skip over comments /*  */ and ignore anything after //
  size_type startComment=line.find("/*", start);
  size_type startLineComment=line.find("//", start);

  if (min(startComment,startLineComment) < pos)
    return stringPos(line,
                     startComment < startLineComment ?
                     endOfComment(line, startComment+2) :
                     npos);
  else
    // Nothing to skip over - the symbol actually starts a string.
    return pos;
}

// A multiline string should retain its newlines when collapsed to a single
// line.  This converts
//   'hello
//   there'
// to
//   'hello\nthere'
// and
//   "hello
//   there"
// to
//   "hello" '\n' "there"
// If the line doesn't end mid-string, this adds a space to the end to preserve
// whitespace, since it is the last function to touch the line.
string endString(const string line, size_type start) {
  assert(start!=npos);

  size_type pos=stringPos(line, start);

  if (pos==npos)
    // String ends in normal code.
    return line+" ";
  else {
    char sym=line[pos];
    size_type eos=endOfString(sym, line, pos+1);
    if (eos==npos) {
      // Line ends while in a string, attach a newline symbol.
      switch (line[pos]) {
        case '\'':
          return line+"\\n";
        case '\"':
          return line+"\" \'\\n\' \"";
        default:
          assert(False);
          return line;
      }
    }
    else {
      return endString(line, eos+1);
    }
  }
}

// Find the first // that isn't in a C-style comment or a string.
size_type slashPos(const string line, size_type start) {
  if (start == npos)
    return npos;

  size_type pos=line.find("//", start);
  if (pos == npos)
    return npos;

  // Skip over comments /*  */ and strings both "   " and '   '
  size_type startComment=line.find("/*", start);
  size_type startString=line.find_first_of("\'\"", start);

  if (min(startComment,startString) < pos)
    return slashPos(line,
                    startComment < startString ?
                    endOfComment(line, startComment+2) :
                    endOfString(line[startString], line, startString+1));
  else
    // Nothing to skip over - the // actually starts a comment.
    return pos;
}

string endCommentOrString(const string line) {
  size_type pos=slashPos(line, 0);
  if (pos == npos)
    return endString(line, 0);
  else {
    string sub=line;

    // Replace the first // by /*
    sub[pos+1]='*';

    // Replace any */ in the comment by *!
    while ((pos = line.find("*/", pos+2)) != npos)
      sub[pos+1]='!';

    // Tack on a */ at the end.
    sub.append(" */ ");
    return sub;
  }
}

bool isSlashed(const string line) {
  // NOTE: This doesn't fully handle escaped slashed in a string literal.
  unsigned n=line.size();
  return n > 0 ? line[line.size()-1] == '\\' : false;
}
string deslash(const string line) {
  return isSlashed(line) ? line.substr(0,line.size()-1) : line;
}

// This transforms a line in to the history, so that when more code is added
// after it, the code behaves the same as if there was a newline between the
// two lines.  This involves changing // style comments to /* */ style comments,
// and adding explicit newlines to multiline strings.
string cleanLine(const string line) {
  // First remove a trailing slash, if there is one.
  return endCommentOrString(deslash(line));
}

class iprompt : public icore {
  // Flag that is set to false to signal the prompt to exit.
  bool running;

  // Flag that is set to restart the main loop once it has exited.
  bool restart;

  // Code ran at start-up.
  string startline;

  void postRun(coenv &, istack &) {
  }

  // Commands are chopped into the starting word and the rest of the line.
  struct commandLine {
    string line;
    string word;
    string rest;

    commandLine(string line) : line(line) {
      string::iterator c=line.begin();

      // Skip leading whitespace
      while (c != line.end() && isspace(*c))
        ++c;

      // Only handle identifiers starting with a letter.
      if (c != line.end() && isalpha(*c)) {
        // Store the command name.
        while (c != line.end() && (isalnum(*c) || *c=='_')) {
          word.push_back(*c);
          ++c;
        }
      }

      // Copy the rest to rest.
      while (c != line.end()) {
        rest.push_back(*c);
        ++c;
      }

#if 0
      cerr << "line: " << line << endl;
      cerr << "word: " << word << endl;
      cerr << "rest: " << rest << endl;
      cerr << "simple: " << simple() << endl;
#endif
    }

    // Simple commands have at most spaces or semicolons after the command word.
    bool simple() {
      for (string::iterator c=rest.begin(); c != rest.end(); ++c)
        if (!isspace(*c) && *c != ';')
          return false;
      return true;
    }
  };


  // The interactive prompt has special functions that cannot be implemented as
  // normal functions.  These special functions take a commandLine as an argument
  // and return true if they can handle the command.  If false is returned, the
  // line is treated as a normal line of code.
  // commands is a map of command names to methods which implement the commands.
  typedef bool (iprompt::*command)(coenv &, istack &, commandLine);
  typedef mem::map<const string, command> commandMap;
  commandMap commands;

  bool exit(coenv &, istack &, commandLine cl) {
    if (cl.simple()) {
      // Don't store exit commands in the history file.
      interact::deleteLastLine();
      running=false;
      return true;
    }
    else return false;
  }

  bool q(coenv &e, istack &s, commandLine cl) {
    if(e.e.ve.getType(symbol::trans("q"))) return false;
    return exit(e,s,cl);
  }

  bool reset(coenv &, istack &, commandLine cl) {
    if (cl.simple()) {
      running=false;
      restart=true;
      startline="";
      run::purge();
      return true;
    }
    else return false;
  }

  bool help(coenv &, istack &, commandLine cl) {
    if (cl.simple()) {
      popupHelp();
      return true;
    }
    else return false;
  }

  bool erase(coenv &e, istack &s, commandLine cl) {
    if (cl.simple()) {
      runLine(e,s,"erase();");
      return true;
    }
    else return false;
  }

  bool input(coenv &, istack &, commandLine cl) {
    running=false;
    restart=true;
    startline="include "+cl.rest;
    return true;
  }

  void initCommands() {
#define ADDCOMMAND(name, func)                  \
    commands[#name]=&iprompt::func

    // keywords.py looks for ADDCOMMAND to identify special commands in the
    // auto-completion.
    ADDCOMMAND(quit,exit);
    ADDCOMMAND(q,q);
    ADDCOMMAND(exit,exit);
    ADDCOMMAND(reset,reset);
    ADDCOMMAND(erase,erase);
    ADDCOMMAND(help,help);
    ADDCOMMAND(input,input);

#undef ADDCOMMAND
  }

  bool handleCommand(coenv &e, istack &s, string line) {
    commandLine cl(line);
    if (cl.word != "") {
      commandMap::iterator p=commands.find(cl.word);
      if (p != commands.end()) {
        // Execute the command.
        command &com=p->second;
        return (this->*com)(e,s,cl);
      }
      else
        return false;
    }
    else
      return false;
  }

  void addToHistory(string line) {
    interact::addToHistory(line);
  }

  void addToLastLine(string line) {
    // Here we clean a line at the last possible point, when we know that more
    // code is going to be appended to it.
    string last=interact::getLastHistoryLine();
    interact::setLastHistoryLine(cleanLine(last)+line);
  }

  void terminateLastHistoryLine() {
    string last=interact::getLastHistoryLine();
    interact::setLastHistoryLine(terminateLine(last));
  }

  // Get input from the interactive prompt.  Such input may be over several
  // user-typed lines if he/she ends a line a with backslash to continue input
  // on the next line.  If continuation is true, the input started on a previous
  // line and is being continued (either because of a backslash or the parser
  // detecting it in multiline mode).
  string getline(bool continuation) {
    string prompt;
    if(!settings::xasy)
      prompt=getSetting<string>(continuation ? "prompt2" : "prompt");
    string line=interact::simpleline(prompt);

    if (continuation)
      addToLastLine(line);
    else
      addToHistory(line);

    // If the line ends in a slash, get more input.
    return isSlashed(line) ? line+"\n"+getline(true) :
      line;
  }

  // Continue taking input for a line until it properly parses, or a syntax
  // error occurs.  Returns the parsed code on success, and throws a
  // handled_error exception on failure.
  block *parseExtendableLine(string line) {
    block *code=parser::parseString(line, "-", true);
    if (code) {
      return code;
    } else {
      string nextline=getline(true);
      return parseExtendableLine(line+"\n"+nextline);
    }
  }

  // Continue taking input until a termination command is received from xasy.
  block *parseXasyLine(string line) {
    const string EOT="\x04\n";
    string s;
    while((s=getline(true)) != EOT)
      line += s;
    return parser::parseString(line, "-", true);
  }

  void runLine(coenv &e, istack &s, string line) {
    try {
      if(getSetting<bool>("multiline")) {
        block *code=parseExtendableLine(line);

        icode i(code);
        i.run(e,s,TRANS_INTERACTIVE);
      } else if(settings::xasy) {
        block *code=parseXasyLine(line);

        icode i(code);
        i.run(e,s,TRANS_INTERACTIVE);
      } else {
        // Add a semi-colon to the end of the line if one is not there.  Do this
        // to the history as well, so the transcript can be run as regular asy
        // code.  This also makes the history work correctly if the multiline
        // setting is changed within an interactive session.
        // It is added to the history at the last possible moment to avoid
        // tampering with other features, such as using a slash to extend a
        // line.
        terminateLastHistoryLine();
        istring i(terminateLine(line), "-");
        i.run(e,s,TRANS_INTERACTIVE);
      }

      run::updateFunction(&s);
      uptodate=false;

    } catch(handled_error const&) {
      vm::indebugger=false;
    } catch(interrupted&) {
      // Turn off the interrupted flag.
      em.Interrupt(false);
      uptodate=true;
      cout << endl;
    } catch(quit&) {
    }

    // Ignore errors from this line when trying to run subsequent lines.
    em.clear();
  }

  void runStartCode(coenv &e, istack &s) {
    if (!startline.empty())
      runLine(e, s, startline);
  }

public:
  iprompt() : running(false), restart(false), startline("") {
    initCommands();
  }

  void doParse() {}

  void doList() {}

  void run(coenv &e, istack &s, transMode=TRANS_NORMAL) {
    running=true;
    interact::setCompleter(new trans::envCompleter(e.e));

    runStartCode(e, s);

    while (running) {
      // Read a line from the prompt.
      string line=getline(false);
      // Check if it is a special command.
      if (handleCommand(e,s,line))
        continue;
      else
        runLine(e, s, line);
    }
  }

  void process(bool purge=false) {
    printGreeting(true);
    interact::init_interactive();
    try {
      setPath("",true);
    } catch(handled_error const&) {
    }

    do {
      try {
        init(false);
        restart=false;
        icore::process();
      } catch(interrupted&) {
        em.Interrupt(false);
        restart=true;
      } catch(EofException&) {
        restart=false;
      }
    } while(restart);

    interact::cleanup_interactive();
  }
};

void processCode(absyntax::block *code) {
  icode(code).process();
}
void processFile(const string& filename, bool purge) {
  ifile(filename).process(purge);
}
void processPrompt() {
  iprompt().process();
}

void runCode(absyntax::block *code) {
  icode(code).doExec();
}
void runString(const string& s, bool interactiveWrite) {
  istring(s).doExec(interactiveWrite ? TRANS_INTERACTIVE : TRANS_NORMAL);
}
void runFile(const string& filename) {
  ifile(filename).doExec();
}
void runPrompt() {
  iprompt().doRun();
}

void runCodeEmbedded(absyntax::block *code, trans::coenv &e, istack &s) {
  icode(code).run(e,s);
}
void runStringEmbedded(const string& str, trans::coenv &e, istack &s) {
  istring(str).run(e,s);
}
void runPromptEmbedded(trans::coenv &e, istack &s) {
  iprompt().run(e,s);
}

void doUnrestrictedList() {
  penv pe;
  env base_env(pe.ge());
  coder base_coder(nullPos, "doUnrestictedList");
  coenv e(base_coder,base_env);

  if (getSetting<bool>("autoplain"))
    absyntax::autoplainRunnable()->trans(e);

  e.e.list(0);
}

// Environment class used by external programs linking to the shared library.
class fullenv : public gc {
  penv pe;
  env base_env;
  coder base_coder;
  coenv e;

  vm::interactiveStack s;

public:
  fullenv() :
    pe(), base_env(pe.ge()), base_coder(nullPos, "fullenv"),
    e(base_coder, base_env), s()
  {
    s.setInitMap(pe.ge().getInitMap());
    s.setEnvironment(&e);

    // TODO: Add way to not run autoplain.
    runAutoplain(e, s);
  }

  coenv &getCoenv()
  {
    return e;
  }

  void runRunnable(runnable *r)
  {
    assert(!em.errors()); // TODO: Decide how to handle prior errors.

    try {
      { withProcessData token(pe.pd());
        ::runRunnable(r, e, s, TRANS_INTERACTIVE);
      }
    } catch(std::bad_alloc&) {
      // TODO: give calling application useful message.
      cerr << "out of memory" << endl;
    } catch (quit const&) {
      // I'm not sure whether this counts as successfully running the code or
      // not.
      cerr << "quit exception" << endl;
    } catch (handled_error const&) {
      // Normally, this is the result of an error that changes the return code
      // of the free-standing asymptote program.
      // An error should probably be reported to the application calling the
      // asymptote library.
      cerr << "handled error" << endl;
    }

    em.clear();
  }
};

fullenv &getFullEnv()
{
  static fullenv fe;
  return fe;
}

coenv &coenvInOngoingProcess()
{
  return getFullEnv().getCoenv();
}

void runInOngoingProcess(runnable *r)
{
  getFullEnv().runRunnable(r);
}
