// 
// TcpBinaryFrameManager.cs
// 
// Author: 
//	Atsushi Enomoto  <atsushi@ximian.com>
// 
// Copyright (C) 2009 Novell, Inc (http://www.novell.com)
// Copyright 2011 Xamarin Inc (http://xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading;
using System.Xml;

namespace System.ServiceModel.Channels.NetTcp
{
	// seealso: [MC-NMF] Windows Protocol document.
	class TcpBinaryFrameManager
	{
		class MyBinaryReader : BinaryReader
		{
			public MyBinaryReader (Stream s)
				: base (s)
			{
			}

			public int ReadVariableInt ()
			{
				return Read7BitEncodedInt ();
			}
		}

		class MyBinaryWriter : BinaryWriter
		{
			public MyBinaryWriter (Stream s)
				: base (s)
			{
			}

			public void WriteVariableInt (int value)
			{
				Write7BitEncodedInt (value);
			}

			public int GetSizeOfLength (int value)
			{
				int x = 0;
				do {
					value /= 0x100;
					x++;
				} while (value != 0);
				return x;
			}
		}

		class MyXmlBinaryWriterSession : XmlBinaryWriterSession
		{
			public override bool TryAdd (XmlDictionaryString value, out int key)
			{
				if (!base.TryAdd (value, out key))
					return false;
				List.Add (value);
				return true;
			}

			public List<XmlDictionaryString> List = new List<XmlDictionaryString> ();
		}

		public const byte VersionRecord = 0;
		public const byte ModeRecord = 1;
		public const byte ViaRecord = 2;
		public const byte KnownEncodingRecord = 3;
		public const byte ExtendingEncodingRecord = 4;
		public const byte UnsizedEnvelopeRecord = 5;
		public const byte SizedEnvelopeRecord = 6;
		public const byte EndRecord = 7;
		public const byte FaultRecord = 8;
		public const byte UpgradeRequestRecord = 9;
		public const byte UpgradeResponseRecord = 0xA;
		public const byte PreambleAckRecord = 0xB;
		public const byte PreambleEndRecord = 0xC;

		public const byte UnsizedMessageTerminator = 0;
		public const byte SingletonUnsizedMode = 1;
		public const byte DuplexMode = 2;
		public const byte SimplexMode = 3;
		public const byte SingletonSizedMode = 4;

		public const byte Soap11EncodingUtf8 = 0;
		public const byte Soap11EncodingUtf16 = 1;
		public const byte Soap11EncodingUtf16LE = 2;
		public const byte Soap12EncodingUtf8 = 3;
		public const byte Soap12EncodingUtf16 = 4;
		public const byte Soap12EncodingUtf16LE = 5;
		public const byte Soap12EncodingMtom = 6;
		public const byte Soap12EncodingBinary = 7;
		public const byte Soap12EncodingBinaryWithDictionary = 8;
		public const byte UseExtendedEncodingRecord = 0xFF;

		MyBinaryReader reader;
		MyBinaryWriter writer;

		public TcpBinaryFrameManager (int mode, Stream s, bool isServiceSide)
		{
			this.mode = mode;
			this.s = s;
			this.is_service_side = isServiceSide;
			reader = new MyBinaryReader (s);
			ResetWriteBuffer ();

			EncodingRecord = Soap12EncodingBinaryWithDictionary; // FIXME: it should depend on mode.
		}

		Stream s;
		MemoryStream buffer;
		bool is_service_side;

		int mode;

		public byte EncodingRecord { get; private set; }
		public string ExtendedEncodingRecord { get; private set; }

		public Uri Via { get; set; }

		static readonly char [] convtest = new char [1] {'A'};
		MessageEncoder encoder;
		public MessageEncoder Encoder {
			get { return encoder; }
			set {
				encoder = value;
				EncodingRecord = UseExtendedEncodingRecord;
				var be = encoder as BinaryMessageEncoder;
				if (be != null)
					EncodingRecord = be.UseSession ? Soap12EncodingBinaryWithDictionary : Soap12EncodingBinary;
				var te = encoder as TextMessageEncoder;
				if (te != null) {
					var u16 = te.Encoding as UnicodeEncoding;
					bool u16be = u16 != null && u16.GetBytes (convtest) [0] == 0;
					if (encoder.MessageVersion.Envelope.Equals (EnvelopeVersion.Soap11)) {
						if (u16 != null)
							EncodingRecord = u16be ? Soap11EncodingUtf16 : Soap11EncodingUtf16LE;
						else
							EncodingRecord = Soap11EncodingUtf8;
					} else {
						if (u16 != null)
							EncodingRecord = u16be ? Soap12EncodingUtf16 : Soap12EncodingUtf16LE;
						else
							EncodingRecord = Soap12EncodingUtf8;
					}
				}
				if (value is MtomMessageEncoder)
					EncodingRecord = Soap12EncodingMtom;

				if (EncodingRecord == UseExtendedEncodingRecord)
					ExtendedEncodingRecord = encoder.ContentType;
			}
		}

		void ResetWriteBuffer ()
		{
			this.buffer = new MemoryStream ();
			writer = new MyBinaryWriter (buffer);
		}

		static readonly byte [] empty_bytes = new byte [0];

		public byte [] ReadSizedChunk ()
		{
			lock (read_lock) {

			int length = reader.ReadVariableInt ();
			if (length == 0)
				return empty_bytes;

			byte [] buffer = new byte [length];
			for (int readSize = 0; readSize < length; )
				readSize += reader.Read (buffer, readSize, length - readSize);
			return buffer;

			}
		}

		void WriteSizedChunk (byte [] data, int index, int length)
		{
			writer.WriteVariableInt (length);
			writer.Write (data, index, length);
		}

		public void ProcessPreambleInitiator ()
		{
			ResetWriteBuffer ();

			buffer.WriteByte (VersionRecord);
			buffer.WriteByte (1);
			buffer.WriteByte (0);
			buffer.WriteByte (ModeRecord);
			buffer.WriteByte ((byte) mode);
			buffer.WriteByte (ViaRecord);
			writer.Write (Via.ToString ());
			buffer.WriteByte (KnownEncodingRecord); // FIXME
			buffer.WriteByte ((byte) EncodingRecord);
			buffer.WriteByte (PreambleEndRecord);
			buffer.Flush ();
			s.Write (buffer.GetBuffer (), 0, (int) buffer.Position);
			s.Flush ();
		}

		public void ProcessPreambleAckInitiator ()
		{
			int b = s.ReadByte ();
			switch (b) {
			case PreambleAckRecord:
				return; // success
			case FaultRecord:
				throw new FaultException (reader.ReadString ());
			default:
				throw new ProtocolException (String.Format ("Preamble Ack Record is expected, got {0:X}", b));
			}
		}

		public void ProcessPreambleAckRecipient ()
		{
			s.WriteByte (PreambleAckRecord);
		}

		public bool ProcessPreambleRecipient ()
		{
			return ProcessPreambleRecipient (-1);
		}
		bool ProcessPreambleRecipient (int initialByte)
		{
			bool preambleEnd = false;
			while (!preambleEnd) {
				int b = initialByte < 0 ? s.ReadByte () : initialByte;
				if (b < 0)
					return false;
				switch (b) {
				case VersionRecord:
					if (s.ReadByte () != 1)
						throw new ProtocolException ("Major version must be 1");
					if (s.ReadByte () != 0)
						throw new ProtocolException ("Minor version must be 0");
					break;
				case ModeRecord:
					if (s.ReadByte () != mode)
						throw new ProtocolException (String.Format ("Duplex mode is expected to be {0:X}", mode));
					break;
				case ViaRecord:
					Via = new Uri (reader.ReadString ());
					break;
				case KnownEncodingRecord:
					EncodingRecord = (byte) s.ReadByte ();
					break;
				case ExtendingEncodingRecord:
					throw new NotImplementedException ("ExtendingEncodingRecord");
				case UpgradeRequestRecord:
					throw new NotImplementedException ("UpgradeRequetRecord");
				case UpgradeResponseRecord:
					throw new NotImplementedException ("UpgradeResponseRecord");
				case PreambleEndRecord:
					preambleEnd = true;
					break;
				default:
					throw new ProtocolException (String.Format ("Unexpected record type {0:X2}", b));
				}
			}
			return true;
		}

		XmlBinaryReaderSession reader_session;
		int reader_session_items;

		object read_lock = new object ();
		object write_lock = new object ();

		public Message ReadSizedMessage ()
		{
			lock (read_lock) {

			// FIXME: implement full [MC-NMF].

			int packetType;
			try {
				packetType = s.ReadByte ();
			} catch (IOException) {
				// it is already disconnected
				return null;
			} catch (SocketException) {
				// it is already disconnected
				return null;
			}
			// FIXME: .NET never results in -1, so there may be implementation mismatch in Socket (but might be in other places)
			if (packetType == -1)
				return null;
			// FIXME: The client should wait for EndRecord, but if we try to send it, the socket blocks and becomes unable to work anymore.
			if (packetType == EndRecord)
				return null;
			if (packetType != SizedEnvelopeRecord) {
				if (is_service_side) {
					// reconnect
					ProcessPreambleRecipient (packetType);
					ProcessPreambleAckRecipient ();
				}
				else
					throw new NotImplementedException (String.Format ("Packet type {0:X} is not implemented", packetType));
			}

			byte [] buffer = ReadSizedChunk ();
			var ms = new MemoryStream (buffer, 0, buffer.Length);

			// FIXME: turned out that it could be either in-band dictionary ([MC-NBFSE]), or a mere xml body ([MC-NBFS]).
			bool inBandDic = false;
			XmlBinaryReaderSession session = null;
			switch (EncodingRecord) {
			case Soap11EncodingUtf8:
			case Soap11EncodingUtf16:
			case Soap11EncodingUtf16LE:
			case Soap12EncodingUtf8:
			case Soap12EncodingUtf16:
			case Soap12EncodingUtf16LE:
				if (!(Encoder is TextMessageEncoder))
					throw new InvalidOperationException (String.Format ("Unexpected message encoding value in the received message: {0:X}", EncodingRecord));
				break;
			case Soap12EncodingMtom:
				if (!(Encoder is MtomMessageEncoder))
					throw new InvalidOperationException (String.Format ("Unexpected message encoding value in the received message: {0:X}", EncodingRecord));
				break;
			default:
					throw new InvalidOperationException (String.Format ("Unexpected message encoding value in the received message: {0:X}", EncodingRecord));
			case Soap12EncodingBinaryWithDictionary:
				inBandDic = true;
				goto case Soap12EncodingBinary;
			case Soap12EncodingBinary:
				session = inBandDic ? (reader_session ?? new XmlBinaryReaderSession ()) : null;
				reader_session = session;
				if (inBandDic) {
					byte [] rsbuf = new TcpBinaryFrameManager (0, ms, is_service_side).ReadSizedChunk ();
					using (var rms = new MemoryStream (rsbuf, 0, rsbuf.Length)) {
						var rbr = new BinaryReader (rms, Encoding.UTF8);
						while (rms.Position < rms.Length)
							session.Add (reader_session_items++, rbr.ReadString ());
					}
				}
				break;
			}
			var benc = Encoder as BinaryMessageEncoder;
			lock (Encoder) {
				if (benc != null)
					benc.CurrentReaderSession = session;

				// FIXME: supply maxSizeOfHeaders.
				Message msg = Encoder.ReadMessage (ms, 0x10000);
				if (benc != null)
					benc.CurrentReaderSession = null;
				return msg;
			}
			
			}
		}

		// FIXME: support timeout
		public Message ReadUnsizedMessage (TimeSpan timeout)
		{
			lock (read_lock) {

			// Encoding type 7 is expected
			if (EncodingRecord != Soap12EncodingBinary)
				throw new NotImplementedException (String.Format ("Message encoding {0:X} is not implemented yet", EncodingRecord));

			var packetType = s.ReadByte ();

			if (packetType == EndRecord)
				return null;
			if (packetType != UnsizedEnvelopeRecord)
				throw new NotImplementedException (String.Format ("Packet type {0:X} is not implemented", packetType));

			var ms = new MemoryStream ();
			while (true) {
				byte [] buffer = ReadSizedChunk ();
				if (buffer.Length == 0) // i.e. it is UnsizedMessageTerminator (which is '0')
					break;
				ms.Write (buffer, 0, buffer.Length);
			}
			ms.Seek (0, SeekOrigin.Begin);

			// FIXME: supply correct maxSizeOfHeaders.
			Message msg = Encoder.ReadMessage (ms, (int) ms.Length);

			return msg;
			
			}
		}

		byte [] eof_buffer = new byte [1];
		MyXmlBinaryWriterSession writer_session;

		public void WriteSizedMessage (Message message)
		{
			lock (write_lock) {

			ResetWriteBuffer ();

			buffer.WriteByte (SizedEnvelopeRecord);

			MemoryStream ms = new MemoryStream ();
			var session = writer_session ?? new MyXmlBinaryWriterSession ();
			writer_session = session;
			int writer_session_count = session.List.Count;
			var benc = Encoder as BinaryMessageEncoder;
			try {
				if (benc != null)
					benc.CurrentWriterSession = session;
				Encoder.WriteMessage (message, ms);
			} finally {
				if (benc != null)
					benc.CurrentWriterSession = null;
			}

			// dictionary
			if (EncodingRecord == Soap12EncodingBinaryWithDictionary) {
				MemoryStream msd = new MemoryStream ();
				BinaryWriter dw = new BinaryWriter (msd);
				for (int i = writer_session_count; i < session.List.Count; i++)
					dw.Write (session.List [i].Value);
				dw.Flush ();

				int length = (int) (msd.Position + ms.Position);
				var msda = msd.ToArray ();
				int sizeOfLength = writer.GetSizeOfLength (msda.Length);

				writer.WriteVariableInt (length + sizeOfLength); // dictionary array also involves the size of itself.
				WriteSizedChunk (msda, 0, msda.Length);
			}
			else
				writer.WriteVariableInt ((int) ms.Position);

			// message body
			var arr = ms.GetBuffer ();
			writer.Write (arr, 0, (int) ms.Position);

			writer.Flush ();

			s.Write (buffer.GetBuffer (), 0, (int) buffer.Position);
			s.Flush ();

			}
		}

		// FIXME: support timeout
		public void WriteUnsizedMessage (Message message, TimeSpan timeout)
		{
			lock (write_lock) {

			ResetWriteBuffer ();

			s.WriteByte (UnsizedEnvelopeRecord);
			s.Flush ();

			Encoder.WriteMessage (message, buffer);
			new MyBinaryWriter (s).WriteVariableInt ((int) buffer.Position);
			s.Write (buffer.GetBuffer (), 0, (int) buffer.Position);

			s.WriteByte (UnsizedMessageTerminator); // terminator
			s.Flush ();

			}
		}

		public void WriteEndRecord ()
		{
			lock (write_lock) {

			s.WriteByte (EndRecord); // it is required
			s.Flush ();

			}
		}

		public void ReadEndRecord ()
		{
			lock (read_lock) {

			int b;
			if ((b = s.ReadByte ()) != EndRecord)
				throw new ProtocolException (String.Format ("EndRecord message was expected, got {0:X}", b));

			}
		}
	}
}