using System;
using System.IO;
using System.Text;

namespace libscomp
{
	public class Scomp
	{
		private StreamWriter w;
		private StreamReader r;
		private Encoding enc;
		private int seq;

		public enum ErrorCode
		{
				SCOMP_ERR_OK,		// no error
				SCOMP_ERR_TIMEOUT,	// operation timed out
				SCOMP_ERR_SND,		// send message failed
				SCOMP_ERR_RCV,		// receive message failed
				SCOMP_ERR_CRC,		// CRC mismatch
				SCOMP_ERR_SEQ,		// sequence number mismatch
				SCOMP_ERR_TYPE,		// message type mismatch
				SCOMP_ERR_RANGE,	// size exceeds max payload length
				SCOMP_ERR_BADNUM,	// numerical field encoding error
				SCOMP_ERR_OVER,		// data length exceeds buffer size
				SCOMP_ERR_BADOPT,	// unrecognized SComP option
		}


		public int Seq {
			get {
				return seq;
			}
			set {
				seq = value;
			}
		}

		public Scomp (Stream s) : this (new StreamReader(s), new StreamWriter(s))	{
			if (s.CanTimeout) s.WriteTimeout = 500;
		} 


		public Scomp (StreamReader r, StreamWriter w)
		{
			this.r = r;
			this.w = w;
			enc = new ASCIIEncoding();
			seq = 0;
		}

		private void incSeq ()
		{
			seq = (seq + 1)%ushort.MaxValue;
		}

		public ErrorCode Recv (out string data, out char type, out int seq, int timeout)
		{
			var hdr = new char[ 10 ];
			var crc32 = new Crc32();
			var crcbuf = new char [ 8 ];
			uint vcrc32;

			seq = 0;
			data = null;
			type = '?';
			if (r.BaseStream.CanTimeout) r.BaseStream.ReadTimeout = timeout;
			try {
				r.ReadBlock(hdr,0,hdr.Length);
			} catch (TimeoutException){
				return ErrorCode.SCOMP_ERR_TIMEOUT;
			} catch (IOException) {
				return ErrorCode.SCOMP_ERR_RCV;
			}

			seq = UInt16.Parse(new string (hdr,0,4),System.Globalization.NumberStyles.HexNumber);
			type = hdr[4];
			int dlen = UInt16.Parse(new string (hdr,5,4),System.Globalization.NumberStyles.HexNumber);


			if (dlen<ushort.MinValue || dlen>ushort.MaxValue) return ErrorCode.SCOMP_ERR_BADNUM;

			//Console.WriteLine("Type "+ type + " So many datas: "+dlen);
			char[] tmpdata = new char[dlen];

			try {
				r.ReadBlock(tmpdata,0,dlen);
			} catch (TimeoutException){
				return ErrorCode.SCOMP_ERR_TIMEOUT;
			}
			catch (IOException) {
				return ErrorCode.SCOMP_ERR_RCV;
			}

			data = new string(tmpdata);

			//Console.WriteLine("And the winner is: "+data);


			crc32.Init();
			crc32.Update(enc.GetBytes(hdr),0, hdr.Length);
			crc32.Update(enc.GetBytes(data.ToCharArray()),0, data.Length);
			crc32.Final();

			try {
				r.ReadBlock(crcbuf,0,8);
			} catch (TimeoutException){
				return ErrorCode.SCOMP_ERR_TIMEOUT;
			}
			catch (IOException) {
				return ErrorCode.SCOMP_ERR_RCV;
			}

			vcrc32 = UInt32.Parse(new string (crcbuf,0,8),System.Globalization.NumberStyles.HexNumber);

			//Console.WriteLine("Mine  : "+crc32.CrcValue.ToString("X8"));
			//Console.WriteLine("Theirs: "+vcrc32.ToString("X8") + "/" + new string(crcbuf,0,8));

			if (vcrc32 != crc32.CrcValue) return ErrorCode.SCOMP_ERR_CRC;


			return ErrorCode.SCOMP_ERR_OK;
		}

		public ErrorCode Send(string data, char type){
			var crc32 = new Crc32();
			var hdr = new char[ 10 ];

			//Beware: number of unicode chars!
			if (data.Length > UInt16.MaxValue) return ErrorCode.SCOMP_ERR_RANGE;


			seq.ToString("X4").ToCharArray().CopyTo(hdr,0);
			hdr[4] = type;
			data.Length.ToString("X4").ToCharArray().CopyTo(hdr,5);
			hdr[9] = '#';
			//%04X%c%04X#

			crc32.Init();
			crc32.Update(enc.GetBytes(hdr),0, hdr.Length);
			crc32.Update(enc.GetBytes(data.ToCharArray()),0, data.Length);
			crc32.Final();
			try {
				w.Write(hdr);
				w.Write(data);
				w.Write(crc32.CrcValue.ToString("X8"));
				//this can block (at least under Linux/Mono) if someone removed underlying character device
				w.Flush();
			} catch (IOException) {
				return ErrorCode.SCOMP_ERR_SND;
			}
			incSeq();
			return ErrorCode.SCOMP_ERR_OK;
		}

		public ErrorCode Exchange (string sdata, out string rdata, int timeout)
		{
			int sseq = seq;
			int rseq;
			ErrorCode err;
			char rtype;

			err = Send (sdata, 'Q');
			rdata = null;
			if (err != ErrorCode.SCOMP_ERR_OK) return err;

			err = Recv (out rdata, out rtype, out rseq, timeout);
			if (err != ErrorCode.SCOMP_ERR_OK) return err;

			if (rseq != sseq) return ErrorCode.SCOMP_ERR_SEQ;
			if (rtype != 'R') return ErrorCode.SCOMP_ERR_TYPE;

			return ErrorCode.SCOMP_ERR_OK;
		}
	}
}

