/////////////////////////////////////////////////////////////////
// NNTP65
// Network News Transport Protocol (NNTP) Client for IP65
// https://www.ietf.org/rfc/rfc3977.txt
// (Based on smtp65.c, which in turn is based on IP65's wget65.c)
// Bobbi September 2020
/////////////////////////////////////////////////////////////////

#include <cc65.h>
#include <errno.h>
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <apple2_filetype.h>

#include "../inc/ip65.h"
#include "w5100.h"

#include "email_common.h"

#define BELL      7
#define BACKSPACE 8

#define LOGFILE "NNTP65.LOG"

// Both pragmas are obligatory to have cc65 generate code
// suitable to access the W5100 auto-increment registers.
#pragma optimize      (on)
#pragma static-locals (on)

#define NETBUFSZ  1500+4       // 4 extra bytes for overlap between packets
#define LINEBUFSZ 2000 /*1000*/         // According to RFC2822 Section 2.1.1 (998+CRLF)
#define READSZ    1024         // Must be less than NETBUFSZ to fit in buf[]

static unsigned char buf[NETBUFSZ+1];    // One extra byte for null terminator
static char          linebuf_pad[1];     // One byte of padding make it easier
static char          linebuf[LINEBUFSZ];
static char          newsgroup[80];
static char          mailbox[80];

uint8_t  exec_email_on_exit = 0;
char     filename[80];
int      len;
FILE     *fp, *newsgroupsfp, *newnewsgroupsfp;
uint32_t filesize;
uint16_t nntp_port;

/*
 * Keypress before quit
 */
void confirm_exit(void) {
  fclose(fp);
  fclose(newsgroupsfp);
  fclose(newnewsgroupsfp);
  w5100_disconnect();
  printf("\n[Press Any Key]");
  cgetc();
  if (exec_email_on_exit) {
    sprintf(filename, "%s/EMAIL.SYSTEM", cfg_instdir);
    exec(filename, NULL);
  }
  exit(0);
}

/*
 * Called for all non IP65 errors
 */
void error_exit() {
  confirm_exit();
}

/*
 * Called if IP65 call fails
 */
void ip65_error_exit(void) {
  printf("%s\n", ip65_strerror(ip65_error));
  confirm_exit();
}

/*
 * Print message to the console, stripping extraneous CRLF stuff
 * from the end.
 */
void print_strip_crlf(char *s) {
  uint8_t i = 0;
  while ((s[i] != '\0') && (s[i] != '\r') && (s[i] != '\n'))
    putchar(s[i++]);
  putchar('\n');
}

/*
 * Spinner while downloading files
 */
void spinner(uint32_t sz, uint8_t final) {
  static char chars[] = "|/-\\";
  static char buf[10] = "";
  static uint8_t i = 0;
  uint8_t j;
  for (j = 0; j < strlen(buf); ++j)
    putchar(BACKSPACE);
  if (final) {
    sprintf(buf, " [%lu]\n", sz);
    printf("%s", buf);
    strcpy(buf, "");
  }
  else {
    sprintf(buf, "%c %lu", chars[(i++) % 4], sz);
    printf("%s", buf);
  }
}

#define DO_SEND   1  // For do_send param
#define DONT_SEND 0  // For do_send param
#define CMD_MODE  0  // For mode param
#define DATA_MODE 1  // For mode param

// Modified verson of w5100_http_open from w5100_http.c
// Sends a TCP message and receives a the first packet of the response.
// sendbuf is the buffer to send (null terminated)
// recvbuf is the buffer into which the received message will be written
// length is the length of recvbuf[]
// do_send Do the sending if true, otherwise skip
// mode Binary mode for received message, maybe first block of long message
bool w5100_tcp_send_recv(char* sendbuf, char* recvbuf, size_t length,
                         uint8_t do_send, uint8_t mode) {

  if (do_send == DO_SEND) {
    uint16_t snd;
    uint16_t pos = 0;
    uint16_t len = strlen(sendbuf);

    if (strncmp(sendbuf, "AUTHINFO PASS", 13) == 0)
      printf(">AUTHINFO PASS ****\n");
    else {
      putchar('>');
      print_strip_crlf(sendbuf);
    }

    while (len) {
      if (input_check_for_abort_key())
      {
        printf("User abort\n");
        w5100_disconnect();
        return false;
      }

      snd = w5100_send_request();
      if (!snd) {
        if (!w5100_connected()) {
          printf("Connection lost\n");
          return false;
        }
        continue;
      }

      if (len < snd)
        snd = len;

      {
        // One less to allow for faster pre-increment below
        const char *dataptr = sendbuf + pos - 1;
        uint16_t i;
        for (i = 0; i < snd; ++i) {
          // The variable is necessary to have cc65 generate code
          // suitable to access the W5100 auto-increment register.
          char data = *++dataptr;
          *w5100_data = data;
        }
      }

      w5100_send_commit(snd);
      len -= snd;
      pos += snd;
    }
  }

  if (mode == DATA_MODE) {
    //
    // Handle article body
    //
    uint16_t rcv, written;
    uint16_t len = 0;
    uint8_t cont = 1;

    // Backspace to put spinner on same line as RETR
    for (rcv = 0; rcv < 15; ++rcv)
      putchar(BACKSPACE);

    filesize = 0;

    // Initialize 4 byte overlap to zero
    bzero(recvbuf, 4);

    while (cont) {
      if (input_check_for_abort_key()) {
        printf("User abort\n");
        w5100_disconnect();
        return false;
      }
    
      rcv = w5100_receive_request();
      if (!rcv) {
        cont = w5100_connected();
        if (cont)
          continue;
      }

      if (rcv > length - len)
        rcv = length - len;

      {
        // One less to allow for faster pre-increment below
        // 4 bytes of overlap between blocks
        char *dataptr = recvbuf + len + 4 - 1;
        uint16_t i;
        for (i = 0; i < rcv; ++i) {
          // The variable is necessary to have cc65 generate code
          // suitable to access the W5100 auto-increment register.
          char data = *w5100_data;
          *++dataptr = data;
          if (!memcmp(dataptr - 4, "\r\n.\r\n", 5))
            cont = 0;
        }
      }
      w5100_receive_commit(rcv);
      len += rcv;

      // Skip 4 byte overlap
      written = fwrite(recvbuf + 4, 1, len, fp);
      if (written != len) {
        printf("Write error");
        return false;
      }

      // Copy 4 bytes of overlap
      memcpy(recvbuf, recvbuf + len, 4);

      filesize += len;
      spinner(filesize, 0);
      len = 0;
    }
  } else {
    //
    // Handle short single line ASCII text responses
    // Must fit in recvbuf[]
    //
    uint16_t rcv;
    uint16_t len = 0;
    uint8_t cont = 1;

    --length; // Leave space for NULL at end in case of buffer overrun

    while (cont) {
      if (input_check_for_abort_key()) {
        printf("User abort\n");
        w5100_disconnect();
        return false;
      }
    
      rcv = w5100_receive_request();
      if (!rcv) {
        cont = w5100_connected();
        if (cont)
          continue;
      }

      if ((length - len) == 0)
        cont = 0;

      if (rcv > length - len)
        rcv = length - len;

      {
        // One less to allow for faster pre-increment below
        // 4 bytes of overlap between blocks
        char *dataptr = recvbuf + len - 1;
        uint16_t i;
        for (i = 0; i < rcv; ++i) {
          // The variable is necessary to have cc65 generate code
          // suitable to access the W5100 auto-increment register.
          char data = *w5100_data;
          *++dataptr = data;
          if (!memcmp(dataptr - 1, "\r\n", 2))
            cont = 0;
        }
      }
      w5100_receive_commit(rcv);
      len += rcv;
    }
    recvbuf[len + 1] = '\0';
    putchar('<');
    print_strip_crlf(recvbuf);
  }
  return true;
}

/*
 * Check expected string from server
 */
uint8_t expect(char *buf, char *s) {
  if (strncmp(buf, s, strlen(s)) != 0) {
    printf("\nExpected '%s' got '%s'\n", s, buf);
    return 1;
  }
  return 0;
}

/*
 * Read parms from NEWS.CFG
 */
void readconfigfile(void) {
  char *colon;
  fp = fopen("NEWS.CFG", "r");
  if (!fp) {
    puts("Can't open config file NEWS.CFG");
    error_exit();
  }
  fscanf(fp, "%s", cfg_server);
  fscanf(fp, "%s", cfg_user);
  fscanf(fp, "%s", cfg_pass);
  fscanf(fp, "%s", cfg_instdir);
  fscanf(fp, "%s", cfg_emaildir);
  fclose(fp);

  colon = strchr(cfg_server, ':');
  if (!colon)
    nntp_port = 119;
  else {
    nntp_port = atoi(colon + 1);    
    *colon = '\0';
  }
}

// Entry on kill-list
struct killent {
  char *address;
  struct killent *next;
};

struct killent *killlist = NULL;

/*
 * Read email addresses from KILL.LIST.CFG
 * Returns 0 if kill-list exists, 1 if it does not
 */
uint8_t readkilllist(void) {
  struct killent *p, *q;
  char *s;
  fp = fopen("KILL.LIST.CFG", "r");
  if (!fp)
    return 1;
  killlist = p = malloc(sizeof(struct killent));
  killlist->address = NULL;
  killlist->next = NULL;
  while (!feof(fp)) {
    fscanf(fp, "%s", linebuf);
    s = malloc(strlen(linebuf) + 1);
    strcpy(s, linebuf);
    q = malloc(sizeof(struct killent));
    q->address = s;
    q->next = NULL;
    p->next = q;
    p = q;
  }
  fclose(fp);
  return 0;
}

/*
 * Read a text file a line at a time
 * Returns number of chars in the line, or 0 if EOF.
 * Expects CRLF line endings (CRLF) and converts to Apple II (CR) convention
 * fp - file to read from
 * writep - Pointer to buffer into which line will be written
 * n - length of buffer. Longer lines will be truncated and terminated with CR.
 */
uint16_t get_line(FILE *fp, char *writep, uint16_t n) {
  static uint16_t rd = 0; // Read
  static uint16_t end = 0; // End of valid data in buf
  uint16_t i = 0;
  // Special case, if fp is NULL then reset the state
  if (!fp) {
    rd = end = 0;
    return 0;
  }
  while (1) {
    if (rd == end) {
      end = fread(buf, 1, READSZ, fp);
      rd = 0;
    }
    if (end == 0)
      goto done;
    writep[i++] = buf[rd++];
    if (i == n - 1) {
      writep[i - 1] = '\r';
      goto done;
    }
    // The following line is safe because of linebuf_pad[]
    if ((writep[i - 1] == '\n') && (writep[i - 2] == '\r')) {
      writep[i - 1] = '\0'; // Remove LF
      return i - 1;
    }
  }
done:
  writep[i] = '\0';
  return i;
}

/*
 * Update EMAIL.DB - quick access database for header info
 */
void update_email_db(char *mbox, struct emailhdrs *h) {
  FILE *fp;
  sprintf(filename, "%s/%s/EMAIL.DB", cfg_emaildir, mbox);
  _filetype = PRODOS_T_BIN;
  _auxtype = 0;
  fp = fopen(filename, "ab");
  if (!fp) {
    printf("Can't open %s\n", filename);
    error_exit();
  }
  fwrite(h, sizeof(struct emailhdrs), 1, fp);
  fclose(fp);
}

/*
 * Copy one header from source->dest, removing '\r' from end
 */
void copyheader(char *dest, char *source, uint16_t len) {
  uint16_t i;
  memset(dest, ' ', len);
  for (i = 0; i < len; ++i) {
    if ((*source == '\0') || (*source == '\r'))
      return;
    *dest++ = *source++;
  }
}

/*
 * Write NEXT.EMAIL file with number of next EMAIL.n file to be created
 */
#if 0
void write_next_email(uint16_t num) {
  sprintf(filename, "%s/INBOX/NEXT.EMAIL", cfg_emaildir);
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  fp = fopen(filename, "wb");
  if (!fp) {
    printf("Can't open %s\n", filename);
    fclose(fp);
    error_exit();
  }
  fprintf(fp, "%u", num);
  fclose(fp);
}
#endif

/*
 * Check if a sender is on the kill-list.
 * sender - email address of sender
 * Returns 1 if on kill-list, false otherwise.
 */
uint8_t sender_is_on_killlist(char *sender) {
  struct killent *p = killlist;
  while (p && p->next) {
    if (strstr(sender, p->address))
      return 1;
    p = p->next;
  }
  return 0;
}

/*
 * Update mailbox
 * Copy messages from spool dir to mailbox and find headers of interest
 * (Date, From, Subject)
 */
void update_mailbox(char *mbox) {
  static struct emailhdrs hdrs;
  struct dirent *d;
  uint16_t msg, chars, headerchars;
  uint8_t headers, onkilllist;
  FILE *destfp;
  DIR *dp;
  sprintf(filename, "%s/NEWS.SPOOL", cfg_emaildir);
  dp = opendir(filename);
  while (d = readdir(dp)) {
    strcpy(linebuf, "");
    sprintf(filename, "%s/NEWS.SPOOL/%s", cfg_emaildir, d->d_name);
    get_line(NULL, NULL, 0); // Reset get_line() state
    fp = fopen(filename, "r");
    if (!fp) {
      printf("Can't open %s\n", filename);
      closedir(dp);
      error_exit();
    }
    hdrs.emailnum = msg = atoi(&(d->d_name[5]));
    sprintf(filename, "%s/%s/EMAIL.%u", cfg_emaildir, mbox, msg);
    fputs(filename, stdout);
    _filetype = PRODOS_T_TXT;
    _auxtype = 0;
    destfp = fopen(filename, "wb");
    if (!destfp) {
      printf("Can't open %s\n", filename);
      closedir(dp);
      error_exit();
    }
    onkilllist = 0;
    headers = 1;
    headerchars = 0;
    hdrs.skipbytes = 0; // Just in case it doesn't get set
    hdrs.status = 'N';
    hdrs.tag = ' ';
    hdrs.date[0] = hdrs.from[0] = hdrs.cc[0] = hdrs.subject[0] = '\0';
    // Store News:newsgroup in TO field
    strcpy(linebuf, "News:");
    strcat(linebuf, newsgroup);
    copyheader(hdrs.to, linebuf, 79);
    while ((chars = get_line(fp, linebuf, LINEBUFSZ)) != 0) {
      if (headers) {
        headerchars += chars;
        if (!strncmp(linebuf, "Date: ", 6)) {
          copyheader(hdrs.date, linebuf + 6, 39);
          hdrs.date[39] = '\0';
        }
        if (!strncmp(linebuf, "From: ", 6)) {
          copyheader(hdrs.from, linebuf + 6, 79);
          hdrs.from[79] = '\0';
          if (sender_is_on_killlist(hdrs.from)) {
            fputs(" - KILLED!", stdout);
            onkilllist = 1;
            break;
          }
        }
        // Store Organization in CC field
        if (!strncmp(linebuf, "Organization: ", 14)) {
          copyheader(hdrs.cc, linebuf + 14, 79);
          hdrs.cc[79] = '\0';
        }
        if (!strncmp(linebuf, "Subject: ", 9)) {
          copyheader(hdrs.subject, linebuf + 9, 79);
          hdrs.subject[79] = '\0';
        }
        if (linebuf[0] == '\r') {
          headers = 0;
          hdrs.skipbytes = headerchars;
        }
      }
      fputs(linebuf, destfp);
    }
    fclose(fp);
    fclose(destfp);
    if (onkilllist == 1)
      unlink(filename);
    else
      update_email_db(mbox, &hdrs);
    puts("");

    //sprintf(filename, "%s/NEWS.SPOOL/NEWS.%u", cfg_emaildir, msg);
    sprintf(filename, "%s/NEWS.SPOOL/%s", cfg_emaildir, d->d_name);
    if (unlink(filename))
      printf("Can't delete %s\n", filename);
  }
  closedir(dp);
}

void main(int argc, char *argv[]) {
  uint32_t nummsgs, lownum, highnum, msgnum, msg;
  uint16_t msgcount, i;
  char sendbuf[80];
  FILE *logfp;
  uint8_t eth_init = ETH_INIT_DEFAULT;

  if ((argc == 2) && (strcmp(argv[1], "EMAIL") == 0))
    exec_email_on_exit = 1;

  linebuf_pad[0] = 0;

  videomode(VIDEOMODE_80COL);
  printf("%c%s NNTP - Receive News Articles%c\n", 0x0f, PROGNAME, 0x0e);

  printf("\nReading NEWS.CFG             -");
  readconfigfile();
  printf(" Ok");
  
  printf("\nReading KILL.LIST.CFG        -");
  if (readkilllist() == 0)
    printf(" Ok");
  else
    printf(" None");
  
  printf("\nReading NEWSGROUPS.CFG       - ");
  sprintf(filename, "%s/NEWSGROUPS.CFG", cfg_emaildir);
  newsgroupsfp = fopen(filename, "r");
  if (!newsgroupsfp) {
    printf("\nCan't read %s\n", filename);
    error_exit();
  }

  printf("Ok\nCreating NEWSGROUPS.NEW      - ");
  sprintf(filename, "%s/NEWSGROUPS.NEW", cfg_emaildir);
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  newnewsgroupsfp = fopen(filename, "wb");
  if (!newnewsgroupsfp) {
    printf("\nCan't open %s\n", filename);
    error_exit();
  }

  {
    int file;
    printf("Ok\nSetting slot                 - ");
    file = open("ethernet.slot", O_RDONLY);
    if (file != -1) {
      read(file, &eth_init, 1);
      close(file);
      eth_init &= ~'0';
    }
  }

  printf("%d\nInitializing %s     - ", eth_init, eth_name);
  if (ip65_init(eth_init)) {
    ip65_error_exit();
  }

  // Abort on Ctrl-C to be consistent with Linenoise
  abort_key = 0x83;

  printf("Ok\nObtaining IP address         - ");
  if (dhcp_init()) {
    ip65_error_exit();
  }

  // Copy IP config from IP65 to W5100
  w5100_init(eth_init);
  w5100_config();

  printf("Ok\nConnecting to %s (%u) - ", cfg_server, nntp_port);

  if (!w5100_connect_addr(parse_dotted_quad(cfg_server), nntp_port)) {
    printf("Fail\n");
    error_exit();
  }

  printf("Ok\n\n");

  if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DONT_SEND, CMD_MODE)) {
    error_exit();
  }
  if (expect(buf, "20")) // "200" if posting is allowed / "201" if no posting
    error_exit();

  // Skip authentication?
  if (strcmp(cfg_user, "-") != 0) {
    sprintf(sendbuf, "AUTHINFO USER %s\r\n", cfg_user);
    if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
      error_exit();
    }
    if (expect(buf, "381")) // Username accepted
      error_exit();

    sprintf(sendbuf, "AUTHINFO PASS %s\r\n", cfg_pass);
    if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
      error_exit();
    }
    if (expect(buf, "281")) // Authentication successful
      error_exit();
  }

  // Make empty log file
  _filetype = PRODOS_T_TXT;
  _auxtype = 0;
  logfp = fopen(LOGFILE, "w");
  fclose(logfp);

  while (1) {
    msg = fscanf(newsgroupsfp, "%s %s %ld", newsgroup, mailbox, &msgnum);
    if (strcmp(newsgroup, "0") == 0)
      break;
    if ((msg == 0) || (msg == EOF))
      break;
    printf("*************************************************************\n");
    printf("* NEWSGROUP: %s\n", newsgroup);
    printf("* MAILBOX:   %s\n", mailbox);
    printf("* START MSG: %ld\n", msgnum);
    printf("*************************************************************\n");

    sprintf(sendbuf, "GROUP %s\r\n", newsgroup);
    if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
      error_exit();
    }
    if (strncmp(buf, "411", 3) == 0) {
      putchar(BELL);
      printf("** Non-existent newsgroup %s\n", newsgroup);
      continue;
    }

    sscanf(buf, "211 %lu %lu %lu", &nummsgs, &lownum, &highnum);
    printf(" Approx. %lu messages, numbered from %lu to %lu\n", nummsgs, lownum, highnum);

    if (msgnum == 0)
      msgnum = highnum - 100; // If 0 is specified grab 100 messages to start

    if (msgnum < lownum)
      msgnum = lownum;

    for (msg = msgnum; msg <= highnum; ++msg) {
      sprintf(sendbuf, "STAT %ld\r\n", msgnum);
      if (!w5100_tcp_send_recv(sendbuf, buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
        error_exit();
      }
      if (strncmp(buf, "220", 3)) // Message number exists
        break;
    }

    msgcount = 0;
    while (1) {
      if (!w5100_tcp_send_recv("NEXT\r\n", buf, NETBUFSZ, DO_SEND, CMD_MODE)) {
        error_exit();
      }
      if (strncmp(buf, "223", 3) != 0)
        break; // No more messages in group
      sscanf(buf, "223 %ld", &msg);
      sprintf(filename, "%s/NEWS.SPOOL/NEWS.%lu", cfg_emaildir, msg);
      _filetype = PRODOS_T_TXT;
      _auxtype = 0;
      fp = fopen(filename, "wb"); 
      if (!fp) {
        printf("Can't create %s\n", filename);
        error_exit();
      }
      printf("\n** Retrieving article %lu/%lu from %s\n", msg, highnum, newsgroup);
      if (!w5100_tcp_send_recv("ARTICLE\r\n", buf, NETBUFSZ, DO_SEND, DATA_MODE)) {
        error_exit();
      }
      spinner(filesize, 1); // Cleanup spinner
      fclose(fp);
      ++msgcount;
    }
    printf("Updating NEWSGROUPS.NEW (%s:%ld) ...\n", newsgroup, msg);
    fprintf(newnewsgroupsfp, "%s %s %ld\n", newsgroup, mailbox, msg);
    printf("Updating mailbox %s ...\n", mailbox);
    update_mailbox(mailbox);

    _filetype = PRODOS_T_TXT;
    _auxtype = 0;
    logfp = fopen(LOGFILE, "a");
    if (logfp) {
      fprintf(logfp, "  %s", newsgroup);
      for (i = 0; i < (30 - strlen(newsgroup)); ++i)
      	fputc(' ', logfp);
      fprintf(logfp, "%5u messages retrieved to mailbox %s\n", msgcount, mailbox);
      fclose(logfp);
    }
  }

  fclose(newsgroupsfp);
  fclose(newnewsgroupsfp);

  sprintf(filename, "%s/NEWSGROUPS.CFG", cfg_emaildir);
  if (unlink(filename)) {
    printf("Can't delete %s\n", filename);
    error_exit();
  }
  sprintf(linebuf, "%s/NEWSGROUPS.NEW", cfg_emaildir);
  if (rename(linebuf, filename)) {
    printf("Can't rename %s to %s\n", linebuf, filename);
    error_exit();
  }

  // Ignore any error - can be a race condition where other side
  // disconnects too fast and we get an error
  w5100_tcp_send_recv("QUIT\r\n", buf, NETBUFSZ, DO_SEND, CMD_MODE);

  printf("Disconnecting\n");
  w5100_disconnect();

  logfp = fopen(LOGFILE, "r");
  if (logfp) {
    puts("\nNNTP65 Session Summary:\n");
    i = fgetc(logfp);
    while (!feof(logfp)) {
      putchar(i);
      i = fgetc(logfp);
    }
    fclose(logfp);
    puts("");
  }

  confirm_exit();
}
