/*

  Launcher for external processes


  Copyright © 2011-6 F.Hroch (hroch@physics.muni.cz)

  This file is part of Munipack.

  Munipack is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  Munipack is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with Munipack.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "mprocess.h"
#include <wx/wx.h>
#include <wx/app.h>
#include <wx/stream.h>
#include <wx/txtstrm.h>
#include <wx/log.h>
#include <wx/event.h>
#include <wx/process.h>
#include <wx/regex.h>
#include <wx/stopwatch.h>
#include <wx/datetime.h>
#include <iostream>
#include <queue>
#include <stdio.h>


using namespace std;

// -----

MuniProcess::MuniProcess(wxEvtHandler *h, const wxString& c, const wxArrayString& args):
  wxProcess(h),command(c),ArgsBuffer(args),exitcode(1),handler(h),
  timer(this),tick(250),MaxBuffer(10000),killing(false),verbose(true)
{
  SetEnvironment();

  argv = static_cast<wchar_t **>(malloc((args.GetCount()+2)*sizeof(wchar_t *)));
  argv[0] = wxStrdup(command.wc_str());
  size_t l = 1;
  for(size_t i = 0; i < args.GetCount(); i++)
    argv[l++] = wxStrdup(args[i].wc_str());
  argv[l] = 0;

  Bind(wxEVT_TIMER,&MuniProcess::OnTimer,this);
  Bind(wxEVT_END_PROCESS,&MuniProcess::OnFinish,this);
}

MuniProcess::~MuniProcess()
{
  Unbind(wxEVT_TIMER,&MuniProcess::OnTimer,this);
  Unbind(wxEVT_END_PROCESS,&MuniProcess::OnFinish,this);

  if( argv ) {
    for(size_t i = 0; argv[i] != 0; i++)
      free(argv[i]);
    free(argv);
  }
}

void MuniProcess::OnStart()
{
  wxLogDebug("Launching `" + command + "' ...");

  Redirect();
  long pid = wxExecute(argv,wxEXEC_ASYNC,this);

  if( pid <= 0 ) {
    wxLogError("Failed to launch the external command `" + command + "'.");
    return;
  }

  timer.Start(tick);
  stopwatch.Start();

  wxOutputStream *i = GetOutputStream();
  wxASSERT(i && i->IsOk());
  wxTextOutputStream out(*i);

  fitskeys(out);

  if( ! InputBuffer.IsEmpty() ) {
    for(size_t i = 0; i < InputBuffer.GetCount(); i++) {
      wxLogDebug(InputBuffer[i]);
      out << InputBuffer[i] << endl;
    }
  }
  CloseOutput();

  wxASSERT(wxProcess::Exists(pid));
}

void MuniProcess::SetEnvironment()
{
  // Switch-off buffering of gfortran's stdout and stderr.
  // We need this setting for on-the-fly parsing of outputs.
  wxSetEnv("GFORTRAN_UNBUFFERED_PRECONNECTED","Y");


  // set path for libexec, generally unportable (!)
  wxString xpath;
  wxGetEnv("PATH",&xpath);

#ifdef MUNIPACK_LIBEXEC_DIR
  xpath = wxString(MUNIPACK_LIBEXEC_DIR ":") + xpath;
#endif

  wxString libexecpath;
  if( wxGetEnv("MUNIPACK_LIBEXEC_PATH",&libexecpath) )
    xpath = libexecpath + wxString(":") + xpath;

  wxSetEnv("PATH",xpath.c_str());
}

void MuniProcess::SetInput(const wxArrayString& i)
{
  for(size_t l = 0; l < i.GetCount(); l++)
    InputBuffer.Add(i[l]);
}

void MuniProcess::Write(const char *line)
{
  InputBuffer.Add(wxString(line));
}


void MuniProcess::Write(const wxString& fmt, ...)
{
  wxString line;

  va_list par;
  va_start(par, fmt);
  line.PrintfV(fmt,par);
  va_end(par);

  InputBuffer.Add(line);
}

wxKillError MuniProcess::Kill(wxSignal sig, int flags)
{
  killing = true;
  return wxProcess::Kill(GetPid(),sig,flags);
}

void MuniProcess::OnTimer(wxTimerEvent& event)
{
  //  wxLogDebug("MuniProcess::OnTimer");
  Flush();
}

void MuniProcess::OnIdle(wxIdleEvent& event)
{
  wxLogDebug("MuniProcess::OnIdle");
  Flush();

  event.RequestMore();
  event.Skip();
}

bool MuniProcess::StopLine(const wxString& line, int& stopcode)
{
  // Processing of possible 'STOP <NUMBER>',

  wxRegEx re("^[[:space:]]*STOP[[:space:]]+([[:digit:]])[[:space:]]*$",
	     wxRE_DEFAULT|wxRE_ICASE);
  wxASSERT(re.IsValid());

  if( re.Matches(line) ) {

    wxString a(re.GetMatch(line,1));
    long s;
    if( a.ToLong(&s) ) {
      stopcode = s;
      return true;
    }
  }

  return false;
}


void MuniProcess::OnFinish(wxProcessEvent& event)
{
  exitcode = 1; // default is error

  wxTimeSpan ts(wxTimeSpan::Milliseconds(stopwatch.Time()));
  wxLogDebug(ts.Format("MuniProcess elapsed time: %Hh %Mm %S.%ls"));
  wxLogDebug("MuniProcess::OnFinish: %d %d.",event.GetPid(),event.GetExitCode());

  timer.Stop();
  Flush();

  /*

    On finish, we must test both the exit code and some error
    output. The testing of just exit code isn't adequate because
    the implementation of wxExecute returns -1 in case when waitpid
    gives 0 (no changes).

    The outline is little bit complicated by using of some
    Fortran utilities which prints STOP <NUMBER> to indicate
    their return statuses.

    By the way, this code looks for 'STOP 0' string as the last
    error output line of a subprocces. We are ensure that
    the process correctly finished.

    There is also another exception. When user requested killing
    of a process, we always returns non-zero. ???

    Shortly:

      Just when both event.GetExitCode() and STOP indicates 0 (zero)
      the exit 0 is returned. All others combinations generates
      non-zero status.


  */

  if( event.GetExitCode() == 0 ) {

    for(int i = ErrorBuffer.GetCount() - 1; i >= 0; i--) {
      int code;
      if( StopLine(ErrorBuffer.Item(i),code) ) {
	exitcode = code;
	break;
      }
    }
  }
  else
    exitcode = event.GetExitCode();


  if( exitcode != 0 ) {

    wxLogError("Executing of external utility `"+command+"' finished with doubts:");

    if( ArgsBuffer.GetCount() > 0 ) {
      wxLogError("=> Arguments:");
      for(size_t i = 0; i < ArgsBuffer.GetCount(); i++)
	wxLogError("[%d]: "+ArgsBuffer[i],(int)i);
    }

    if( InputBuffer.GetCount() > 0 ) {
      wxLogError("=> Standard input:");
      for(size_t i = 0; i < InputBuffer.GetCount(); i++)
	wxLogError(InputBuffer[i]);
    }

    if( OutputBuffer.GetCount() > 0 ) {
      wxLogError("=> Standard output:");
      for(size_t i = 0; i < OutputBuffer.GetCount(); i++)
	wxLogError(OutputBuffer[i]);
    }

    if( ErrorBuffer.GetCount() > 0 ) {
      wxLogError("=> Error output:");
      for(size_t i = 0; i < ErrorBuffer.GetCount(); i++)
	wxLogError(ErrorBuffer[i]);
    }

    /*

       There is also idea to save output in a structured document
       like XML to aditional parsing.

     */
  }

  wxQueueEvent(handler,new wxProcessEvent(event.GetId(),event.GetPid(),exitcode));
}

void MuniProcess::Flush()
{
  //  wxLogDebug("MuniProcess::Flush");

  wxStopWatch sw;
  long tlimit = tick/4;
  /* To give a chance of GUI to be updated, we must periodically
     interrupt this flushing. The value is derived from ticks and
     one should be short (about 10-50ms).
  */

  if( IsInputAvailable() ) {
    wxInputStream *i = GetInputStream();
    if( i ) {
      wxTextInputStream out(*i);
      size_t n = 0;
      sw.Start();
      while( i->IsOk() && ! i->Eof() && i->CanRead() && sw.Time() < tlimit ) {
	wxString line = out.ReadLine();
	if( line != "" ) {
	  if( verbose )
	    fprintf(stdout,"%s\n",static_cast<const char *>(line.char_str()));
	  OutputBuffer.Add(line);
	  n++;
	}
      }
      if( OutputBuffer.GetCount() > MaxBuffer )
	OutputBuffer.RemoveAt(0,n);
    }
  }

  if( IsErrorAvailable() ) {

    int loglevel = wxLog::GetLogLevel();

    wxInputStream *e = GetErrorStream();
    if( e ) {
      wxTextInputStream err(*e);
      size_t n = 0;
      sw.Start();
      while( e->IsOk() && ! e->Eof() && e->CanRead() && sw.Time() < tlimit ) {
	wxString line = err.ReadLine();

	if( line != "" ) {

	  // filter lines with STOP 0 to be quite
	  int code;
	  if( ! (StopLine(line,code) && code == 0 && loglevel < wxLOG_Info ))
	    //	    exitcode = code;
	    //	    break;
	    //	  }

	    //	  if( ! (line == "STOP 0" && loglevel < wxLOG_Info) )
	    fprintf(stderr,"%s\n",static_cast<const char *>(line.char_str()));

	  ErrorBuffer.Add(line);
	  n++;
	}
      }
      if( ErrorBuffer.GetCount() > MaxBuffer )
	ErrorBuffer.RemoveAt(0,n);
    }
  }
}

void MuniProcess::fitskeys(wxTextOutputStream& out)
{
  // redefine FITS keywords

  const char *keys[] = {
    "FITS_KEY_FILTER",
    "FITS_KEY_TEMPERATURE",
    "FITS_KEY_DATEOBS",
    "FITS_KEY_EXPTIME",
    "FITS_KEY_OBJECT",
    "FITS_KEY_SATURATE",
    "FITS_KEY_READNOISE",
    "FITS_KEY_GAIN",
    "FITS_KEY_AREA",
    "FITS_KEY_EPOCH",
    "FITS_KEY_LATITUDE",
    "FITS_KEY_LONGITUDE",
    "FITS_KEY_ALTITUDE",
    "FITS_KEY_AIRMASS",
    "FITS_KEY_TIME",
    0
  };

  for(int i = 0; keys[i] != 0; i++) {
    const char *var = getenv(keys[i]);
    if( var ) {
      wxString line;
      line.Printf("%s = '%s'",keys[i],var);
      wxLogDebug(line);
      out << line << endl;
    }
  }
}

void MuniProcess::WriteFiles(const wxString& filename,
			     const wxString& backup_name,
			     const wxString& new_name)
{
  wxASSERT(filename != "");
  InputBuffer.Add("FILE = '" + filename + "' '"  + backup_name + "' '" + new_name + "'");
}

void MuniProcess::WriteFile(const wxString& filename)
{
  wxASSERT(filename != "");
  InputBuffer.Add("FILE = '" + filename + "'");
}

void MuniProcess::WriteOutput(const wxString& filename, const wxString& backup)
{
  wxASSERT(filename != "");
  InputBuffer.Add("OUTPUT = '" + filename + "' '" + backup + "'");
}


// ------


MuniPipe::MuniPipe(wxEvtHandler *h): handler(h),exitcode(1)
{
  Bind(wxEVT_END_PROCESS,&MuniPipe::OnFinish,this);
}

MuniPipe::~MuniPipe()
{
  wxASSERT(procs.empty());
}

void MuniPipe::push(MuniProcess *p)
{
  if( p )
    procs.push(p);
}

void MuniPipe::Start()
{
  MuniProcess *p = procs.front();
  if( p ) {
    p->SetInput(OutputBuffer);
    p->OnPreProcess();
    p->OnStart();
  }
}

void MuniPipe::Stop()
{
  if( ! procs.empty() )
    procs.front()->Kill();
}

void MuniPipe::OnFinish(wxProcessEvent& event)
{
  wxLogDebug("MuniPipe::OnFinish %d %d",event.GetPid(),event.GetExitCode());
  exitcode = event.GetExitCode();

  if( ! procs.empty() ) {
    OutputBuffer = procs.front()->GetOutput();
    ErrorBuffer = procs.front()->GetErrors();
  }

  if( event.GetExitCode() != 0 ) {
    while( ! procs.empty() ) {
      MuniProcess *p = procs.front();
      delete p;
      procs.pop();
    }
    wxASSERT(procs.empty());
  }
  else {
    wxASSERT(!procs.empty());
    procs.front()->OnPostProcess();
    MuniProcess *p = procs.front();
    delete p;
    procs.pop();
  }

  //  wxLogDebug("MuniPipe::OnFinish Remaning processes: %d",(int) procs.size());

  if( ! procs.empty() )
    Start();

  else if( handler )
    wxQueueEvent(handler,event.Clone());

  else
    event.Skip();
}

wxArrayString MuniPipe::GetOutput() const
{
  if( ! procs.empty() )
    return procs.front()->GetOutput();
  else
    return OutputBuffer;
}

wxArrayString MuniPipe::GetErrors() const
{
  if( ! procs.empty() )
    return procs.front()->GetErrors();
  else
    return ErrorBuffer;
}
