/*
    ANY-LICENSE V1.0
    ----------------

    You can use these files under any license approved by the
    Open Source Initiative, preferrably one of the popular licenses,
    as long as the license you choose is compatible to the
    dependencies of these files.

    See http://www.opensource.org/licenses/ for a list of
    approved licenses.

    Author:   Martin Furter <mf@borg.ch>
    Project:  borgnet, borg.ch C# classes and examples.
    Modified: 2019


    A serial port class which works reliable on Windows and Linux.

	The knowledge how to do it right can be found here:
	http://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport
	http://www.sparxeng.com/wp-content/uploads/2014/05/If-you-must-use-net-systems-2.jpg

*/


using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Ports;
using BorgCh.Comm;

namespace BorgCh.Comm
{

/**
 * A serial port class which works reliable on Windows and Linux.
 */
public class SerPort
{
	/**
	 * The callback to receive data from the serial port.
	 */
	public delegate void ReadData( byte[] data );

	/// The standard serial port.
	private SerialPort serial;
	/// When true the port is open.
	private bool running;
	/// When true the port is closed.
	private bool stopped;
	/// The buffer for incoming data.
	private byte[] rd_buf;
	/// 
	private Action kickoff_read;
	/// The callback to deliver received data.
	private ReadData serialDataReceived;

	/**
	 * Creates a new SerPort and opens it.
	 * Portspec format: portname-baudrate-databits-parity-stopbits
	 *
	 * @param port_spec	Port specification string as returned by ToString().
	 * @param cb		The read callback.
	 */
	public SerPort( string port_spec, ReadData cb=null )
	{
		string port_name;
		int baud_rate;
		Parity parity;
		int data_bits;
		StopBits stop_bits;

		string[] parts = port_spec.Split( new char[]{ '-' } );
		if( parts.Length != 5 )
		{
			throw new ArgumentException(
				"The port description must have 5 fields separated by '-'.",
				"port_spec" );
		}
		// part 0: port name
		port_name = parts[0];
		// part 1: baud rate
		if( !Int32.TryParse( parts[1], out baud_rate ) )
		{
			throw new ArgumentException( "The baud rate must be an integer.",
				"baud_rate" );
		}
		// part 2: number of data bits
		if( !Int32.TryParse( parts[2], out data_bits ) )
		{
			throw new ArgumentException( "The number of data bits must be an integer." );
		}
		if( data_bits < 5 || data_bits > 8 )
		{
			throw new ArgumentOutOfRangeException( "data_bits",
					"The number of data bits must be between 5 and 8." );
		}
		// part 3: parity
		if( parts[3] == "N" )
		{
			parity = Parity.None;
		}
		else if( parts[3] == "E" )
		{
			parity = Parity.Even;
		}
		else if( parts[3] == "O" )
		{
			parity = Parity.Odd;
		}
		else if( parts[3] == "M" )
		{
			parity = Parity.Mark;
		}
		else if( parts[3] == "S" )
		{
			parity = Parity.Space;
		}
		else
		{
			throw new ArgumentException(
				"The parity must be one of letters 'NEOMS'.", "parity" );
		}
		// part 4: number of stop bits
		if( parts[4] == "1" )
		{
			stop_bits = StopBits.One;
		}
		else if( parts[4] == "1.5" )
		{
			stop_bits = StopBits.OnePointFive;
		}
		else if( parts[4] == "2" )
		{
			stop_bits = StopBits.Two;
		}
		else
		{
			throw new ArgumentException(
				"The number of stop bits must be '1', '1.5' or '2'.",
				"stop_bits" );
		}
		initialize( port_name, baud_rate, parity, data_bits, stop_bits, cb );
	}

	/**
	 * Creates a new SerPort and opens it.
	 *
	 * @param port_name		Name of the serial port.
	 * @param baud_rate		The baud rate.
	 * @param parity		The parity.
	 * @param data_bits		The number of data bits per word.
	 * @param stop_bits		The number of stop bits.
	 * @param cb			The read callback.
	 */
	public SerPort( string port_name, int baud_rate, Parity parity,
				int data_bits, StopBits stop_bits, ReadData cb=null )
	{
		initialize( port_name, baud_rate, parity, data_bits, stop_bits, cb );
	}

	/**
	 * Creates a new SerPort and opens it.
	 *
	 * @param port_name		Name of the serial port.
	 * @param baud_rate		The baud rate.
	 * @param parity		The parity.
	 * @param data_bits		The number of data bits per word.
	 * @param stop_bits		The number of stop bits.
	 * @param cb			The read callback.
	 */
	 //[MemberNotNull( nameof(serial), nameof(rd_buf), nameof(kickoff_read), nameof(serialDataReceived) )]
	private void initialize( string port_name, int baud_rate, Parity parity,
				int data_bits, StopBits stop_bits, ReadData cb )
	{
		serial = new SerialPort( port_name, baud_rate, parity,
					data_bits, stop_bits );
		serial.Handshake = Handshake.None;
		serial.RtsEnable = false;
		serial.ReadTimeout = 100;
		if( serial.IsOpen )
		{
			Console.WriteLine( "Serial port " + port_name +
						" seems to be already open!" );
		}
		running = false;
		stopped = true;
		rd_buf = new byte[256];
		set_read_callback( cb );
		kickoff_read = delegate {
			serial.BaseStream.BeginRead( rd_buf, 0, rd_buf.Length,
				delegate( IAsyncResult ar )
				{
					try
					{
						int actualLength = serial.BaseStream.EndRead( ar );
						byte[] received = new byte[actualLength];
						Buffer.BlockCopy( rd_buf, 0, received, 0,
								actualLength );
						serialDataReceived( received );
					}
					catch( IOException exc )
					{
						Console.WriteLine( "IOException:" );
						Console.WriteLine( exc.Message );
					}
					// catch( Exception exc )
					catch( Exception )
					{
						/* * /
						Console.WriteLine( "Exception: {0}", exc.Message );
						/ * */
						// just assume it is a timeout
						if( !running )
						{
							stopped = true;
						}
					}
					if( running )
					{
						// Console.WriteLine( "kickoff_read again..." );
						if( kickoff_read != null )
						{
							kickoff_read();
						}
					}
				},
				null );
		};
	}

	/**
	 * A callback which just ignores the data, used as default callback.
	 */
	private void ignore_callback( byte[] data )
	{
	}

	/**
	 * Sets the read callback.
	 */
	 //[MemberNotNull( nameof(serialDataReceived) )]
	public void set_read_callback( ReadData cb )
	{
		if( cb == null )
		{
			cb = ignore_callback;
		}
		serialDataReceived = cb;
	}

	/**
	 * Opens the serial port.
	 */
	public void open()
	{
		if( running )
		{
			return;
		}
		try
		{
			serial.Open();
		}
		catch( Exception )
		{
			Console.WriteLine( serial.PortName );
			throw;
		}
		running = true;
		stopped = false;
		kickoff_read();
	}

	/**
	 * Closes the serial port.
	 */
	public void close()
	{
		int counter = 25;

		if( !running )
		{
			return;
		}
		/*
		running = false;
		// Console.WriteLine( "running = false" );
		while( counter > 0 && !stopped )
		{
			counter--;
			System.Threading.Thread.Sleep( 100 );
		}
		// Console.WriteLine( "close" );
		serial.Close();
		*/
		// not running anymore
		running = false;
		// close the serial port, this will also end the BeginRead()
		// (actually it should terminate the async read, but it does not on linux)
		serial.Close();
		// wait util it's finished
		System.Threading.Thread.Sleep( 10 );
		while( counter > 0 && !stopped )
		{
			// Console.WriteLine( "{0}...", counter );
			counter--;
			System.Threading.Thread.Sleep( 100 );
		}
	}

	/**
	 * Returns true when the port is open.
	 */
	public bool is_open()
	{
		return running;
	}

	/**
	 * Writes the string @c s to the serial port.
	 */
	public void write( string s )
	{
		write( System.Text.Encoding.ASCII.GetBytes( s ) );
	}

	/**
	 * Writes the bytes in @c wr_buf to the serial port.
	 */
	public void write( byte[] wr_buf )
	{
		// on Linux WriteAsync does not write until something is received
		// serial.BaseStream.WriteAsync( wr_buf, 0, wr_buf.Length );
		serial.BaseStream.Write( wr_buf, 0, wr_buf.Length );
	}

	/**
	 * Writes @c length bytes from @c offset to the serial port.
	 */
	public void write( byte[] wr_buf, int offset, int length )
	{
		// on Linux WriteAsync does not write until something is received
		// serial.BaseStream.WriteAsync( wr_buf, 0, wr_buf.Length );
		serial.BaseStream.Write( wr_buf, offset, length );
	}

	/**
	 * Returns the duration of sending/receiving @n data words.
	 *
	 * @n:		The number of data words to calculate the duration for.
	 * @return:	The duration in seconds.
	 */
	public double get_word_duration( int n=1 )
	{
		// start bit + stop bit + number of data bits
		int bits_per_word = 2 + serial.DataBits;
		// if there is a parity add one bit
		if( serial.Parity != Parity.None )
		{
			bits_per_word++;
		}
		// if there are 1.5 or 2 stop bits add one bit
		if( serial.StopBits != StopBits.One )
		{
			bits_per_word++;
		}
		double duration = bits_per_word * n;
		duration /= serial.BaudRate;
		return duration;
	}

	/**
	 * The baud rate.
	 */
	public int BaudRate
	{
		get {
			return serial.BaudRate;
		}
		set {
			serial.BaudRate = value;
		}
	}

	/**
	 * The number of data bits.
	 */
	public int DataBits
	{
		get {
			return serial.DataBits;
		}
		set {
			serial.DataBits = value;
		}
	}

	/**
	 * The parity as defined in System.IO.Ports.Parity
	 */
	public Parity Parity
	{
		get {
			return serial.Parity;
		}
		set {
			serial.Parity = value;
		}
	}

	/**
	 * The number of stop bits as defined in System.IO.Ports.StopBits
	 */
	public StopBits StopBits
	{
		get {
			return serial.StopBits;
		}
		set {
			serial.StopBits = value;
		}
	}

	/**
	 * Returns a string describing the configuration of the serial port.
	 *
	 * The returned string consists of 5 substrings separated by '-'.
	 * The five parts are port name, baud rate, number of data bits,
	 * parity and number of stop bits in this order.
	 *
	 * The parity take on the values 'E', 'O', 'M', 'S' or 'N'.
	 *
	 * The number of stop bits can be '1', '1.5' or '2'.
	 */
	public override String ToString()
	{
		String parity;
		String stopbits;
		switch( serial.Parity )
		{
			case Parity.Even:	parity = "E"; break;
			case Parity.Odd:	parity = "O"; break;
			case Parity.Mark:	parity = "M"; break;
			case Parity.Space:	parity = "S"; break;
			default:			parity = "N"; break;
		}
		switch( serial.StopBits )
		{
			case StopBits.One:			stopbits = "1"; break;
			case StopBits.OnePointFive:	stopbits = "1.5"; break;
			case StopBits.Two:			stopbits = "2"; break;
			default:					stopbits = "1"; break;
		}
		return String.Format( "{0}-{1}-{2}-{3}-{4}",
				serial.PortName,
				serial.BaudRate,
				serial.DataBits,
				parity,
				stopbits );
	}
}

} // namespace BorgCh.Comm