/*****************************************************************************
 *
 * $Id$
 * Purpose ...............: Fidonet mailer
 *
 *****************************************************************************
 * Copyright (C) 1997-2004
 *   
 * Michiel Broek		FIDO:	2:280/2802
 * Beekmansbos 10
 * 1971 BV IJmuiden
 * the Netherlands
 *
 * This file is part of MBSE BBS.
 *
 * This BBS 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, or (at your option) any
 * later version.
 *
 * MBSE BBS 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 MBSE BBS; see the file COPYING.  If not, write to the Free
 * Software Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 *****************************************************************************/

#include "../config.h"
#include "../lib/mbselib.h"
#include "../lib/nodelist.h"
#include "session.h"
#include "ttyio.h"
#include "statetbl.h"
#include "config.h"
#include "lutil.h"
#include "openfile.h"
#include "m7recv.h"
#include "xmrecv.h"
#include "filelist.h"
#include "filetime.h"


#define XMBLKSIZ 128


static int	xm_recv(void);
static int	resync(off_t);
static int	closeit(int);

static char	*recvname=NULL;
static char	*fpath=NULL;
static FILE	*fp=NULL;
static int	last;
struct timeval	starttime, endtime;
struct timezone	tz;
static off_t	startofs;
static long	recv_blk;

extern unsigned long 	rcvdbytes;



int xmrecv(char *Name)
{
	int rc;

	Syslog('+', "Xmodem start receive \"%s\"",MBSE_SS(Name));
	recvname = Name;
	last = 0;
	rc = xm_recv();
	if (fp) 
		closeit(0);
	if (rc) 
		return -1;
	else 
		if (last) 
			return 1;
		else 
			return 0;
}



int closeit(int success)
{
	off_t	endofs;

	endofs = recv_blk*XMBLKSIZ;
	gettimeofday(&endtime, &tz);
	if (success)
	    Syslog('+', "Xmodem: OK %s", transfertime(starttime, endtime, endofs-startofs, FALSE));
	else
	    Syslog('+', "Xmodem: dropped after %ld bytes", endofs-startofs);
	rcvdbytes += (unsigned long)(endofs-startofs);
	fp = NULL;
	return closefile();
}



SM_DECL(xm_recv,(char *)"xmrecv")
SM_STATES
	sendnak0,
	waitblk0,
	sendnak,
	waitblk,
	recvblk,
	sendack,
	checktelink,
	recvm7,
	goteof
SM_NAMES
	(char *)"sendnak0",
	(char *)"waitblk0",
	(char *)"sendnak",
	(char *)"waitblk",
	(char *)"recvblk",
	(char *)"sendack",
	(char *)"checktelink",
	(char *)"recvm7",
	(char *)"goteof"
SM_EDECL

	int		tmp, i;
	int		SEAlink = FALSE, Slo = FALSE;
	int		crcmode = session_flags & FTSC_XMODEM_CRC;
	int		count=0,junk=0,cancount=0;
	int		header = 0;
	struct _xmblk {
		unsigned char	n1,n2;
		unsigned char	data[XMBLKSIZ];
		unsigned char	c1,c2;
	} xmblk;
	unsigned short	localcrc,remotecrc;
	unsigned char	localcs,remotecs;
	long		ackd_blk=-1L;
	long		next_blk=1L;
	long		last_blk=0L;
	off_t		resofs;
	char		tmpfname[16];
	off_t		wsize;
	time_t		remtime=0L;
	off_t		remsize=0;
	int		goteot = FALSE;

	gettimeofday(&starttime, &tz);
	recv_blk=-1L;

	memset(&tmpfname, 0, sizeof(tmpfname));
	if (recvname) 
		strncpy(tmpfname,recvname,sizeof(tmpfname)-1);

SM_START(sendnak0)

SM_STATE(sendnak0)

	Syslog('x', "xmrecv SENDNAK0 count=%d mode=%s", count, crcmode?"crc":"cksum");
	if (count++ > 9) {
		Syslog('+', "too many errors while xmodem receive init");
		SM_ERROR;
	}
	if ((ackd_blk < 0) && crcmode && (count > 5)) {
		Syslog('x', "no responce to 'C', try checksum mode");
		session_flags &= ~FTSC_XMODEM_CRC;
		crcmode = FALSE;
	}

	if (crcmode) 
		PUTCHAR('C');
	else 
		PUTCHAR(NAK);
	junk = 0;
	SM_PROCEED(waitblk0);

SM_STATE(waitblk0)

	header = GETCHAR(5);
	if (header == TIMEOUT) {
		Syslog('x', "timeout waiting for xmodem block 0 header, count=%d", count);
		if ((count > 2) && (session_flags & SESSION_IFNA)) {
			Syslog('+', "Timeout waiting for file in WaZOO session, report success");
			last=1;
			SM_SUCCESS;
		}
		SM_PROCEED(sendnak0);
	} else if (header < 0) {
		Syslog('x', "Error");
		SM_ERROR;
	} else {
		switch (header) {
		case EOT:	Syslog('x', "got EOT");
				Slo = FALSE;
				if (ackd_blk == -1L)
					last=1;
				else {
					ackd_blk++;
					PUTCHAR(ACK);
					if (SEAlink) {
						PUTCHAR(ackd_blk);
						PUTCHAR(~ackd_blk);
					}
				}
				if (STATUS) {
					SM_ERROR;
				}
				SM_SUCCESS;
				break;
		case CAN:	Syslog('+', "Got CAN while xmodem receive init");
				SM_ERROR;
				break;
		case SOH:	SEAlink = TRUE;
				Syslog('x', "Got SOH, SEAlink mode");
				SM_PROCEED(recvblk);
				break;
		case SYN:	SEAlink = FALSE;
				Syslog('x', "Got SYN, Telink mode");
				SM_PROCEED(recvblk);
				break;
		case ACK:	SEAlink = FALSE;
				Syslog('x', "Got ACK, Modem7 mode");
				SM_PROCEED(recvm7);
				break;
		case TSYNC:	Syslog('x', "Got TSYSNC char");
				SM_PROCEED(sendnak0);
				break;
		case NAK:
		case 'C':	Syslog('x', "Got %s waiting for block 0, sending EOT", printablec(header));
				PUTCHAR(EOT); /* other end still waiting us to send? */
				SM_PROCEED(waitblk0);
				break;
		default:	Syslog('x', "Got '%s' waiting for block 0", printablec(header));
				if (junk++ > 300) {
					SM_PROCEED(sendnak0);
				} else {
					SM_PROCEED(waitblk0);
				}
				break;
		}
	}

SM_STATE(sendnak)

	if (ackd_blk < 0) {
		SM_PROCEED(sendnak0);
	}

	if (count++ > 9) {
		Syslog('+', "too many errors while xmodem receive");
		SM_ERROR;
	}

	junk = 0;

	if (remote_flags & FTSC_XMODEM_RES) {
		if (resync(ackd_blk*XMBLKSIZ)) {
			SM_ERROR;
		} else {
			SM_PROCEED(waitblk);
		}
	} else { /* simple NAK */
		Syslog('x', "negative acknowlege block %ld",ackd_blk+1);

		if (crcmode)
			PUTCHAR('C');
		else
			PUTCHAR(NAK);
		if (SEAlink) {
			PUTCHAR(ackd_blk+1);
			PUTCHAR(~(ackd_blk+1));
		}
		if (STATUS) {
			SM_ERROR;
		} else {
			SM_PROCEED(waitblk);
		}
	}

SM_STATE(sendack)

	Syslog('x', "xmrecv SENDACK block=%d", recv_blk);
	ackd_blk = recv_blk;
	count = 0;
	cancount = 0;

	PUTCHAR(ACK);
	if (SEAlink) {
		PUTCHAR(ackd_blk);
		PUTCHAR(~ackd_blk);
	}
	if (STATUS) {
		SM_ERROR;
	}
	if (goteot) {
		SM_SUCCESS;
	}
	SM_PROCEED(waitblk);

SM_STATE(waitblk)

	header = GETCHAR(15);
	if (header == TIMEOUT) {
		Syslog('x', "timeout waiting for xmodem block header, count=%d", count);
		SM_PROCEED(sendnak);
	} else if (header < 0) {
		SM_ERROR;
	} else {
		switch (header) {
		case EOT:	if (last_blk && (ackd_blk != last_blk)) {
					Syslog('x', "false EOT after %ld block, need after %ld", ackd_blk,last_blk);
					SM_PROCEED(waitblk);
				} else {
					SM_PROCEED(goteof);
				}
				break;
		case CAN:	if (cancount++ > 4) {
					closeit(0);
					Syslog('+', "Got CAN while xmodem receive");
					SM_ERROR;
				} else {
					SM_PROCEED(waitblk);
				}
				break;
		case SOH:	SM_PROCEED(recvblk);
				break;
		default:	Syslog('x', "got '%s' waiting SOH", printablec(header));
				if (junk++ > 200) {
					SM_PROCEED(sendnak);
				} else {
					SM_PROCEED(waitblk);
				}
				break;
		}
	}

SM_STATE(recvblk)

	Nopper();
	GET((char*)&xmblk,(crcmode && (header != SYN))?  sizeof(xmblk): sizeof(xmblk)-1,15);
	if (STATUS == STAT_TIMEOUT) {
		Syslog('x', "xmrecv timeout waiting for block body");
		SM_PROCEED(sendnak);
	}
	if (STATUS) {
		SM_ERROR;
	}
	if ((xmblk.n1 & 0xff) != ((~xmblk.n2) & 0xff)) {
		Syslog('x', "bad block number: 0x%02x/0x%02x (0x%02x)", xmblk.n1,xmblk.n2,(~xmblk.n2)&0xff);
		SM_PROCEED(waitblk);
	}
	recv_blk = xmblk.n1 + (ackd_blk & ~0xff);
	if (abs(recv_blk - ackd_blk) > 128)
		recv_blk += 256;

	if (crcmode && (header != SYN)) {
		remotecrc = (short)xmblk.c1 << 8 | xmblk.c2;
		localcrc = crc16xmodem(xmblk.data, sizeof(xmblk.data));
		if (remotecrc != localcrc) {
			Syslog('x', "bad crc: 0x%04x/0x%04x",remotecrc,localcrc);
			if (recv_blk == (ackd_blk+1)) {
				SM_PROCEED(sendnak);
			} else {
				SM_PROCEED(waitblk);
			}
		}
	} else {
		remotecs = xmblk.c1;
		localcs = checksum(xmblk.data, sizeof(xmblk.data));
		if (remotecs != localcs) {
			Syslog('x', "bad checksum: 0x%02x/0x%02x",remotecs,localcs);
			if (recv_blk == (ackd_blk+1)) {
				SM_PROCEED(sendnak);
			} else {
				SM_PROCEED(waitblk);
			}
		}
	}
	if ((ackd_blk == -1L) && (recv_blk == 0L)) {
		SM_PROCEED(checktelink);
	}
	if ((ackd_blk == -1L) && (recv_blk == 1L)) {
		if (count < 3) {
			SM_PROCEED(sendnak0);
		} else 
			ackd_blk=0L;
	}
	if (recv_blk < (ackd_blk+1L)) {
		Syslog('x', "old block number %ld after %ld, go on", recv_blk,ackd_blk);
		SM_PROCEED(waitblk);
	} else if (recv_blk > (ackd_blk+1L)) {
		Syslog('x', "bad block order: %ld after %ld, go on", recv_blk,ackd_blk);
		SM_PROCEED(waitblk);
	}

	Syslog('X', "received block %ld \"%s\"", recv_blk,printable(xmblk.data,128));

	if (fp == NULL) {
		if ((fp = openfile(tmpfname,remtime,remsize,&resofs,resync)) == NULL) {
			SM_ERROR;
		} else {
			if (resofs) 
				ackd_blk=(resofs-1)/XMBLKSIZ+1L;
			else 
				ackd_blk=-1L;
		}
		startofs=resofs;
		Syslog('+', "Xmodem receive: \"%s\"",tmpfname);
	}
	
	if (recv_blk > next_blk) {
		WriteError("xmrecv internal error: recv_blk %ld > next_blk %ld", recv_blk,next_blk);
		SM_ERROR;
	}
	if (recv_blk == next_blk) {
		if (recv_blk == last_blk) 
			wsize=remsize%XMBLKSIZ;
		else 
			wsize=XMBLKSIZ;
		if (wsize == 0) 
			wsize=XMBLKSIZ;
		if ((tmp = fwrite(xmblk.data,wsize,1,fp)) != 1) {
			WriteError("$error writing block %l (%d bytes) to file \"%s\" (fwrite return %d)",
				recv_blk,wsize,fpath,tmp);
			SM_ERROR;
		} else 
			Syslog('x', "Block %ld size %d written (ret %d)", recv_blk,wsize,tmp);
		next_blk++;
	} else {
		Syslog('x', "recv_blk %ld < next_blk %ld, ack without writing", recv_blk,next_blk);
	}
	SM_PROCEED(sendack);

SM_STATE(checktelink)

	Syslog('x', "checktelink got \"%s\"",printable(xmblk.data,45));
	if (tmpfname[0] == '\0') {
		strncpy(tmpfname,xmblk.data+8,16);
		/*
		 *  Some systems fill the rest of the filename with spaces, sigh.
		 */
		for (i = 16; i; i--) {
			if ((tmpfname[i] == ' ') || (tmpfname[i] == '\0'))
				tmpfname[i] = '\0';
			else
				break;
		}
	} else {
		Syslog('+', "Remote uses %s",printable(xmblk.data+25,-14));
		Syslog('x', "Remote file name \"%s\" discarded", printable(xmblk.data+8,-16));
	}
	remsize = ((off_t)xmblk.data[0]) + ((off_t)xmblk.data[1]<<8) + ((off_t)xmblk.data[2]<<16) + ((off_t)xmblk.data[3]<<24);
	last_blk = (remsize-1)/XMBLKSIZ+1;
	if (header == SOH) {
		/*
		 * SEAlink block
		 */
		remtime=sl2mtime(((time_t)xmblk.data[4])+ ((time_t)xmblk.data[5]<<8)+
			((time_t)xmblk.data[6]<<16)+ ((time_t)xmblk.data[7]<<24));
		if (xmblk.data[40]) {
			Slo = TRUE;
			remote_flags |= FTSC_XMODEM_SLO;
		} else 
			remote_flags &= ~FTSC_XMODEM_SLO;
		if (xmblk.data[41]) 
			remote_flags |= FTSC_XMODEM_RES;
		else 
			remote_flags &= ~FTSC_XMODEM_RES;
		if (xmblk.data[42]) 
			remote_flags |= FTSC_XMODEM_XOF;
		else 
			remote_flags &= ~FTSC_XMODEM_XOF;
	} else if (header == SYN) {
		/*
		 * Telink block
		 */
		remtime=tl2mtime(((time_t)xmblk.data[4])+ ((time_t)xmblk.data[5]<<8)+
			((time_t)xmblk.data[6]<<16)+ ((time_t)xmblk.data[7]<<24));
		if (xmblk.data[41]) 
			session_flags |= FTSC_XMODEM_CRC;
		else 
			session_flags &= ~FTSC_XMODEM_CRC;
	} else {
		WriteError("Got data block with header 0x%02x", header);
		SM_PROCEED(sendnak0);
	}

	Syslog('x', "%s block, session_flags=0x%04x, remote_flags=0x%04x",
		(header == SYN)?"Telink":"Sealink",session_flags,remote_flags);

	if ((fp = openfile(tmpfname,remtime,remsize,&resofs,resync)) == NULL) {
		SM_ERROR;
	}
	if (resofs) 
		ackd_blk=(resofs-1)/XMBLKSIZ+1L;
	else 
		ackd_blk=-1L;
	startofs=resofs;

	Syslog('+', "Xmodem %s receive: \"%s\" %ld bytes dated %s", (header == SYN)?"Telink":"Sealink",
		tmpfname, remsize, rfcdate(remtime));

	if (ackd_blk == -1) {
		SM_PROCEED(sendack);
	} else {
		SM_PROCEED(waitblk);
	}

SM_STATE(recvm7)

	switch (m7recv(tmpfname)) {
		case 0:		ackd_blk=0; 
				SM_PROCEED(sendnak); 
				break;
		case 1:		last=1; 
				SM_SUCCESS; 
				break;
		default: 	SM_PROCEED(sendnak);
	}

SM_STATE(goteof)

	Slo = FALSE;
	closeit(1);
	if (ackd_blk == -1L) 
		last=1;
	else {
		ackd_blk++;
		PUTCHAR(ACK);
		if (SEAlink) {
			PUTCHAR(ackd_blk);
			PUTCHAR(~ackd_blk);
		}
	}
	if (STATUS) {
		SM_ERROR;
	}
	SM_SUCCESS;

SM_END
SM_RETURN



int resync(off_t resofs)
{
	char	resynbuf[16];
	short	lcrc;
	int	count=0;
	int	gotack,gotnak;
	int	c;
	long	sblk;

	Syslog('x', "trying to resync at offset %ld",resofs);

	sblk=resofs/XMBLKSIZ+1;
	sprintf(resynbuf,"%ld",sblk);
	lcrc=crc16xmodem(resynbuf,strlen(resynbuf));
	gotack=0;
	gotnak=0;

	do {
		count++;
		PUTCHAR(SYN);
		PUTSTR(resynbuf);
		PUTCHAR(ETX);
		PUTCHAR(lcrc&0xff);
		PUTCHAR(lcrc>>8);
		do {
			if ((c=GETCHAR(5)) == ACK) {
				if ((c=GETCHAR(1)) == SOH) 
					gotack=1;
				UNGETCHAR(c);
			} else if (c == NAK) {
				if ((c=GETCHAR(1)) == TIMEOUT) 
					gotnak=1;
				UNGETCHAR(c);
			}
		}
		while (!gotack && !gotnak && (c >= 0));
		if ((c < 0) && (c != TIMEOUT)) 
			return 1;
	}
	while (!gotack && !gotnak && (count < 6));

	if (gotack) {
		Syslog('x', "resyncing at offset %ld",resofs);
		return 0;
	} else {
		Syslog('+', "sealink resync at offset %ld failed",resofs);
		return 1;
	}
}