// This may look like C code, but it is really -*- C++ -*-
/*
 ************************************************************************
 *
 *			Verify integer and bit I/O
 *
 * Note, this code also tests reading and writing into extended
 * files (pipes), if EXT_OPEN is defined and set to 1.
 * Another defined variable, EXT_NETIO may be set to a 'hostname'.
 * In that case, this code tests opening "files" as
 *	tcp://hostname:7
 *	tcp://hostname:13
 *
 * $Id: vendian_io.cc,v 2.13 2005/06/26 21:46:10 oleg Exp oleg $
 *
 ************************************************************************
 */

#include "endian_io.h"
#include <iostream>
#if defined(WIN32)
#include <strstrea.h>
#else
#include <sstream>
#endif
#include <math.h>
#include <float.h>
#include <unistd.h>
#define MAXDOUBLE DBL_MAX
#define MINDOUBLE DBL_MIN

using std::cout;
using std::cerr;
#define strstream std::stringstream
using std::fstream;
using std::endl;
using std::ends;

#if defined(unix) || defined(macintosh)
#define TMP_FILE_NAME "/tmp/aa"
#else
#define TMP_FILE_NAME "vendian.tmp"
#endif

/*
 *------------------------------------------------------------------------
 *			Reading and checking functions
 */

static void read_and_check_long(EndianIn& i_stream, const unsigned long ethalon)
{
  const unsigned long read = i_stream.read_long("Reading a long integer");
  if( read != ethalon )
    _error("The read long int %d differs from what was expected %d",
	   read,ethalon);
}

static void read_and_check_short(EndianIn& i_stream, const unsigned short ethalon)
{
  const unsigned short read = i_stream.read_short("Reading a short integer");
  if( read != ethalon )
    _error("The read short int %d differs from what was expected %d",
	   read,ethalon);
}

static void read_and_check_byte(EndianIn& i_stream, const unsigned char ethalon)
{
  const unsigned char read = i_stream.read_byte("Reading a byte");
  if( read != ethalon )
    _error("The read byte %d differs from what was expected %d",
	   read,ethalon);
}

/*
 *------------------------------------------------------------------------
 *		Reading and writing ethalon patterns
 */

static const unsigned long MyPattern [] =
 { 1, (unsigned)-1, 0, 0xffff0000, 0x0000ffff, 0x5a5a5a5a, 0xa5a5a5a5 };

static void write_patterns(EndianOut& o_stream)
{
  {
    const unsigned long * p = (unsigned long *)MyPattern; 
    for(; (char *)p < (char *)MyPattern + sizeof(MyPattern); p++)
      o_stream.write_long(*p);
  }
  {
    const unsigned short * p = (unsigned short *)MyPattern; 
    for(; (char *)p < (char *)MyPattern + sizeof(MyPattern); p++)
      o_stream.write_short(*p);
  }
  {
    const unsigned char * p = (unsigned char *)MyPattern; 
    for(; (char *)p < (char *)MyPattern + sizeof(MyPattern); p++)
      o_stream.write_byte(*p);
  }
}

static void read_and_check_patterns(EndianIn& i_stream)
{
  {
    const unsigned long * p = (unsigned long *)MyPattern; 
    for(; (char *)p < (char *)MyPattern + sizeof(MyPattern); p++)
      read_and_check_long(i_stream,*p);
  }
  {
    const unsigned short * p = (unsigned short *)MyPattern; 
    for(; (char *)p < (char *)MyPattern + sizeof(MyPattern); p++)
      read_and_check_short(i_stream,*p);
  }
  {
    const unsigned char * p = (unsigned char *)MyPattern; 
    for(; (char *)p < (char *)MyPattern + sizeof(MyPattern); p++)
      read_and_check_byte(i_stream,*p);
  }
}

/*
 *------------------------------------------------------------------------
 *			Verify reading/writing integers in
 *		  littleendian mode (most significant byte last)
 */

static void test_littleendian()
{
  cout << "\n--> Test reading/writing integers in the littleendian mode\n";

  cout << "Opening the output stream to file " TMP_FILE_NAME << endl;
  EndianOut stream(TMP_FILE_NAME);
  stream.set_littlendian();

  cout << "Writing patterns\n";
  write_patterns(stream);
  stream.close();

  EndianIn istream;
#if defined(EXT_OPEN) && EXT_OPEN
  cout << "Opening the file as a reading stream through 'cat'" << endl;
  istream.open("cat " TMP_FILE_NAME " |");
#else
  cout << "Opening the file as a reading stream" << endl;
  istream.open(TMP_FILE_NAME);
#endif
  istream.set_littlendian();

  cout << "Reading what we've written back\n";
  read_and_check_patterns(istream);
  istream.close();

  cout << "\nDone\n";
}

/*
 *------------------------------------------------------------------------
 *			Verify reading/writing integers in
 *		  bigendian mode (most significant byte first)
 */

static void test_bigendian()
{
  cout << "\n--> Test reading/writing integers in the bigendian mode\n";

  {
#if defined(EXT_OPEN) && EXT_OPEN
    cout << "Opening the output stream to file " TMP_FILE_NAME " through 'cat'" << endl;
    EndianOut stream("| cat > " TMP_FILE_NAME);
#else
    cout << "Opening the output stream to file " TMP_FILE_NAME << endl;
    EndianOut stream(TMP_FILE_NAME);
#endif
    stream.set_bigendian();

    cout << "Writing patterns\n";
    write_patterns(stream);
				// Stream should be closed upon destruction
  }
#if defined(EXT_OPEN) && EXT_OPEN
  sleep(1);
#endif
  cout << "Opening the file as a reading stream straight\n";
  EndianIn istream;
  istream.open(TMP_FILE_NAME);
  istream.set_bigendian();

  cout << "Reading what we've written back\n";
  read_and_check_patterns(istream);
  istream.close();

  cout << "\nDone\n";
}

/*
 *------------------------------------------------------------------------
 *	Verify reading/writing floating-point numbers ('doubles')
 *	    			in both modes
 */

			// Reading and writing 'doubles' in a portable
			// way. See README for more discussion
static void write_double(EndianOut& out_stream, const double d)
{
  int exponent;
  const double mantissa = frexp(d,&exponent);
  double mantissa_part1;		// first 31 bits of mantissa
  const double mantissa_part2 = modf(ldexp(mantissa,31),
				     &mantissa_part1);
  out_stream.write_short(exponent);
  out_stream.write_long((long)mantissa_part1);
					// the last 31 bits of mantissa
  out_stream.write_long((long)ldexp(mantissa_part2,31));
}

static double read_double(EndianIn& in_stream)
{
  const int exponent =  (signed short)in_stream.read_short("reading exp");
  const long mantissa_part1 =  in_stream.read_long("reading the first 31 bits of mantissa and sign");
  const long mantissa_part2 =  in_stream.read_long("reading the last 31 bits of mantissa and sign");
  return ldexp(mantissa_part2,exponent-62) +
  	 ldexp(mantissa_part1,exponent-31);
}


static void read_and_check_double(EndianIn& i_stream, const double expected)
{
  const double read = read_double(i_stream);
  if( read != expected )
    _error("The read double %g differs from what was expected %g",
	   read,expected);
}


static void test_doubles()
{
  static const double patterns [] = {
    0.0, -1.0, 1.0/3.0, 2.2204460492503131E-16, 1+2.2204460492503131E-16,
    1e-23, -1e-23,
    MAXDOUBLE, -MAXDOUBLE, MINDOUBLE, -MINDOUBLE,
    3.14159265358979323846};
  
  const unsigned long delim = 0xdeadbeef;
  const unsigned long delim_rev = 0xefbeadde;
  
  cout << "\n--> Test reading/writing floating-point numbers\n";

  {
#if defined(EXT_OPEN) && EXT_OPEN
    cout << "Opening the output stream to file " TMP_FILE_NAME " through 'cat'" << endl;
    EndianOut stream("| cat > " TMP_FILE_NAME);
#else
    cout << "Opening the output stream to file " TMP_FILE_NAME << endl;
    EndianOut stream(TMP_FILE_NAME);
#endif

    cout << "Writing patterns in big-endian mode\n";
    stream.set_bigendian();
    for(register size_t i=0; i<sizeof(patterns)/sizeof(patterns[0]); i++)
      write_double(stream,patterns[i]);
  
    stream.write_long(delim);	// Just as a delimiter
    
    cout << "Adding patterns in little-endian mode\n";
    stream.set_littlendian();
    stream.write_long(delim);	// Just as a delimiter
    for(register size_t i=0; i<sizeof(patterns)/sizeof(patterns[0]); i++)
      write_double(stream,patterns[i]);
				// Stream should be closed upon destruction
  }
#if defined(EXT_OPEN) && EXT_OPEN
  sleep(1);
#endif
  {
    cout << "Reading the patterns from the file\n";
    EndianIn istream;
    istream.open(TMP_FILE_NAME);

    cout << "Reading patterns in big-endian mode\n";
    istream.set_bigendian();
    for(register size_t i=0; i<sizeof(patterns)/sizeof(patterns[0]); i++)
      read_and_check_double(istream,patterns[i]);

    cout << "Check delimiters\n";
    assert( istream.read_long("reading delim") == delim );
    assert( istream.read_long("reading delim") == delim_rev );
    
    cout << "Reading patterns in little-endian mode\n";
    istream.set_littlendian();
    for(register size_t i=0; i<sizeof(patterns)/sizeof(patterns[0]); i++)
      read_and_check_double(istream,patterns[i]);
    istream.close();
  }
  cout << "\nDone\n";
}

/*
 *------------------------------------------------------------------------
 *		Test the mixed int/bit stream I/O
 */

static void test_int_bit_IO()
{
  cout << "\n--> Test mixed int/bit stream I/O with file attachments\n";

#if defined(EXT_OPEN) && EXT_OPEN
  cout << "Opening the output stream to file " TMP_FILE_NAME " through 'cat'" << endl;
  EndianOut stream("| cat > " TMP_FILE_NAME);
#else
  cout << "Opening the output stream to file " TMP_FILE_NAME << endl;
  EndianOut stream(TMP_FILE_NAME);
#endif
  stream.set_bigendian();

  cout << "Writing integer patterns\n";
  write_patterns(stream);

  cout << "Attaching the bitstream\n";
  BitOut bitstream;
  bitstream.share_with(stream);

  cout << "Writing 8 bits of ones followed by 3*8 zero bits several times\n";
  register int i;
  for(i=0; i<5; i++)
  {
    register int i;
    for(i=0; i<8; i++)
      bitstream.put_bit(1);
    for(i=0; i<3*8; i++)
      bitstream.put_bit(0);
  }
  bitstream.put_bit(1);			// Put two extra bits
  bitstream.put_bit(1);
  bitstream.close();
  {
    cout << "Attaching the second bitstream\n";
    BitOut bitstream;
    bitstream.share_with(stream);

    cout << "Writing 8 bits of zeros followed by 3*8 one bits several times\n";
    register int i;
    for(i=0; i<5; i++)
    {
      register int i;
      for(i=0; i<8; i++)
	bitstream.put_bit(0);
      for(i=0; i<3*8; i++)
	bitstream.put_bit(1);
    }
    bitstream.put_bit(0);			// Put two extra bits
    bitstream.put_bit(1);
  }
  stream.close();
#if defined(EXT_OPEN) && EXT_OPEN
  sleep(1);
#endif

  cout << "Opening the file as a reading stream straight\n";
  EndianIn istream;
  istream.open(TMP_FILE_NAME);
  istream.set_bigendian();

  cout << "Reading what we've written back\n";
  read_and_check_patterns(istream);

  cout << "Attaching the input bitstream\n";
  BitIn ibitstream;
  ibitstream.share_with(istream);

#if defined(unix) || defined(B_BEOS_VERSION)
 system("ls -l " TMP_FILE_NAME "; od -x " TMP_FILE_NAME);
#endif 
  cout << "Reading the bit pattern\n";
  for(i=0; i<5; i++)
  {
    register int i;
    for(i=0; i<8; i++)
      assert( ibitstream.get_bit() == 1 );
    for(i=0; i<3*8; i++)
      assert( ibitstream.get_bit() == 0 );
  }
  assert( ibitstream.get_bit() == 1 );
  assert( ibitstream.get_bit() == 1 );
  ibitstream.close();
  {
    cout << "Attaching the second input bitstream\n";
    BitIn ibitstream;
    ibitstream.share_with(istream);

    cout << "Reading the bit pattern\n";
    for(i=0; i<5; i++)
    {
      register int i;
      for(i=0; i<8; i++)
	assert( ibitstream.get_bit() == 0 );
      for(i=0; i<3*8; i++)
	assert( ibitstream.get_bit() == 1 );
    }
    assert( ibitstream.get_bit() == 0 );
    assert( ibitstream.get_bit() == 1 );
  }
  cout << "\nDone\n";
}

/*
 *------------------------------------------------------------------------
 *		Test the mixed int/bit stream I/O
 */

static void test_varbit_int(void)
{
  cout << "\n--> Test reading/writing short ints using variable number of bits"
       << endl;
  
  const short ethalon [] =
  { 0, 1, 2, -1, -31, -17, 31, 15, -32, 63, 10000, 64+512, 64+511,
    -64-511, -4096, -3, -64-512-4095, 64+512+4096, 0, 1
  };
  
  {
    BitOut outbs;
    outbs.open(TMP_FILE_NAME);
    assert( outbs.good() );
    for(register unsigned int i=0; i<sizeof(ethalon)/sizeof(ethalon[0]); i++)
      outbs.put_short(ethalon[i]);
    outbs.put_bit(1);		// just for the heck of it
    outbs.put_bit(1);
    outbs.put_bit(1);
  }
    
  {
    BitIn inbs;
    inbs.open(TMP_FILE_NAME);
    assert( inbs.good() );
    for(register unsigned int i=0; i<sizeof(ethalon)/sizeof(ethalon[0]); i++)
    {
      short val_read = inbs.get_short();
      if( val_read != ethalon[i] )
        _error("%d-th read value %d does not match the ethalon %d",
               i,val_read,ethalon[i]);
    }
    assert( inbs.get_bit() == 1 );
    assert( inbs.get_bit() == 1 );
    assert( inbs.get_bit() == 1 );
    inbs.close();
  }
  
  cout << "\nDone\n";
}

static void test_tcp_as_file(const char * hostname)
{
  cout << "\n--> Test reading/writing 'files' which are actually TCP pipes"
       << endl;
  {
    strstream filename; filename << "tcp://" << hostname << ":13" << ends;
    cout << "\tReading from a datetime port: opening a FILE '"
	 << filename.str()
         << "'" << endl;
    FILE * fp = fopen(filename.str().c_str(),"r");
    if( fp == 0 )
      perror("Opening failed"), _error("Failure");
    cout << "\t\tthe result is: ";
    int c;
    while( (c = fgetc(fp)) != EOF )
      cout << (char)c;
    fclose(fp);
  }
#if !(defined(__GNUC__) && __GNUC__ >= 3)
  {
    strstream filename; filename << "tcp://" << hostname << ":7" << ends;
    cout << "\tEchoing: opening a fstream '" << filename.str()
         << "'" << endl;
    fstream fp(filename.str().c_str(),ios::in|ios::out);
    if( !fp.good() )
      perror("Opening failed"), _error("Failure");
    const char pattern [] = "1234567\r\n\007\r\n";
    char reply [sizeof(pattern)];
    cout <<"\t\tsending " << pattern << "..." << endl;
    fp << pattern << endl; fp.flush(); assert( fp.good() );
    cout << "\t\treceiving...";
    fp.read(reply,sizeof(reply)-1); reply[sizeof(reply)-1] = '\0';
    assert( fp.good() );
    cout << reply << endl;
    assert( strcmp(pattern,reply) == 0 );
    fp.close();
  }
#endif

  {
    const int input_port = 5000;
    const char test_string [] = "abc\n\n\tend";
    strstream filename; 
    filename << "ltcp://" << "0" << ":" << input_port << ends;
    cout << "\tListening on a port: opening a FILE '"
	 << filename.str()
         << "'" << endl;
    {				// Launch the program that will initiate
                                // the connection, with a delay
      pid_t pid;
      if((pid = fork()) != 0)
      {				// In the parent process
	if (pid < 0)
	  perror("Fork error"), _error("Failure");
      }
      else
      {
	strstream filename; 
	filename << "tcp://" << "127.0.0.1" << ":" << input_port << ends;
	sleep(1);
	cout << "In the child process: opening the file name '"
	     << filename.str() << "'" << endl;
	fstream fp(filename.str().c_str(),ios::out);
	fp << test_string;
	fp.close();
	exit(0);
      }
    }
    FILE * fp = fopen(filename.str().c_str(),"r");
    if( fp == 0 )
      perror("Opening failed"), _error("Failure");
    cout << "\t\tthe result is: ";
    int c;
    while( (c = fgetc(fp)) != EOF )
      cout << (char)c;
    fclose(fp);
  }

  cout << "\nDone\n";
}

static void test_bidirectional_pipe(void)
{
  cout << "\n--> Test a bidirectional pipe to a separate process "
       << endl;
  {
    const char filename [] = "| while read i; do echo $i >&2; echo $i; done";
    cout << "\tEchoing: opening a fstream '" << filename
         << "'" << endl;
    fstream fp(filename,ios::in|ios::out|ios::binary);
    fp.rdbuf()->pubsetbuf(0,0);
    if( !fp.good() )
      perror("Opening failed"), _error("Failure");
    const char pattern [] = "1234567\r\n\007\r\n";
    char reply [sizeof(pattern)];
    cout <<"\t\tsending " << pattern << "..." << endl;
    //fp.write(pattern,sizeof(reply)-1);
    fp << pattern << endl; fp.flush(); fp.sync(); assert( fp.good() );
    cout << "\t\treceiving...";
    fp.read(reply,sizeof(reply)-1); reply[sizeof(reply)-1] = '\0';
    assert( fp.good() );
    cout << reply << endl;
    assert( strcmp(pattern,reply) == 0 );
    fp.close();
  }

  cout << "\nDone\n";
}



/*
 *------------------------------------------------------------------------
 *			Root module
 */

int main()
{
  cout << "\n\n\t\tVerify integer stream I/O in big/little endian modes"
          "\n\t\t\t\tand bit stream I/O\n\n";
  test_littleendian();
  test_bigendian();
  test_doubles();
  test_int_bit_IO();
  test_varbit_int();

#if defined(EXT_OPEN) && EXT_OPEN && !(defined(__GNUC__) && __GNUC__ >= 3)
   test_bidirectional_pipe();
#endif

#if defined(EXT_NETIO)
#define TOSTR1(X) #X
#define TOSTR(X) TOSTR1(X)
  test_tcp_as_file(TOSTR(EXT_NETIO));
#endif
  cout << "\nAll tests passed" << endl;
  return 0;
}
