/*
Copyright (c) 2014, Kevin Pope
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using MatterHackers.Agg.Platform;
using Microsoft.Win32;

namespace MatterHackers.SerialPortCommunication.FrostedSerial
{
	[MonitoringDescription("")]
	[System.ComponentModel.DesignerCategory("")]
	public class FrostedSerialPort : Component, IFrostedSerialPort
	{
		public const int InfiniteTimeout = -1;
		private const int DefaultReadBufferSize = 4096;
		private const int DefaultWriteBufferSize = 2048;
		private const int DefaultBaudRate = 9600;
		private const int DefaultDataBits = 8;
		private const Parity DefaultParity = Parity.None;
		private const StopBits DefaultStopBits = StopBits.One;

		private bool is_open;
		private int baud_rate;
		private Parity parity;
		private StopBits stop_bits;
		private Handshake handshake;
		private int data_bits;
		private bool break_state = false;
		private bool dtr_enable = false;
		private bool rts_enable = false;
		private IFrostedSerialStream stream;
		private Encoding encoding = Encoding.ASCII;
		private string new_line = Environment.NewLine;
		private string port_name;
		private int read_timeout = InfiniteTimeout;
		private int write_timeout = InfiniteTimeout;
		private int readBufferSize = DefaultReadBufferSize;
		private int writeBufferSize = DefaultWriteBufferSize;
		private object error_received = new object();
		private object data_received = new object();
		private object pin_changed = new object();

		public FrostedSerialPort() :
			this(FrostedSerialPort.GetDefaultPortName(), DefaultBaudRate, DefaultParity, DefaultDataBits, DefaultStopBits)
		{
		}

		public FrostedSerialPort(IContainer container)
			: this()
		{
			// TODO: What to do here?
		}

		public FrostedSerialPort(string portName) :
			this(portName, DefaultBaudRate, DefaultParity, DefaultDataBits, DefaultStopBits)
		{
		}

		public FrostedSerialPort(string portName, int baudRate) :
			this(portName, baudRate, DefaultParity, DefaultDataBits, DefaultStopBits)
		{
		}

		public FrostedSerialPort(string portName, int baudRate, Parity parity) :
			this(portName, baudRate, parity, DefaultDataBits, DefaultStopBits)
		{
		}

		public FrostedSerialPort(string portName, int baudRate, Parity parity, int dataBits) :
			this(portName, baudRate, parity, dataBits, DefaultStopBits)
		{
		}

		public FrostedSerialPort(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits)
		{
			port_name = portName;
			baud_rate = baudRate;
			data_bits = dataBits;
			stop_bits = stopBits;
			this.parity = parity;
		}

		public static bool MockPortsForTest { get; set; } = false;

#if DEBUG
		public static bool AllowEmulator { get; set; } = true;
#else
		public static bool AllowEmulator { get; set; } = false;
#endif

		// On non-Android platforms simply return true as port access validation isn't applicable
		public static bool EnsureDeviceAccess()
		{
			return true;
		}

		private static Regex linuxDefaultUIFilter = new Regex("/dev/ttyS*\\d+", RegexOptions.CultureInvariant | RegexOptions.Compiled);

		private static IEnumerable<string> FilterPortsForMac(List<string> allPorts)
		{
			IEnumerable<string> filteredPorts;

			if (AggContext.OperatingSystem == OSType.X11)
			{
				// A default and naive filter that works well on Ubuntu 14
				filteredPorts = allPorts.Where(portName => portName != "/dev/tty" && !linuxDefaultUIFilter.Match(portName).Success);
			}
			else
			{
				// looks_like_mac -- serialPort.StartsWith("/dev/tty."); looks_like_pc -- serialPort.StartsWith("COM")
				filteredPorts = allPorts.Where(portName => portName.StartsWith("/dev/tty.") 
					|| portName.StartsWith("COM")
					|| portName == "Emulator");
			}

			return filteredPorts.Any() ? filteredPorts : allPorts;
		}

		public static string[] GetPortNames(bool filter = true)
		{
			var p = Environment.OSVersion.Platform;
			var serial_ports = new List<string>();

			// Are we on Unix?
			if (MockPortsForTest)
			{
				serial_ports.Add("COM-TestA");
				serial_ports.Add("COM-TestB");
				serial_ports.Add("COM-TestC");
				serial_ports.Add("COM-Test0");
				serial_ports.Add("COM-Test1");
			}
			else if (p == PlatformID.Unix || p == PlatformID.MacOSX)
			{
				string[] ttys = Directory.GetFiles("/dev/", "tty*");

				// If filtering was not requested, return the raw listing of /dev/tty* - (subsequent filtering happens in client code)
				if (!filter)
				{
					return ttys;
				}

				// Probe for Linux-styled devices: /dev/ttyS* or /dev/ttyUSB*
				foreach (string dev in ttys)
				{
					if (dev != "/dev/tty" && dev.StartsWith("/dev/tty") && !dev.StartsWith("/dev/ttyC"))
					{
						serial_ports.Add(dev);
					}
				}
			}
			else
			{
				using (RegistryKey subkey = Registry.LocalMachine.OpenSubKey("HARDWARE\\DEVICEMAP\\SERIALCOMM"))
				{
					if (subkey != null)
					{
						string[] names = subkey.GetValueNames();
						foreach (string value in names)
						{
							string port = subkey.GetValue(value, "").ToString();
							if (port != "")
								serial_ports.Add(port);
						}
					}
				}
			}

			if (AllowEmulator)
			{
				serial_ports.Add("Emulator");
			}

			return FilterPortsForMac(serial_ports).ToArray();
		}

		public static string GetDefaultPortName()
		{
			string[] ports = FrostedSerialPort.GetPortNames();
			if (ports.Length > 0)
			{
				return ports[0];
			}
			else
			{
				int p = (int)Environment.OSVersion.Platform;
				if (p == 4 || p == 128 || p == 6)
					return "ttyS0"; // Default for Unix
				else
					return "COM1"; // Default for Windows
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		public Stream BaseStream
		{
			get
			{
				CheckOpen();
				return (Stream)stream;
			}
		}

		[DefaultValueAttribute(DefaultBaudRate)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public int BaudRate
		{
			get
			{
				return baud_rate;
			}
			set
			{
				if (value <= 0)
					throw new ArgumentOutOfRangeException("value");

				if (is_open)
					stream.SetAttributes(value, parity, data_bits, stop_bits, handshake);

				baud_rate = value;
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		public bool BreakState
		{
			get
			{
				return break_state;
			}
			set
			{
				CheckOpen();
				if (value == break_state)
					return; // Do nothing.

				stream.SetBreakState(value);
				break_state = value;
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		public int BytesToRead
		{
			get
			{
				CheckOpen();
				return stream.BytesToRead;
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		public int BytesToWrite
		{
			get
			{
				CheckOpen();
				return stream.BytesToWrite;
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		public bool CDHolding
		{
			get
			{
				CheckOpen();
				return (stream.GetSignals() & SerialSignal.Cd) != 0;
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		public bool CtsHolding
		{
			get
			{
				CheckOpen();
				return (stream.GetSignals() & SerialSignal.Cts) != 0;
			}
		}

		[DefaultValueAttribute(DefaultDataBits)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public int DataBits
		{
			get
			{
				return data_bits;
			}
			set
			{
				if (value < 5 || value > 8)
					throw new ArgumentOutOfRangeException("value");

				if (is_open)
					stream.SetAttributes(baud_rate, parity, value, stop_bits, handshake);

				data_bits = value;
			}
		}

		//[MonoTODO("Not implemented")]
		[Browsable(true)]
		[MonitoringDescription("")]
		[DefaultValue(false)]
		public bool DiscardNull
		{
			get
			{
				throw new NotImplementedException();
			}
			set
			{
				// LAMESPEC: Msdn states that an InvalidOperationException exception
				// is fired if the port is not open, which is *not* happening.

				throw new NotImplementedException();
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		public bool DsrHolding
		{
			get
			{
				CheckOpen();
				return (stream.GetSignals() & SerialSignal.Dsr) != 0;
			}
		}

		[DefaultValueAttribute(false)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public bool DtrEnable
		{
			get
			{
				return dtr_enable;
			}
			set
			{
				if (value == dtr_enable)
					return;
				if (is_open)
					stream.SetSignal(SerialSignal.Dtr, value);

				dtr_enable = value;
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
		[MonitoringDescription("")]
		public Encoding Encoding
		{
			get
			{
				return encoding;
			}
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");

				encoding = value;
			}
		}

		[DefaultValueAttribute(Handshake.None)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public Handshake Handshake
		{
			get
			{
				return handshake;
			}
			set
			{
				if (value < Handshake.None || value > Handshake.RequestToSendXOnXOff)
					throw new ArgumentOutOfRangeException("value");

				if (is_open)
					stream.SetAttributes(baud_rate, parity, data_bits, stop_bits, value);

				handshake = value;
			}
		}

		[Browsable(false)]
		public bool IsOpen
		{
			get
			{
				return is_open;
			}
		}

		[DefaultValueAttribute("\n")]
		[Browsable(false)]
		[MonitoringDescription("")]
		public string NewLine
		{
			get
			{
				return new_line;
			}
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				if (value.Length == 0)
					throw new ArgumentException("NewLine cannot be null or empty.", "value");

				new_line = value;
			}
		}

		[DefaultValueAttribute(DefaultParity)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public Parity Parity
		{
			get
			{
				return parity;
			}
			set
			{
				if (value < Parity.None || value > Parity.Space)
					throw new ArgumentOutOfRangeException("value");

				if (is_open)
					stream.SetAttributes(baud_rate, value, data_bits, stop_bits, handshake);

				parity = value;
			}
		}

		//[MonoTODO("Not implemented")]
		[Browsable(true)]
		[MonitoringDescription("")]
		[DefaultValue(63)]
		public byte ParityReplace
		{
			get
			{
				throw new NotImplementedException();
			}
			set
			{
				throw new NotImplementedException();
			}
		}

		[Browsable(true)]
		[MonitoringDescription("")]
		[DefaultValue("COM1")] // silly Windows-ism. We should ignore it.
		public string PortName
		{
			get
			{
				return port_name;
			}
			set
			{
				if (is_open)
					throw new InvalidOperationException("Port name cannot be set while port is open.");
				if (value == null)
					throw new ArgumentNullException("value");
				if (value.Length == 0 || value.StartsWith("\\\\"))
					throw new ArgumentException("value");

				port_name = value;
			}
		}

		[DefaultValueAttribute(DefaultReadBufferSize)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public int ReadBufferSize
		{
			get
			{
				return readBufferSize;
			}
			set
			{
				if (is_open)
					throw new InvalidOperationException();
				if (value <= 0)
					throw new ArgumentOutOfRangeException("value");
				if (value <= DefaultReadBufferSize)
					return;

				readBufferSize = value;
			}
		}

		[DefaultValueAttribute(InfiniteTimeout)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public int ReadTimeout
		{
			get
			{
				return read_timeout;
			}
			set
			{
				if (value < 0 && value != InfiniteTimeout)
					throw new ArgumentOutOfRangeException("value");

				if (is_open)
					stream.ReadTimeout = value;

				read_timeout = value;
			}
		}

		//[MonoTODO("Not implemented")]
		[DefaultValueAttribute(1)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public int ReceivedBytesThreshold
		{
			get
			{
				throw new NotImplementedException();
			}
			set
			{
				if (value <= 0)
					throw new ArgumentOutOfRangeException("value");

				throw new NotImplementedException();
			}
		}

		[DefaultValueAttribute(false)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public bool RtsEnable
		{
			get
			{
				return rts_enable;
			}
			set
			{
				if (value == rts_enable)
					return;
				if (is_open)
					stream.SetSignal(SerialSignal.Rts, value);

				rts_enable = value;
			}
		}

		[DefaultValueAttribute(DefaultStopBits)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public StopBits StopBits
		{
			get
			{
				return stop_bits;
			}
			set
			{
				if (value < StopBits.One || value > StopBits.OnePointFive)
					throw new ArgumentOutOfRangeException("value");

				if (is_open)
					stream.SetAttributes(baud_rate, parity, data_bits, value, handshake);

				stop_bits = value;
			}
		}

		[DefaultValueAttribute(DefaultWriteBufferSize)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public int WriteBufferSize
		{
			get
			{
				return writeBufferSize;
			}
			set
			{
				if (is_open)
					throw new InvalidOperationException();
				if (value <= 0)
					throw new ArgumentOutOfRangeException("value");
				if (value <= DefaultWriteBufferSize)
					return;

				writeBufferSize = value;
			}
		}

		[DefaultValueAttribute(InfiniteTimeout)]
		[Browsable(true)]
		[MonitoringDescription("")]
		public int WriteTimeout
		{
			get
			{
				return write_timeout;
			}
			set
			{
				if (value < 0 && value != InfiniteTimeout)
					throw new ArgumentOutOfRangeException("value");

				if (is_open)
					stream.WriteTimeout = value;

				write_timeout = value;
			}
		}

		// methods

		public void Close()
		{
			Dispose(true);
		}

		protected override void Dispose(bool disposing)
		{
			if (!is_open)
				return;

			is_open = false;
			// Do not close the base stream when the finalizer is run; the managed code can still hold a reference to it.
			if (disposing)
				stream.Close();
			stream = null;
		}

		public void DiscardInBuffer()
		{
			CheckOpen();
			stream.DiscardInBuffer();
		}

		public void DiscardOutBuffer()
		{
			CheckOpen();
			stream.DiscardOutBuffer();
		}

		public void Open()
		{
			if (is_open)
				throw new InvalidOperationException("Port is already open");

			stream = new FrostedSerialPortStream(port_name, baud_rate, data_bits, parity, stop_bits, dtr_enable,
				rts_enable, handshake, read_timeout, write_timeout, readBufferSize, writeBufferSize);

			is_open = true;
		}

		public int Read(byte[] buffer, int offset, int count)
		{
			CheckOpen();
			if (buffer == null)
				throw new ArgumentNullException("buffer");
			if (offset < 0 || count < 0)
				throw new ArgumentOutOfRangeException("offset or count less than zero.");

			if (buffer.Length - offset < count)
				throw new ArgumentException("offset+count",
								  "The size of the buffer is less than offset + count.");

			return stream.Read(buffer, offset, count);
		}

		public int Read(char[] buffer, int offset, int count)
		{
			CheckOpen();
			if (buffer == null)
				throw new ArgumentNullException("buffer");
			if (offset < 0 || count < 0)
				throw new ArgumentOutOfRangeException("offset or count less than zero.");

			if (buffer.Length - offset < count)
				throw new ArgumentException("offset+count",
								  "The size of the buffer is less than offset + count.");

			int c, i;
			for (i = 0; i < count && (c = ReadChar()) != -1; i++)
				buffer[offset + i] = (char)c;

			return i;
		}

		internal int read_byte()
		{
			byte[] buff = new byte[1];
			if (stream.Read(buff, 0, 1) > 0)
				return buff[0];

			return -1;
		}

		public int ReadByte()
		{
			CheckOpen();
			return read_byte();
		}

		public int ReadChar()
		{
			CheckOpen();

			byte[] buffer = new byte[16];
			int i = 0;

			do
			{
				int b = read_byte();
				if (b == -1)
					return -1;
				buffer[i++] = (byte)b;
				char[] c = encoding.GetChars(buffer, 0, 1);
				if (c.Length > 0)
					return (int)c[0];
			} while (i < buffer.Length);

			return -1;
		}

		public string ReadExisting()
		{
			CheckOpen();

			int count = BytesToRead;
			byte[] bytes = new byte[count];

			int n = stream.Read(bytes, 0, count);
			return new String(encoding.GetChars(bytes, 0, n));
		}

		public string ReadLine()
		{
			return ReadTo(new_line);
		}

		public string ReadTo(string value)
		{
			CheckOpen();
			if (value == null)
				throw new ArgumentNullException("value");
			if (value.Length == 0)
				throw new ArgumentException("value");

			// Turn into byte array, so we can compare
			byte[] byte_value = encoding.GetBytes(value);
			int current = 0;
			List<byte> seen = new List<byte>();

			while (true)
			{
				int n = read_byte();
				if (n == -1)
					break;
				seen.Add((byte)n);
				if (n == byte_value[current])
				{
					current++;
					if (current == byte_value.Length)
						return encoding.GetString(seen.ToArray(), 0, seen.Count - byte_value.Length);
				}
				else
				{
					current = (byte_value[0] == n) ? 1 : 0;
				}
			}
			return encoding.GetString(seen.ToArray());
		}

		public void Write(string str)
		{
			CheckOpen();
			if (str == null)
				throw new ArgumentNullException("str");

			byte[] buffer = encoding.GetBytes(str);
			Write(buffer, 0, buffer.Length);
		}

		public void Write(byte[] buffer, int offset, int count)
		{
			CheckOpen();
			if (buffer == null)
				throw new ArgumentNullException("buffer");

			if (offset < 0 || count < 0)
				throw new ArgumentOutOfRangeException();

			if (buffer.Length - offset < count)
				throw new ArgumentException("offset+count",
								 "The size of the buffer is less than offset + count.");

			stream.Write(buffer, offset, count);
		}

		public void Write(char[] buffer, int offset, int count)
		{
			CheckOpen();
			if (buffer == null)
				throw new ArgumentNullException("buffer");

			if (offset < 0 || count < 0)
				throw new ArgumentOutOfRangeException();

			if (buffer.Length - offset < count)
				throw new ArgumentException("offset+count",
								 "The size of the buffer is less than offset + count.");

			byte[] bytes = encoding.GetBytes(buffer, offset, count);
			stream.Write(bytes, 0, bytes.Length);
		}

		public void WriteLine(string str)
		{
			Write(str + new_line);
		}

		private void CheckOpen()
		{
			if (!is_open)
				throw new InvalidOperationException("Specified port is not open.");
		}

		internal void OnErrorReceived(SerialErrorReceivedEventArgs args)
		{
			SerialErrorReceivedEventHandler handler =
				(SerialErrorReceivedEventHandler)Events[error_received];

			if (handler != null)
				handler(this, args);
		}

		internal void OnDataReceived(SerialDataReceivedEventArgs args)
		{
			SerialDataReceivedEventHandler handler =
				(SerialDataReceivedEventHandler)Events[data_received];

			if (handler != null)
				handler(this, args);
		}

		internal void OnDataReceived(SerialPinChangedEventArgs args)
		{
			SerialPinChangedEventHandler handler =
				(SerialPinChangedEventHandler)Events[pin_changed];

			if (handler != null)
				handler(this, args);
		}

		// events
		[MonitoringDescription("")]
		public event SerialErrorReceivedEventHandler ErrorReceived
		{
			add { Events.AddHandler(error_received, value); }
			remove { Events.RemoveHandler(error_received, value); }
		}

		[MonitoringDescription("")]
		public event SerialPinChangedEventHandler PinChanged
		{
			add { Events.AddHandler(pin_changed, value); }
			remove { Events.RemoveHandler(pin_changed, value); }
		}

		[MonitoringDescription("")]
		public event SerialDataReceivedEventHandler DataReceived
		{
			add { Events.AddHandler(data_received, value); }
			remove { Events.RemoveHandler(data_received, value); }
		}
	}
}
