using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using MatterHackers.Agg.UI;
using MatterHackers.Localizations;
using MatterHackers.MatterControl;
using MatterHackers.MatterControl.SlicerConfiguration;
using MatterHackers.SerialPortCommunication.FrostedSerial;

namespace TcpipDriver
{
	class TcpipSerialPort : IFrostedSerialPort
	{
		// Telnet protocol characters
		const byte IAC = 255;  // escape
		const byte DONT = 254; // negotiation
		const byte DO = 253;// negotiation
		const byte WILL = 251;  // negotiation
		const byte SB = 250;  // subnegotiation begin
		const byte SE = 240;  // subnegotiation end
		const byte ComPortOpt = 44;  // COM port options
		const byte SetBaud = 1;  // Set baud rate
		const byte SetDataSize = 2; // Set data size
		const byte SetParity = 3;  // Set parity
		const byte SetControl = 5;  // Set control lines
		const byte DTR_ON = 8;  // used here to reset microcontroller
		const byte DTR_OFF = 9;
		const byte RTS_ON = 11;  // used here to signal ISP (in-system-programming) to uC
		const byte RTS_OFF = 12;

		private Socket socket;
		private NetworkStream stream;//Seems to have more in common with the socket so we will use to make this interface easier
		private IPAddress ipAddress;
		private int port;
		private IPEndPoint ipEndPoint;
		private byte[] readBuffer;
		private int bufferIndex;

		//These get set before open is called but the stream is not created until open is called. Preserver values to be set after stream is created.
		private int tempReadTimeout;
		private int tempWriteTimeout;

		private bool reconnecting = false;
		private PrinterSettings settings;

		public TcpipSerialPort(PrinterSettings settings, string name)
		{
			this.settings = settings;

			socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

			if (int.TryParse(settings.GetValue("ip_port"), out port)
				&& IPAddress.TryParse(settings.GetValue("ip_address"), out ipAddress))
			{
				ipEndPoint = new IPEndPoint(ipAddress, port);
				readBuffer = new byte[1024];
				bufferIndex = 0;
			}
			else
			{
				this.IsValid = false;
			}
		}

		public bool IsValid { get; } = true;

		public int BaudRate { get; set; }

		public int BytesToRead
		{
			get
			{
				if (stream.DataAvailable)
				{
					int bytesRead = stream.Read(readBuffer, bufferIndex, readBuffer.Length);
					bufferIndex += bytesRead;
				}

				return bufferIndex;
			}
		}

		public bool DtrEnable
		{
			get { return dtrEnable; }
			set
			{
				if (stream != null)
				{
					SetDtrEnable(value);
				}
				dtrEnable = value;
			}
		}
		private bool dtrEnable;

		// Eventually I will need to find out how to check that the port is open and connectable
		public bool IsOpen { get; } = true;

		public int ReadTimeout
		{
			get
			{
				return stream.ReadTimeout;
			}

			set
			{
				if (stream != null)
				{
					stream.ReadTimeout = value;
				}
				else
				{
					tempReadTimeout = value;
				}
			}
		}

		public bool RtsEnable { get; set; }

		public int WriteTimeout
		{
			get
			{
				return stream.WriteTimeout;
			}

			set
			{
				if (stream != null)
				{
					stream.WriteTimeout = value;
				}
				else
				{
					tempWriteTimeout = value;
				}
			}
		}

		public void Close()
		{
			socket.Close();
		}

		public void Dispose()
		{
			stream.Dispose();
		}

		public void Open()
		{
			this.LogInfo("Attempting to connect to: " + ipEndPoint.Address + " on port " + ipEndPoint.Port);

			// Connect with timeout
			int timeoutMs = 8000;
			long startedMs = UiThread.CurrentTimerMs;
			IAsyncResult result = socket.BeginConnect(ipEndPoint, null, null);
			result.AsyncWaitHandle.WaitOne(timeoutMs, true);

			if (socket.Connected)
			{
				socket.EndConnect(result);
			}
			else
			{
				socket.Close();

				long elapsedMs = UiThread.CurrentTimerMs - startedMs;
				if (elapsedMs >= timeoutMs)
				{
					throw new TimeoutException("Connection timed out".Localize());
				}
				else
				{
					throw new Exception("Failed to connect server".Localize());
				}
			}

			stream = new NetworkStream(socket)
			{
				WriteTimeout = tempWriteTimeout,
				ReadTimeout = tempReadTimeout
			};

			this.LogInfo("Connected to: " + ipEndPoint.Address + " on port " + ipEndPoint.Port);

			if (this.BaudRate != 0)
			{
				//Send Telnet handshake so that esp will enter the telnet mode allowing us to set baud and reset board
				byte[] bytes = new byte[] { IAC, WILL, ComPortOpt };
				Write(bytes, 0, bytes.Length);
				//Set baud and reset board
				SetBaudRate(this.BaudRate);
			}
		}

		private void LogInfo(string message)
		{
			ApplicationController.Instance.LogInfo(message);
		}

		public int Read(byte[] buffer, int offset, int count)
		{
			Array.Copy(readBuffer, offset, buffer, 0, count);
			Array.Clear(buffer, 0, count);
			bufferIndex -= count;
			Array.Copy(readBuffer, count, readBuffer, 0, bufferIndex);//THis may throw an exception as the target and source are the same

			return count;
		}

		public string ReadExisting()
		{
			string bufferAsString = ConvertBytesToString(readBuffer, bufferIndex);
			Array.Clear(readBuffer, 0, bufferIndex);
			bufferIndex = 0;
			return bufferAsString;
		}

		public void Write(string str)
		{
			var buffer = ConvertStringToBytes(str);
			Write(buffer, 0, buffer.Length);
		}

		public void Write(byte[] buffer, int offset, int count)
		{
			if (!reconnecting)
			{
				try
				{
					stream.Write(buffer, offset, count);
				}
				catch (Exception e)
				{
					this.LogInfo("Exception:" + e.Message);
					Reconnect();
					stream.Write(buffer, offset, count);
				}
			}
		}

		private static byte[] ConvertStringToBytes(string str)
		{
			byte[] bytes = new byte[str.Length];
			for (int i = 0; i < str.Length; i++)
			{
				bytes[i] = Convert.ToByte(str[i]);
			}

			return bytes;
		}

		private string ConvertBytesToString(byte[] inputBytes, int bytesRead)
		{
			var builder = new StringBuilder();

			for (int index = 0; index < bytesRead; index++)
			{
				builder.Append(Convert.ToChar(inputBytes[index]));
			}

			return builder.ToString();
		}

		private void Reconnect()
		{
			reconnecting = true;
			try
			{
				socket?.Close();
			}
			catch { }

			for (int i = 0; i < 5; i++)
			{
				ipEndPoint = new IPEndPoint(ipAddress, port);
				socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
				try
				{
					// Attempt to connect Message to just the console
					this.LogInfo("Attempting to connect to: " + ipEndPoint.Address + " on port " + ipEndPoint.Port);
					socket.Connect(ipEndPoint);
					stream = new NetworkStream(socket);
					this.LogInfo("Connected to: " + ipEndPoint.Address + " on port " + ipEndPoint.Port);

					// Send telnet handshake
					byte[] bytes = new byte[] { IAC, WILL, ComPortOpt };
					Write(bytes, 0, bytes.Length);
					break;
				}
				catch (Exception e)
				{
					ApplicationController.Instance.LogError("Exception:" + e.Message);
					Thread.Sleep((int)(500 * Math.Pow(i, 2)));
				}
			}

			reconnecting = false;
		}

		private void SetDtrEnable(bool dtr)
		{
			byte dtrEnabled = dtr ? DTR_ON : DTR_OFF;

			//Create Sequence of bytes that will cause board to be reset
			byte[] bytes = new byte[] { IAC, SB, ComPortOpt, SetControl, dtrEnabled, IAC, SE };

			Write(bytes, 0, bytes.Length);
		}

		private void SetBaudRate(int baudRate)
		{
			byte[] baudBytes = BitConverter.GetBytes(baudRate);
			if (BitConverter.IsLittleEndian)
			{
				Array.Reverse(baudBytes);
			}
			//Create Sequence of bytes that will set baudrate
			byte[] bytes = new byte[] { IAC, SB, ComPortOpt, SetBaud, baudBytes[0], baudBytes[1], baudBytes[2], baudBytes[3], IAC, SE };

			Write(bytes, 0, bytes.Length);
		}
	}
}
