/***************************************************************************
**
**  This file is part of GeopsyLand.
**
**  GeopsyLand 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.
**
**  GeopsyLand 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 Foobar.  If not, see <http://www.gnu.org/licenses/>
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2018-12-03
**  Copyright: 2018-2019
**    Marc Wathelet (ISTerre, Grenoble, France)
**
***************************************************************************/

#include "MiniBash.h"
#include "Terminal.h"
#include "TerminalProcess.h"

/*!
  \class MiniBash MiniBash.h
  \brief Brief description of class still missing

  Full description of class still missing
*/

#define HISTORY_DEPTH 1000
/*!
  Description of constructor still missing
*/
MiniBash::MiniBash(Terminal * terminal)
{
  TRACE;
  _terminal=terminal;
  _status=Idle;

  _builtInCommands << "cat"
                   << "ls"
                   << "echo"
                   << "help"
                   << "cd"
                   << "mkdir"
/*                   << "cp"
                   << "mv"
                   << "rm"
                   << "head"
                   << "tail" */
                   << "pwd";

  QSettings& reg=CoreApplication::instance()->settings();
  _history=reg.value("history").toStringList();
  _currentHistoryIndex=_history.count()-1;
}

/*!
  Description of destructor still missing
*/
MiniBash::~MiniBash()
{
  TRACE;
  qDeleteAll(_processes);
  QSettings& reg=CoreApplication::instance()->settings();
  reg.setValue("history", _history);
}

void MiniBash::parse(const QString& text)
{
  TRACE;
  qDeleteAll(_processes);
  _processes.clear();
  _buffer+=text.trimmed();
  if(_buffer.isEmpty()) {
    _status=Idle;
  } else {
    if(parse()) {
      _history.append(_buffer);
      if(_history.count()>HISTORY_DEPTH) {
        _history.removeFirst();
      }
      _currentHistoryIndex=_history.count();
      if(exec()) {
        if(!_processes.isEmpty() && !_processes.last()->isDetached())
        _status=Processing;
      } else {
        _status=Idle;
      }
      _buffer.clear();
    } else {
      if(_buffer.endsWith('\\')) {
        _buffer.chop(1);
      } else {
        _buffer+="\n";
      }
      _status=WaitMoreData;
    }
  }
}

QStringList MiniBash::availableCommands() const
{
  QStringList list=_builtInCommands;
  for(QStringList::const_iterator it=_paths.begin(); it!=_paths.end(); it++) {
    QDir d(*it);
    list.append(d.entryList(QDir::Files | QDir::Executable | QDir::Readable, QDir::Name));
  }
  return list;
}

QStringList MiniBash::complete(const QString& text, QString& completion) const
{
  static const QDir::Filters commonFilter=QDir::Readable | QDir::CaseSensitive | QDir::NoDotAndDotDot;
  bool firstWord=true;
  QString word=lastWord(text, firstWord);
  QFileInfo fi(word);
  QStringList nameFilter;
  QString fileName=fi.fileName();
  nameFilter << fileName+"*";
  QDir d(fi.path());
  QStringList possibilities;
  QStringList files;
  if(firstWord) {
    // Executable built-in commands and executable in PATH
    QStringList cmds=availableCommands();
    for(QStringList::const_iterator it=cmds.begin(); it!=cmds.end(); it++) {
      if(it->startsWith(word)) {
        possibilities.append(*it);
      }
    }
    // Files
    if(word.contains("/")) {
      QStringList dirs=d.entryList(nameFilter, QDir::Dirs | QDir::Executable | commonFilter, QDir::Name);
      TerminalProcess::addDirMark(dirs);
      possibilities.append(dirs);
      files=d.entryList(nameFilter, QDir::Files | QDir::Executable | commonFilter, QDir::Name);
      possibilities.append(files);
    } else {
      QStringList dirs=d.entryList(nameFilter, QDir::Dirs | QDir::Executable | commonFilter, QDir::Name);
      TerminalProcess::addDirMark(dirs);
      possibilities.append(dirs);
    }
  } else {
    QStringList dirs=d.entryList(nameFilter, QDir::Dirs | QDir::Executable | commonFilter, QDir::Name);
    TerminalProcess::addDirMark(dirs);
    possibilities.append(dirs);
    files=d.entryList(nameFilter, QDir::Files | commonFilter, QDir::Name);
    possibilities.append(files);
  }
  possibilities.sort(Qt::CaseInsensitive);
  // Find the common starting pattern of all possibilities
  switch(possibilities.count()) {
  case 0:
    break;
  case 1:
    completion=possibilities.first().mid(fileName.count());
    break;
  default:
    int mCount=INT_MAX;
    for(int i=possibilities.count()-2; i>=0; i--) {
      mCount=matchingCount(possibilities.at(i), possibilities.at(i+1), mCount);
    }
    ASSERT(mCount>=fileName.count());
    completion=possibilities.first().mid(fileName.count(), mCount-fileName.count());
    break;
  }
  return possibilities;
}

/*!

*/
QString MiniBash::lastWord(const QString& text, bool& firstWord)
{
  const QChar * ptr=text.data();
  QString s;
  while(true) {
    switch (ptr->unicode()) {
    case '\0':
      return s;
    case '\\':
      ptr++;
      if(ptr->unicode()=='\0') {
        return s;
      }
      break;
    case '"':
      if(!skipDoubleQuoteString(ptr, s)) {
        return QString();
      }
      break;
    case '\'':
      if(!skipSimpleQuoteString(ptr, s)) {
        return QString();
      }
      break;
    case '|':
    case '&':
      ptr++;
      while(ptr->unicode()==' ') {
        ptr++;
      }
      ptr--;
      s.clear();
      firstWord=true;
      break;
    case '>':
    case ' ':
      firstWord=false;
      s.clear();
      break;
    default:
      s+=*ptr;
      break;
    }
    ptr++;
  }
}

int MiniBash::matchingCount(const QString& s1, const QString& s2, int maxCount)
{
  const QChar * ptr1=s1.data();
  const QChar * ptr2=s2.data();
  if(maxCount>s1.count()) {
    maxCount=s1.count();
  }
  if(maxCount>s2.count()) {
    maxCount=s2.count();
  }
  for(int i=0; i<maxCount; i++) {
    if(ptr1[i]!=ptr2[i]) {
      return i;
    }
  }
  return maxCount;
}

/*!
  Return true if it can be fully parsed, else false is returned if there are missing parts:
  open quote or unfinished pipe.
*/
bool MiniBash::parse()
{
  const QChar * ptr=_buffer.data();
  TerminalProcess * p=new TerminalProcess(this);
  QString s;
  while(true) {
    switch (ptr->unicode()) {
    case '\0':
      p->addArgument(s);
      if(p->name().isEmpty()) {
        delete p;
        return false;
      } else {
        _processes.append(p);
        connect(p, SIGNAL(finished()), this, SLOT(oneProcessFinished()));
        connect(p, SIGNAL(exit()), this, SLOT(exit()), Qt::QueuedConnection);
        return true;
      }
    case ' ':
      p->addArgument(s);
      s.clear();
      break;
    case '\\':
      ptr++;
      if(ptr->unicode()=='\0') {
        delete p;
        return false;
      }
      break;
    case '"':
      if(!skipDoubleQuoteString(ptr, s)) {
        delete p;
        return false;
      }
      break;
    case '\'':
      if(!skipSimpleQuoteString(ptr, s)) {
        delete p;
        return false;
      }
      break;
    case '|':
      p->addArgument(s);
      s.clear();
      _processes.append(p);
      connect(p, SIGNAL(finished()), this, SLOT(oneProcessFinished()));
      {
        TerminalProcess * np=new TerminalProcess(this);
        p->setOutputProcess(np);
        np->setInputProcess(p);
        p=np;
      }
      break;
    case '>':
      p->addArgument(s);
      s.clear();
      ptr++;
      if(ptr->unicode()=='>') {
        p->setOutputType(TerminalProcess::FileAppend);
      } else {
        p->setOutputType(TerminalProcess::FileNew);
        ptr--;
      }
      break;
    case '&':
      p->addArgument(s);
      s.clear();
      p->setDetached(true);
      _processes.append(p);
      connect(p, SIGNAL(finished()), this, SLOT(oneProcessFinished()));
      connect(p, SIGNAL(exit()), this, SLOT(exit()), Qt::QueuedConnection);
      return true;
    default:
      s+=*ptr;
      break;
    }
    ptr++;
  }
}

/*!
  Return true if the quoted string is fully retrieved
*/
bool MiniBash::skipDoubleQuoteString(const QChar *& ptr, QString& s)
{
  TRACE;
  ptr++;
  while(true) {
    switch (ptr->unicode()) {
    case '\0':
      return false;
    case '\\':
      ptr++;
      if(ptr->unicode()=='\0') {
        return false;
      }
      break;
    case '"':
      return true;
    default:
      s+=*ptr;
      break;
    }
    ptr++;
  }
}

/*!
  Return true if the quoted string is fully retrieved
*/
bool MiniBash::skipSimpleQuoteString(const QChar *& ptr, QString& s)
{
  TRACE;
  ptr++;
  while(true) {
    switch (ptr->unicode()) {
    case '\0':
      return false;
    case '\\':
      ptr++;
      if(ptr->unicode()=='\0') {
        return false;
      }
      break;
    case '\'':
      return true;
    default:
      s+=*ptr;
      break;
    }
    ptr++;
  }
}

/*!
  Return false if one of the name of a process cannot be resolved.
*/
bool MiniBash::exec()
{
  TRACE;
  for(QList<TerminalProcess *>::iterator it=_processes.begin(); it!=_processes.end(); it++) {
    if(!(*it)->resolvePath(_paths, this)) {
      return false;
    }
  }
  for(QList<TerminalProcess *>::iterator it=_processes.begin(); it!=_processes.end(); it++) {
    (*it)->start();
  }
  return true;
}

void MiniBash::oneProcessFinished()
{
  TerminalProcess * p=qobject_cast<TerminalProcess *>(sender());
  ASSERT(p);
  p->closeStandardOutput();
  for(QList<TerminalProcess *>::iterator it=_processes.begin(); it!=_processes.end(); it++) {
    if((*it)->isRunning()) {
      return;
    }
  }
  _status=Idle;
  emit processFinished();
}

QString MiniBash::previousHistory()
{
  if(_currentHistoryIndex<=0) {
    _currentHistoryIndex=_history.count()-1;
  } else {
    _currentHistoryIndex--;
  }
  if(_currentHistoryIndex>=0 && _currentHistoryIndex<_history.count()) {
    return _history.at(_currentHistoryIndex);
  } else {
    return QString();
  }
}

QString MiniBash::nextHistory()
{
  if(_currentHistoryIndex>=_history.count()-1) {
    _currentHistoryIndex=0;
  } else {
    _currentHistoryIndex++;
  }
  if(_currentHistoryIndex>=0 && _currentHistoryIndex<_history.count()) {
    return _history.at(_currentHistoryIndex);
  } else {
    return QString();
  }
}

void MiniBash::exit()
{
  _terminal->exit();
}

bool MiniBash::killCurrentProcess()
{
  bool isRunning=false;
  for(QList<TerminalProcess *>::iterator it=_processes.begin(); it!=_processes.end(); it++) {
    if((*it)->isRunning()) {
      isRunning=true;
      (*it)->kill();
    }
  }
  _status=Idle;
  return isRunning;
}

QString MiniBash::fromNativePath(const QString& p)
{
#ifdef Q_OS_WIN
  QString sp;
  QRegExp drive("^([a-zA-Z]):[\\/]");
  if(drive.indexIn(p)==0) {
    sp="/";
    sp+=drive.cap(1);
    if(p.count()>3) {
      sp+="/";
      sp+=p.mid(3);
    }
  } else {
    sp=p;
  }
  sp.replace("\\", "/");
  return sp;
#else
  return p;
#endif
}

QString MiniBash::toNativePath(const QString& p)
{
#ifdef Q_OS_WIN
  QString sp;
  QRegExp drive("^/([a-zA-Z])");
  if(drive.indexIn(p)==0) {
    if(p.count()==2) {
      sp=drive.cap(1);
      sp+=":/";
    } else if(p.mid(2)=="/") {
      sp=drive.cap(1);
      sp+=":";
      sp+=p.mid(2);
    }
  } else {
    sp=p;
  }
  return sp;
#else
  return p;
#endif
}

