/*
    knnntpclient.cpp

    KNode, the KDE newsreader
    Copyright (c) 1999-2001 the KNode authors.
    See file AUTHORS for details

    This program 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 2 of the License, or
    (at your option) any later version.
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software Foundation,
    Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, US
*/

#include <stdlib.h>
#include <algorithm>
#include <vector>

#include <tdelocale.h>
#include <tqtextcodec.h>
#include <tqmutex.h>

#include "kngroupmanager.h"
#include "knnntpclient.h"
#include "utilities.h"


KNNntpClient::KNNntpClient(int NfdPipeIn, int NfdPipeOut, TQMutex& nntpMutex)
: KNProtocolClient(NfdPipeIn,NfdPipeOut), mutex(nntpMutex)
{}


KNNntpClient::~KNNntpClient()
{}


// examines the job and calls the suitable handling method
void KNNntpClient::processJob()
{
  switch (job->type()) {
    case KNJobData::JTLoadGroups :
      doLoadGroups();
      break;
    case KNJobData::JTFetchGroups :
      doFetchGroups();
      break;
    case KNJobData::JTCheckNewGroups :
      doCheckNewGroups();
      break;
    case KNJobData::JTfetchNewHeaders :
    case KNJobData::JTsilentFetchNewHeaders :
      doFetchNewHeaders();
      break;
    case KNJobData::JTfetchArticle :
      doFetchArticle();
      break;
    case KNJobData::JTpostArticle :
      doPostArticle();
      break;
   case KNJobData::JTfetchSource :
      doFetchSource();
      break;
    default:
#ifndef NDEBUG
      tqDebug("knode: KNNntpClient::processJob(): mismatched job");
#endif
      break;
  }
}


void KNNntpClient::doLoadGroups()
{
  KNGroupListData *target = static_cast<KNGroupListData *>(job->data());
  sendSignal(TSloadGrouplist);

  if (!target->readIn(this))
    job->setErrorString(i18n("Unable to read the group list file"));
}


void KNNntpClient::doFetchGroups()
{
  KNGroupListData *target = static_cast<KNGroupListData *>(job->data());

  sendSignal(TSdownloadGrouplist);
  errorPrefix = i18n("The group list could not be retrieved.\nThe following error occurred:\n");

  progressValue = 100;
  predictedLines = 30000;     // rule of thumb ;-)

  if (!sendCommandWCheck("LIST",215))       // 215 list of newsgroups follows
    return;

  char *s, *line;
  TQString name;
  KNGroup::Status status;
  bool subscribed;

  while (getNextLine()) {
    line = getCurrentLine();
    if (line[0]=='.') {
      if (line[1]=='.')
        line++;        // collapse double period into one
      else
        if (line[1]==0)
          break;   // message complete
    }
    s = strchr(line,' ');
    if(!s) {
#ifndef NDEBUG
      tqDebug("knode: retrieved broken group-line - ignoring");
#endif
    } else {
      s[0] = 0;    // cut string

      name = TQString::fromUtf8(line);

      if (target->subscribed.contains(name)) {
        target->subscribed.remove(name);    // group names are unique, we wont find it again anyway...
        subscribed = true;
      } else
        subscribed = false;

      while (s[1]!=0) s++;   // the last character determines the moderation status
      switch (s[0]) {
        case 'n' : status = KNGroup::readOnly;
                   break;
        case 'y' : status = KNGroup::postingAllowed;
                   break;
        case 'm' : status = KNGroup::moderated;
                   break;
        default  : status = KNGroup::unknown;
      }

      target->groups->push_back(new KNGroupInfo(name,TQString(),false,subscribed,status));
    }
    doneLines++;
  }

  if (!job->success() || job->canceled())
    return;     // stopped...

  std::vector<KNGroupInfo*> tempVector;
  for (KNGroupInfo *g : *target->groups)
  {
    tempVector.push_back(g);
  }
  std::sort(tempVector.begin(), tempVector.end(), KNGroupInfo::PtrCompFn);

  if (target->getDescriptions) {
    errorPrefix = i18n("The group descriptions could not be retrieved.\nThe following error occurred:\n");
    progressValue = 100;
    doneLines = 0;
    predictedLines = target->groups->size();

    sendSignal(TSdownloadDesc);
    sendSignal(TSprogressUpdate);

    int rep;
    if (!sendCommand("LIST NEWSGROUPS",rep))
      return;

    if (rep == 215) {       // 215 informations follows
      TQString description;
      KNGroupInfo info;
      int pos;

      while (getNextLine()) {
        line = getCurrentLine();
        if (line[0]=='.') {
          if (line[1]=='.')
            line++;        // collapse double period into one
          else
            if (line[1]==0)
              break;   // message complete
        }
        s = line;
        while (*s != '\0' && *s != '\t' && *s != ' ') s++;
        if (*s == '\0') {
#ifndef NDEBUG
          tqDebug("knode: retrieved broken group-description - ignoring");
#endif
        } else {
          s[0] = 0;         // terminate groupname
          s++;
          while (*s == ' ' || *s == '\t') s++;    // go on to the description

          name = TQString::fromUtf8(line);
          if (target->codecForDescriptions)          // some countries use local 8 bit characters in the tag line
            description = target->codecForDescriptions->toUnicode(s);
          else
            description = TQString::fromLocal8Bit(s);
          info.name = name;

          if (std::binary_search(tempVector.begin(), tempVector.end(), &info, KNGroupInfo::PtrCompFn))
          {
            KNGroupInfo *posGroup = *std::lower_bound(tempVector.begin(), tempVector.end(), &info, KNGroupInfo::PtrCompFn);
            posGroup->description = description;
          }
        }
        doneLines++;
      }
    }

    if (!job->success() || job->canceled())
      return;     // stopped...
  }

  target->groups->clear();
  for (KNGroupInfo *g : tempVector)
  {
    target->groups->push_back(g);
  }

  sendSignal(TSwriteGrouplist);
  if (!target->writeOut())
    job->setErrorString(i18n("Unable to write the group list file"));

}


void KNNntpClient::doCheckNewGroups()
{
  KNGroupListData *target = static_cast<KNGroupListData *>(job->data());

  sendSignal(TSdownloadNewGroups);
  errorPrefix = i18n("New groups could not be retrieved.\nThe following error occurred:\n");

  progressValue = 100;
  predictedLines = 30;     // rule of thumb ;-)

  TQCString cmd;
  cmd.sprintf("NEWGROUPS %.2d%.2d%.2d 000000",target->fetchSince.year()%100,target->fetchSince.month(),target->fetchSince.day());
  if (!sendCommandWCheck(cmd,231))      // 231 list of new newsgroups follows
    return;

  char *s, *line;
  TQString name;
  KNGroup::Status status;
  std::list<KNGroupInfo*> tmpList;

  while (getNextLine()) {
    line = getCurrentLine();
    if (line[0]=='.') {
      if (line[1]=='.')
        line++;        // collapse double period into one
      else
        if (line[1]==0)
          break;   // message complete
    }
    s = strchr(line,' ');
    if(!s) {
#ifndef NDEBUG
      tqDebug("knode: retrieved broken group-line - ignoring");
#endif
    } else {
      s[0] = 0;    // cut string
      name = TQString::fromUtf8(line);

      while (s[1]!=0) s++;   // the last character determines the moderation status
      switch (s[0]) {
        case 'n' : status = KNGroup::readOnly;
                   break;
        case 'y' : status = KNGroup::postingAllowed;
                   break;
        case 'm' : status = KNGroup::moderated;
                   break;
        default  : status = KNGroup::unknown;
      }

      tmpList.push_back(new KNGroupInfo(name,TQString(),true,false,status));
    }
    doneLines++;
  }

  bool statusOk = true;

  if (!job->success() || job->canceled())
  {
    statusOk = false;     // stopped...
  }

  if (statusOk && target->getDescriptions) {
    errorPrefix = i18n("The group descriptions could not be retrieved.\nThe following error occurred:\n");
    progressValue = 100;
    doneLines = 0;
    predictedLines = tmpList.size()*3;

    sendSignal(TSdownloadDesc);
    sendSignal(TSprogressUpdate);

    cmd = "LIST NEWSGROUPS ";
    TQStrList desList;
    char *s;
    int rep;

    for (KNGroupInfo *group : tmpList)
    {
      if (!sendCommand(cmd+group->name.utf8(),rep))
      {
        statusOk = false;
        break;
      }
      if (rep != 215)        // 215 informations follows
        break;
      desList.clear();
      if (!getMsg(desList))
      {
        statusOk = false;
        break;
      }

      if (desList.count()>0) {        // group has a description
        s = desList.first();
        while (*s !=- '\0' && *s != '\t' && *s != ' ') s++;
        if (*s == '\0') {
#ifndef NDEBUG
          tqDebug("knode: retrieved broken group-description - ignoring");
#endif
        } else {
          while (*s == ' ' || *s == '\t') s++;    // go on to the description
          if (target->codecForDescriptions)          // some countries use local 8 bit characters in the tag line
            group->description = target->codecForDescriptions->toUnicode(s);
          else
            group->description = TQString::fromLocal8Bit(s);
        }
      }
    }
  }

  if (statusOk)
  {
    sendSignal(TSloadGrouplist);

    if (!target->readIn()) {
      job->setErrorString(i18n("Unable to read the group list file"));
    }
    else
    {
      target->merge(&tmpList);
      sendSignal(TSwriteGrouplist);
      if (!target->writeOut()) {
        job->setErrorString(i18n("Unable to write the group list file"));
      }
    }
  }

  for (KNGroupInfo *group : tmpList)
  {
    delete group;
  }
}


void KNNntpClient::doFetchNewHeaders()
{
  KNGroup* target=static_cast<KNGroup*>(job->data());
  char* s;
  int first=0, last=0, oldlast=0, toFetch=0, rep=0;
  TQCString cmd;

  target->setLastFetchCount(0);

  sendSignal(TSdownloadNew);
  errorPrefix=i18n("No new articles could be retrieved for\n%1/%2.\nThe following error occurred:\n")
              .arg(account.server()).arg(target->groupname());

  cmd="GROUP ";
  cmd+=target->groupname().utf8();
  if (!sendCommandWCheck(cmd,211)) {       // 211 n f l s group selected
    return;
  }

  currentGroup = target->groupname();

  progressValue = 90;

  s = strchr(getCurrentLine(),' ');
  if (s) {
    s++;
    s = strchr(s,' ');
  }
  if (s) {
    s++;
    first=atoi(s);
    target->setFirstNr(first);
    s = strchr(s,' ');
  }
  if (s) {
    last=atoi(s);
  } else {
    TQString tmp=i18n("No new articles could be retrieved.\nThe server sent a malformatted response:\n");
    tmp+=getCurrentLine();
    job->setErrorString(tmp);
    closeConnection();
    return;
  }

  if(target->lastNr()==0) {   //first fetch
    if(first>0)
      oldlast=first-1;
    else
      oldlast=first;
  } else
    oldlast=target->lastNr();

  toFetch=last-oldlast;
  //tqDebug("knode: last %d  oldlast %d  toFetch %d\n",last,oldlast,toFetch);

  if(toFetch<=0) {
    //tqDebug("knode: No new Articles in group\n");
    target->setLastNr(last);     // don't get stuck when the article numbers wrap
    return;
  }

  if(toFetch>target->maxFetch()) {
    toFetch=target->maxFetch();
    //tqDebug("knode: Fetching only %d articles\n",toFetch);
  }

  progressValue = 100;
  predictedLines = toFetch;

  // get list of additional headers provided by the XOVER command
  // see RFC 2980 section 2.1.7
  TQStrList headerformat;
  cmd = "LIST OVERVIEW.FMT";
  if ( sendCommand( cmd, rep ) && rep == 215 ) {
    TQStrList tmp;
    if (getMsg(tmp)) {
      for(TQCString s = tmp.first(); !s.isNull(); s = tmp.next()) {
        s = s.stripWhiteSpace();
        // remove the mandatory xover header
        if (s == "Subject:" || s == "From:" || s == "Date:" || s == "Message-ID:"
            || s == "References:" || s == "Bytes:" || s == "Lines:")
          continue;
        else
          headerformat.append(s);
      }
    }
  }

  //tqDebug("knode: KNNntpClient::doFetchNewHeaders() : xover %d-%d", last-toFetch+1, last);
  cmd.sprintf("xover %d-%d",last-toFetch+1,last);
  if (!sendCommand(cmd,rep))
    return;

  // no articles in selected range...
  if (rep==420) {         // 420 No article(s) selected
    target->setLastNr(last);
    return;
  } else if (rep!=224) {  // 224 success
    handleErrors();
    return;
  }

  TQStrList headers;
  if (!getMsg(headers)) {
    return;
  }

  progressValue = 1000;
  sendSignal(TSprogressUpdate);

  sendSignal(TSsortNew);

  mutex.lock();
  target->insortNewHeaders(&headers, &headerformat, this);
  target->setLastNr(last);
  mutex.unlock();
}


void KNNntpClient::doFetchArticle()
{
  KNRemoteArticle *target = static_cast<KNRemoteArticle*>(job->data());
  TQCString cmd;

  sendSignal(TSdownloadArticle);
  errorPrefix = i18n("Article could not be retrieved.\nThe following error occurred:\n");

  progressValue = 100;
  predictedLines = target->lines()->numberOfLines()+10;

  if (target->collection()) {
    TQString groupName = static_cast<KNGroup*>(target->collection())->groupname();
    if (currentGroup != groupName) {
      cmd="GROUP ";
      cmd+=groupName.utf8();
      if (!sendCommandWCheck(cmd,211))       // 211 n f l s group selected
        return;
      currentGroup = groupName;
    }
  }

  if (target->articleNumber() != -1) {
    cmd.setNum(target->articleNumber());
    cmd.prepend("ARTICLE ");
  } else {
    cmd = "ARTICLE " + target->messageID()->as7BitString(false);
  }

  if (!sendCommandWCheck(cmd,220)) {      // 220 n <a> article retrieved - head and body follow
    int code = atoi(getCurrentLine());
    if ( code == 430 || code == 423 ) { // 430 no such article found || 423 no such article number in this group
      TQString msgId = target->messageID()->as7BitString( false );
      // strip of '<' and '>'
      msgId = msgId.mid( 1, msgId.length() - 2 );
      job->setErrorString( errorPrefix + getCurrentLine() +
        i18n("<br><br>The article you requested is not available on your news server."
             "<br>You could try to get it from <a href=\"http://groups.google.com/groups?selm=%1\">groups.google.com</a>.")
          .arg( msgId ) );
    }
    return;
  }

  TQStrList msg;
  if (!getMsg(msg))
    return;

  progressValue = 1000;
  sendSignal(TSprogressUpdate);

  target->setContent(&msg);
  target->parse();
}


void KNNntpClient::doPostArticle()
{
  KNLocalArticle *art=static_cast<KNLocalArticle*>(job->data());

  sendSignal(TSsendArticle);

  if (art->messageID(false)!=0) {
    int rep;
    if (!sendCommand(TQCString("STAT ")+art->messageID(false)->as7BitString(false),rep))
      return;

    if (rep==223) {   // 223 n <a> article retrieved - request text separately
      #ifndef NDEBUG
      tqDebug("knode: STAT successful, we have probably already sent this article.");
      #endif
      return;       // the article is already on the server, lets put it silently into the send folder
    }
  }

  if(!sendCommandWCheck("POST", 340))       // 340 send article to be posted. End with <CR-LF>.<CR-LF>
    return;

  if (art->messageID(false)==0) {  // article has no message ID => search for a ID in the response
    TQCString s = getCurrentLine();
    int start = s.findRev(TQRegExp("<[^\\s]*@[^\\s]*>"));
    if (start != -1) {        // post response includes a recommended id
      int end = s.find('>',start);
      art->messageID()->from7BitString(s.mid(start,end-start+1));
      art->assemble();
      #ifndef NDEBUG
      tqDebug("knode: using the message-id recommended by the server: %s",s.mid(start,end-start+1).data());
      #endif
    }
  }

  if (!sendMsg(art->encodedContent(true)))
    return;

  if (!checkNextResponse(240))            // 240 article posted ok
    return;
}


void KNNntpClient::doFetchSource()
{
  KNRemoteArticle *target = static_cast<KNRemoteArticle*>(job->data());

  sendSignal(TSdownloadArticle);
  errorPrefix = i18n("Article could not be retrieved.\nThe following error occurred:\n");

  progressValue = 100;
  predictedLines = target->lines()->numberOfLines()+10;

  TQCString cmd = "ARTICLE " + target->messageID()->as7BitString(false);
  if (!sendCommandWCheck(cmd,220))      // 220 n <a> article retrieved - head and body follow
    return;

  TQStrList msg;
  if (!getMsg(msg))
    return;

  progressValue = 1000;
  sendSignal(TSprogressUpdate);

  target->setContent(&msg);
}


bool KNNntpClient::openConnection()
{
  currentGroup = TQString();

  TQString oldPrefix = errorPrefix;
  errorPrefix=i18n("Unable to connect.\nThe following error occurred:\n");

  if (!KNProtocolClient::openConnection())
    return false;

  progressValue = 30;

  int rep;
  if (!getNextResponse(rep))
    return false;

  if ( ( rep < 200 ) || ( rep > 299 ) ) { // RFC977: 2xx - Command ok
    handleErrors();
    return false;
  }

  progressValue = 50;

  if (!sendCommand("MODE READER",rep))
    return false;

  if (rep==500) {
#ifndef NDEBUG
    tqDebug("knode: \"MODE READER\" command not recognized.");
#endif
  } else
    if ( ( rep < 200 ) || ( rep > 299 ) ) { // RFC977: 2xx - Command ok
      handleErrors();
      return false;
    }

  progressValue = 60;

  // logon now, some newsserver send a incomplete group list otherwise
  if (account.needsLogon() && !account.user().isEmpty()) {
    //tqDebug("knode: user: %s",account.user().latin1());

    TQCString command = "AUTHINFO USER ";
    command += account.user().local8Bit();
    if (!KNProtocolClient::sendCommand(command,rep))
      return false;

    if (rep==381) {          // 381 PASS required
      //tqDebug("knode: Password required");

      if (!account.pass().length()) {
        job->setErrorString(i18n("Authentication failed.\nCheck your username and password."));
        job->setAuthError(true);
        return false;
      }

      //tqDebug("knode: pass: %s",account.pass().latin1());

      command = "AUTHINFO PASS ";
      command += account.pass().local8Bit();
      if (!KNProtocolClient::sendCommand(command,rep))
        return false;

      if (rep==281) {         // 281 authorization success
        #ifndef NDEBUG
        tqDebug("knode: Authorization successful");
        #endif
      } else {
        #ifndef NDEBUG
        tqDebug("knode: Authorization failed");
        #endif
        job->setErrorString(i18n("Authentication failed.\nCheck your username and password.\n\n%1").arg(getCurrentLine()));
        job->setAuthError(true);
        closeConnection();
        return false;
      }
    } else {
      if (rep==281) {         // 281 authorization success
        #ifndef NDEBUG
        tqDebug("knode: Authorization successful");
        #endif
      } else {
        if ((rep==482)||(rep==500)) {   //482 Authentication rejected
          #ifndef NDEBUG
          tqDebug("knode: Authorization failed");    // we don't care, the server can refuse the info
          #endif
        } else {
          handleErrors();
          return false;
        }
      }
    }
  }

  progressValue = 70;

  errorPrefix = oldPrefix;
  return true;
}


// authentication on demand
bool KNNntpClient::sendCommand(const TQCString &cmd, int &rep)
{
  if (!KNProtocolClient::sendCommand(cmd,rep))
    return false;

  if (rep==480) {            // 480 requesting authorization
    //tqDebug("knode: Authorization requested");

    if (!account.user().length()) {
      job->setErrorString(i18n("Authentication failed.\nCheck your username and password."));
      job->setAuthError(true);
      closeConnection();
      return false;
    }

    //tqDebug("knode: user: %s",account.user().data());

    TQCString command = "AUTHINFO USER ";
    command += account.user().local8Bit();
    if (!KNProtocolClient::sendCommand(command,rep))
      return false;

    if (rep==381) {          // 381 PASS required
      //tqDebug("knode: Password required");

      if (!account.pass().length()) {
        job->setErrorString(i18n("Authentication failed.\nCheck your username and password.\n\n%1").arg(getCurrentLine()));
        job->setAuthError(true);
        closeConnection();
        return false;
      }

      //tqDebug("knode: pass: %s",account.pass().data());

      command = "AUTHINFO PASS ";
      command += account.pass().local8Bit();
      if (!KNProtocolClient::sendCommand(command,rep))
        return false;
    }

    if (rep==281) {         // 281 authorization success
      #ifndef NDEBUG
      tqDebug("knode: Authorization successful");
      #endif
      if (!KNProtocolClient::sendCommand(cmd,rep))    // retry the original command
        return false;
    } else {
      job->setErrorString(i18n("Authentication failed.\nCheck your username and password.\n\n%1").arg(getCurrentLine()));
      job->setAuthError(true);
      closeConnection();
      return false;
    }
  }
  return true;
}


void KNNntpClient::handleErrors()
{
  if (errorPrefix.isEmpty())
    job->setErrorString(i18n("An error occurred:\n%1").arg(getCurrentLine()));
  else
    job->setErrorString(errorPrefix + getCurrentLine());

  int code = atoi(getCurrentLine());

  // close the connection only when necessary:
  // 430 no such article found
  // 411 no such news group
  // 423 no such article number in this group
  if ((code != 430)&&(code != 411)&&(code != 423))
    closeConnection();
}


//--------------------------------

