using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using skyscraper5.Skyscraper;
using skyscraper5.Skyscraper.IO.CrazycatStreamReader;
using skyscraper8.SatIp.RtspRequests;
using skyscraper8.SatIp.RtspResponses;

namespace skyscraper8.SatIp
{
	internal class RtspClient : Validatable, IDisposable
	{
		private uint cseqCounter;
		private const string USER_AGENT = "sophiaNetRtspClient/1.0";

		public RtspClient(string ip, int port)
		{
			IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
			ConstructStep2(ipEndPoint);
		}

		public RtspClient(IPAddress ip, int port)
		{
			IPEndPoint ipEndPoint = new IPEndPoint(ip, port);
			ConstructStep2(ipEndPoint);
		}

		private void ConstructStep2(IPEndPoint ipEndPoint)
		{
			this.TcpClient = new TcpClient();
			this.TcpClient.Connect(ipEndPoint);
			this.RootPath = string.Format("rtsp://{0}:{1}", ipEndPoint.Address.ToString(), ipEndPoint.Port.ToString());
			this.NetworkStream = TcpClient.GetStream();
			this.BufferedStream = new BufferedStream(this.NetworkStream);
			this.StreamReader = new StreamReader(this.BufferedStream);
			this.StreamWriter = new StreamWriter(this.BufferedStream);
			this.cseqCounter = 2;
			this.ListenIp = GetListenIp(this.TcpClient.Client.LocalEndPoint);
			RtspOptionsResponse rtspOptionsResponse = GetOptions("/");
			this.Valid = rtspOptionsResponse.Valid;
		}

		public bool AutoReconnect { get; set; }
		public IPAddress ListenIp { get; set; }

		private IPAddress GetListenIp(EndPoint clientLocalEndPoint)
		{
			IPEndPoint ipEndPoint = clientLocalEndPoint as IPEndPoint;
			if (ipEndPoint == null)
			{
				throw new NotImplementedException(clientLocalEndPoint.GetType().Name);
			}

			if (ipEndPoint.Address.AddressFamily == AddressFamily.InterNetwork)
			{
				return ipEndPoint.Address;
			}

			if (ipEndPoint.Address.IsIPv4MappedToIPv6)
			{
				return ipEndPoint.Address.MapToIPv4();
			}

			throw new NotImplementedException(String.Format("Don't know whether I can listen on IP {0}", ipEndPoint.ToString()));
		}

		public string RootPath { get; set; }

		private TcpClient TcpClient { get; set; }
		private NetworkStream NetworkStream { get; set; }
		private BufferedStream BufferedStream { get; set; }
		private StreamReader StreamReader { get; set; }
		private StreamWriter StreamWriter { get; set; }

		public RtspOptionsResponse GetOptions(string url)
		{
			RtspOptionsRequest request = new RtspOptionsRequest();
			request.RequestPath = url;
			request.CSeq = cseqCounter++;
			request.UserAgent = USER_AGENT;
			RtspResponseHeader header = GetResponse(request.ListHeaders(RootPath));
			RtspOptionsResponse result = new RtspOptionsResponse(header);
			return result;
		}

		public RtspDescribeResponse GetDescribe(string url)
		{
			RtspDescribeRequest request = new RtspDescribeRequest();
			request.RequestPath = url;
			request.CSeq = cseqCounter++;
			request.UserAgent = USER_AGENT;
			request.Accept = "application/sdp";
			RtspResponseHeader header = GetResponse(request.ListHeaders(RootPath));
			RtspDescribeResponse result = new RtspDescribeResponse(header);
			return result;
		}

		public RtspSetupResponse GetSetup(string url, int destinationPort = 0, bool multicastMode = false, IPAddress multicastIp = null)
		{
			RtspSetupRequest request = new RtspSetupRequest();
			request.RequestPath = url;
			request.CSeq = cseqCounter++;
			request.UserAgent = USER_AGENT;

			int rtpPort = 0;
			Socket rtpSocket = null;
			if (destinationPort == 0)
			{
				rtpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
				rtpSocket.Bind(new IPEndPoint(ListenIp, 0));
				rtpPort = GetPortFromEndPoint(rtpSocket.LocalEndPoint);
			}
			else
			{
				rtpPort = destinationPort;
			}

			Socket rtcpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
			rtcpSocket.Bind(new IPEndPoint(ListenIp, 0));
			int rtcpPort = GetPortFromEndPoint(rtcpSocket.LocalEndPoint);

			if (multicastMode && multicastIp != null)
			{
				request.SetRtpAvpMulticast(multicastIp, rtpPort, rtcpPort);
			}
			else
			{
				request.SetRtpAvpUnicast(rtpPort, rtcpPort);
			}

			RtspResponseHeader response = GetResponse(request.ListHeaders(RootPath));
			RtspSetupResponse setupResponse = new RtspSetupResponse(response);
			switch (response.statusCode)
			{
				case 200:
					setupResponse.RtpSocket = rtpSocket;
					setupResponse.RtcpSocket = rtcpSocket;
					setupResponse.SetupListeners();
					setupResponse.Valid = true;
					break;
				case 404:
					setupResponse.Valid = true;
					break;
				default:
					throw new NotImplementedException(setupResponse.RtspStatusCode.ToString());
			}
			return setupResponse;
		}

		public RtspPlayResponse GetPlay(RtspSetupResponse setupData)
		{
			RtspPlayRequest request = new RtspPlayRequest();
			request.RequestPath = string.Format("/stream={0}", setupData.StreamId);
			request.Session = setupData.Session;
			request.UserAgent = USER_AGENT;

			RtspPlayResponse response = new RtspPlayResponse(GetResponse(request.ListHeaders(RootPath)));
			return response;
		}

		public RtspTeardownResponse GetTeardown(RtspSetupResponse setupData)
		{
			RtspTeardownRequest request = new RtspTeardownRequest();
			request.RequestPath = string.Format("/stream={0}", setupData.StreamId);
			request.Session = setupData.Session;
			request.UserAgent = USER_AGENT;

			RtspTeardownResponse response = new RtspTeardownResponse(GetResponse(request.ListHeaders(RootPath)));
			if (response.RtspStatusCode == 200)
			{
				setupData.InvokeCancellationTokens();
				if (AutoReconnect)
				{
					Reconnect();
				}
			}
			return response;
		}

		private int GetPortFromEndPoint(EndPoint endpoint)
		{
			IPEndPoint ipEndPoint = endpoint as IPEndPoint;
			if (ipEndPoint == null)
				throw new NotImplementedException(endpoint.GetType().Name);
			return ipEndPoint.Port;
		}

		private RtspResponseHeader GetResponse(string request)
		{
			StreamWriter.Write(request);
			StreamWriter.Flush();

			RtspResponseHeader result = new RtspResponseHeader();

			string response = StreamReader.ReadLine();
			if (!response.StartsWith("RTSP/"))
				throw new RtspException("Invalid RTSP response.");
			
			response = response.Substring(5);

			string versionString = response.Substring(0, 3);
			result.rtspVersion = Version.Parse(versionString);
			response = response.Substring(4);

			string statusCodeString = response.Substring(0,3);
			result.statusCode = ushort.Parse(statusCodeString);

			response = response.Substring(4);
			result.statusLine = response;

			long contentLength = 0;

			result.kv = new Dictionary<string, string>();
			while (true)
			{
				string lineIn = StreamReader.ReadLine();
				if (string.IsNullOrEmpty(lineIn))
					break;

				int indexOf = lineIn.IndexOf(": ");
				string key = lineIn.Substring(0, indexOf);
				string value = lineIn.Substring(indexOf + 2);
				result.kv.Add(key, value);

				if (key.Equals("Content-Length"))
				{
					contentLength = long.Parse(value);
				}
			}

			if (contentLength > 0)
			{
				StreamReader.DiscardBufferedData();
				byte[] buffer = new byte[contentLength];
				int sucessfullyRead = BufferedStream.Read(buffer, 0, (int)contentLength);
				if (sucessfullyRead != contentLength)
				{
					throw new IOException("incomplete read");
				}

				result.payload = buffer;
			}

			return result;
		}

		public void Reconnect()
		{
			EndPoint clientRemoteEndPoint = this.TcpClient.Client.RemoteEndPoint;
			IPEndPoint ipEndPoint = clientRemoteEndPoint as IPEndPoint;
			if (ipEndPoint == null)
			{
				throw new NotImplementedException(clientRemoteEndPoint.GetType().ToString());
			}

			this.TcpClient.Close();
			this.TcpClient = new TcpClient();
			this.TcpClient.Connect(ipEndPoint);
			this.RootPath = string.Format("rtsp://{0}:{1}", ipEndPoint.Address, ipEndPoint.Port);
			this.NetworkStream = TcpClient.GetStream();
			this.BufferedStream = new BufferedStream(this.NetworkStream);
			this.StreamReader = new StreamReader(this.BufferedStream);
			this.StreamWriter = new StreamWriter(this.BufferedStream);
			this.cseqCounter = 2;
			this.ListenIp = GetListenIp(this.TcpClient.Client.LocalEndPoint);
		}

		/// <summary>
		/// Generates a SAT>IP Tuning string.
		/// </summary>
		/// <param name="diseqcChannel">The DiSEqC Command to send.</param>
		/// <param name="freq">The frequency to tune to in MHz.</param>
		/// <param name="isS2">Set this to true if tuning to DVB-S2/S2X, or false for DVB-S</param>
		/// <param name="symbolrate">The transponder's symbol rate in Ksyms.</param>
		/// <param name="forceBbframeMode">Set this to true to force a STiD135 to BBFrame mode. Set this to false if you do not want this, or if the tuner isn't a STiD135. false is always safe here.</param>
		/// <returns>A SAT>IP Tuning String</returns>
		/// <exception cref="ArgumentOutOfRangeException">Thrown when an invalid DiSEqC Command is supplied.</exception>
		public static string MakeUrl(DiSEqC_Opcode diseqcChannel, int freq, bool isS2, int symbolrate, byte? mis = null, bool forceBbframeMode = false)
		{
			bool diseqcOk = false;
			byte diseqc = 1;
			if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_A) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_A))
			{
				diseqc = 1;
				diseqcOk = true;
			}
			if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_A) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_B))
			{
				diseqc = 2;
				diseqcOk = true;
			}
			if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_B) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_A))
			{
				diseqc = 3;
				diseqcOk = true;
			}
			if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_B) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_B))
			{
				diseqc = 4;
				diseqcOk = true;
			}
			if (!diseqcOk)
			{
				throw new ArgumentOutOfRangeException(nameof(diseqcChannel));
			}

			char pol;
			if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_HORIZONTAL))
			{
				pol = 'h';
			}
			else
			{
				pol = 'v';
			}
			
			StringBuilder sb = new StringBuilder();
			sb.AppendFormat("/?src={0}", diseqc);
			sb.AppendFormat("&freq={0}", freq);
			sb.AppendFormat("&pol={0}", pol);
			sb.AppendFormat("&msys={0}", isS2 ? "dvbs2" : "dvbs");
			sb.AppendFormat("&sr={0}", symbolrate);
			sb.AppendFormat("&pids=all");

			//Thanks to the Digital Devices Customer Support for giving me this advice.
			if (forceBbframeMode)
			{
				sb.Append("&x_isi=0x80000000");
			} 
			else if (mis.HasValue)
			{
				sb.AppendFormat("&x_isi={0}", mis.Value);
			}

			return sb.ToString();
		}

		private bool disposed;
		public void Dispose()
		{
			if (disposed)
				throw new ObjectDisposedException(nameof(RtspClient));

			ListenIp = null;
			RootPath = null;
			if (TcpClient.Connected)
			{
				TcpClient.Close();
			}

			TcpClient.Dispose();
			NetworkStream = null;
			BufferedStream = null;
			StreamReader = null;
			StreamWriter = null;
			disposed = true;
		}
	}
}
