/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * Copyright (c) 2003-2007 by AG-Software 											 *
 * All Rights Reserved.																 *
 * Contact information for AG-Software is available at http://www.ag-software.de	 *
 *																					 *
 * Licence:																			 *
 * The agsXMPP SDK is released under a dual licence									 *
 * agsXMPP can be used under either of two licences									 *
 * 																					 *
 * A commercial licence which is probably the most appropriate for commercial 		 *
 * corporate use and closed source projects. 										 *
 *																					 *
 * The GNU Public License (GPL) is probably most appropriate for inclusion in		 *
 * other open source projects.														 *
 *																					 *
 * See README.html for details.														 *
 *																					 *
 * For general enquiries visit our website at:										 *
 * http://www.ag-software.de														 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Collections;
using System.Security.Cryptography;

using agsXMPP.Xml;
using agsXMPP.Xml.Dom;
using agsXMPP.protocol.extensions.bosh;

namespace agsXMPP.net
{
    public class BoshClientSocket : BaseSocket
    {
        private const string    CONTENT_TYPE   = "text/xml; charset=utf-8";
        private const string    METHOD         = "POST";

        private string[]        m_Keys;

        private int             waitingRequests = 0; // currently active (waiting) WebRequests
       
        private int             m_CurrentKeyIdx;
        private Queue           m_SendQueue     = new Queue();
        private bool            streamStarted   = false;
        private int             polling         = 0;
        private DateTime        lastSend;
        /// <summary>
        /// equals the StreamId
        /// </summary>
        private string          m_AuthId        = null;

        private long            rid;
        internal string sid;
                
           
        public BoshClientSocket()
        {
           
        }

        private void Init()
        {         
            m_Keys = null;
            streamStarted = false;
        }

        #region << Properties >>
        private Jid             m_To;
        private int             m_Wait          = 60;
        private int             m_Requests      = 2;
        
#if !CF               
        private int             m_CountKeys     = 256;
#else
		// set this lower on embedded devices because the key generation is slow there
		private int				m_CountKeys		= 32;
#endif
        private int             m_Hold          = 1;        // should be 1

        public Jid To
        {
            get { return m_To; }
            set { m_To = value; }
        }

        /// <summary>
        /// The longest time (in seconds) that the connection manager is allowed to wait before responding to any request during the session.
        /// This enables the client to prevent its TCP connection from expiring due to inactivity, as well as to limit the delay before 
        /// it discovers any network failure.
        /// </summary>
        public int Wait
        {
            get { return m_Wait; }
            set { m_Wait = value; }
        }

        public int Requests
        {
            get { return m_Requests; }
            set { m_Requests = value; }
        }

        /// <summary>
        /// count of keys to generate each time. Keys are generated with the Sha1 algoritm.
        /// You can reduce the num,ber of keys to gemerate each time if your device is to slow on generating the keys
        /// or you want to save memory.
        /// 256 is the default value, 32 on CF
        /// </summary>
        public int CountKeys
        {
            get { return m_CountKeys; }
            set { m_CountKeys = value; }
        }
        
        public int Hold
        {
            get { return m_Hold; }
            set { m_Hold = value; }
        }
        #endregion


        private string DummyStreamHeader
        {
            get
            {
                // <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='myjabber.net' id='1075705237'>
                // create dummy stream header		
                StringBuilder sb = new StringBuilder();

                sb.Append("<stream:stream");

                sb.Append(" xmlns='");
                sb.Append(Uri.CLIENT);

                sb.Append("' xmlns:stream='");
                sb.Append(Uri.STREAM);

                sb.Append("' id='");
                sb.Append(sid);

                sb.Append("' version='");
                sb.Append("1.0");

                sb.Append("'>");

                return sb.ToString();
            }
        }

        /// <summary>
        /// Generates a bunch of keys
        /// </summary>
        private void GenerateKeys()
        {           
            /*
            13.3 Generating the Key Sequence

            Prior to requesting a new session, the client MUST select an unpredictable counter ("n") and an unpredictable value ("seed").
            The client then processes the "seed" through a cryptographic hash and converts the resulting 160 bits to a hexadecimal string K(1).
            It does this "n" times to arrive at the initial key K(n). The hashing algorithm MUST be SHA-1 as defined in RFC 3174.

            Example 25. Creating the key sequence

                    K(1) = hex(SHA-1(seed))
                    K(2) = hex(SHA-1(K(1)))
                    ...
                    K(n) = hex(SHA-1(K(n-1)))      

            */

            m_Keys = new string[m_CountKeys];
            string prev = GenerateSeed();            

            for (int i = 0; i < m_CountKeys; i++)
            {
                m_Keys[i] = util.Hash.Sha1Hash(prev);            
                prev = m_Keys[i];
            }
            m_CurrentKeyIdx = m_CountKeys - 1;
        }

        private string GenerateSeed()
        {
            int m_lenght = 10;

            RandomNumberGenerator rng = RandomNumberGenerator.Create();
            byte[] buf = new byte[m_lenght];
            rng.GetBytes(buf);

            return util.Hash.HexToString(buf);
        }

        private int GenerateRid()
        {
            int min = 1;
            int max = int.MaxValue;
            
            Random rnd = new Random();
            
            return rnd.Next(min, max);
        }

        public void RequestBoshSession()
        {
            /*
            Example 1. Requesting a BOSH session

            POST /webclient HTTP/1.1
            Host: httpcm.jabber.org
            Accept-Encoding: gzip, deflate
            Content-Type: text/xml; charset=utf-8
            Content-Length: 104

            <body content='text/xml; charset=utf-8'
                  hold='1'
                  rid='1573741820'
                  to='jabber.org'
                  route='xmpp:jabber.org:9999'
                  secure='true'
                  ver='1.6'
                  wait='60'
                  ack='1'
                  xml:lang='en'
                  xmlns='http://jabber.org/protocol/httpbind'/>
             */

            lastSend = DateTime.Now;

            // Generate the keys
            GenerateKeys();
            rid = GenerateRid();
            Body body = new Body();

            body.Hold       = m_Hold;
            body.Wait       = m_Wait;
            body.Rid        = rid;
            body.Polling    = 0;
            body.Requests   = m_Requests;
            //body.To = new Jid("vm-win2k");
            body.To = new Jid("jwchat.org");
            //body.To = new Jid("vm-xp");

            //body.NewKey = m_Keys[m_CurrentKeyIdx];


            waitingRequests++;

            //HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://vm-xp:8080/http-bind/");
            //HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://vm-win2k:8080/http-bind/");
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://jwchat.org/JHB/");

            req.Method      = METHOD;
            req.ContentType = CONTENT_TYPE;
            req.Timeout     = 60000;

            WebRequestState state = new WebRequestState(req);
            state.Output            = body.ToString();
            state.IsSessionRequest  = true;

            IAsyncResult result = req.BeginGetRequestStream(new AsyncCallback(this.OnGetSessionRequestStream), state);
            
        }

        private void OnGetSessionRequestStream(IAsyncResult ar)
        {
            WebRequestState state = ar.AsyncState as WebRequestState;

            HttpWebRequest req = state.WebRequest as HttpWebRequest;

            Stream outputStream = req.EndGetRequestStream(ar);            
            
            byte[] bytes = Encoding.UTF8.GetBytes(state.Output);
               
            state.RequestStream = outputStream;
            IAsyncResult result = outputStream.BeginWrite(bytes, 0, bytes.Length, new AsyncCallback(this.OnEndWrite), state);
        }
        
        private void OnGetSessionRequestResponse(IAsyncResult result)
        {
            // grab the custom state object
            WebRequestState state = (WebRequestState)result.AsyncState;
            HttpWebRequest request = (HttpWebRequest)state.WebRequest;

            // get the Response
            HttpWebResponse resp = (HttpWebResponse)request.EndGetResponse(result);

            // The server must always return a 200 response code,
            // sending any session errors as specially-formatted identifiers.
            if (resp.StatusCode != HttpStatusCode.OK)
            {
                //FireOnError(new PollSocketException("unexpected status code " + resp.StatusCode.ToString()));
                return;
            }

            Stream rs = resp.GetResponseStream();

            int readlen;
            byte[] readbuf = new byte[1024];
            MemoryStream ms = new MemoryStream();
            while ((readlen = rs.Read(readbuf, 0, readbuf.Length)) > 0)
            {
                ms.Write(readbuf, 0, readlen);
            }

            byte[] recv = ms.ToArray();

            if (recv.Length > 0)
            {
                string body = null;
                string stanzas = null;

                //ParseResponse(Encoding.UTF8.GetString(recv), ref body, ref stanzas);
                string res = Encoding.UTF8.GetString(recv);

                ParseResponse(res, ref body, ref stanzas);
              
                Document doc = new Document();
                doc.LoadXml(body);
                Body boshBody = doc.RootElement as Body;

                sid     = boshBody.Sid;
                polling = boshBody.Polling;              	

                byte[] bin = Encoding.UTF8.GetBytes(DummyStreamHeader + stanzas);
                //byte[] bin = Encoding.UTF8.GetBytes( stanzas);
                base.FireOnReceive(bin, bin.Length);
                
                waitingRequests--;
                
                //if (waitingRequests == 0)
                //    StartWebRequest(); 
            }
        }

        /// <summary>
        /// This is ugly code, but currently all BOSH server implementaions are not namespace correct,
        /// which means we can't use the XML parser here and have to spit it with string functions.
        /// </summary>
        /// <param name="res"></param>
        /// <param name="body"></param>
        /// <param name="stanzas"></param>
        private void ParseResponse(string res, ref string body, ref string stanzas)
        {
            res = res.Trim();
            if (res.EndsWith("/>"))
            {
                // <body ..../>
                // empty response
                body = res;
                stanzas = null;
            }
            else
            {
                /* 
                 * <body .....>
                 *  <message/>
                 *  <presence/>
                 * </body>  
                 */

                // find position of the first closing angle bracket
                int startPos = res.IndexOf(">");
                // find position of the last opening angle bracket
                int endPos = res.LastIndexOf("<");

                body = res.Substring(0, startPos) + "/>";
                stanzas = res.Substring(startPos + 1, endPos - startPos - 1);
            }
        }

        private class WebRequestState
        {
            public WebRequestState(WebRequest request)
            {
                m_WebRequest = request;
            }

            WebRequest  m_WebRequest    = null;
            Stream      m_RequestStream = null;
            string m_Output = null;
            bool m_IsSessionRequest = false;

            public bool IsSessionRequest
            {
                get { return m_IsSessionRequest; }
                set { m_IsSessionRequest = value; }
            }

            public string Output
            {
                get { return m_Output; }
                set { m_Output = value; }
            }

            public WebRequest WebRequest
            {
                get { return m_WebRequest; }
                set { m_WebRequest = value; }
            }

            public Stream RequestStream
            {
                get { return m_RequestStream; }
                set { m_RequestStream = value; }
            }
        }

       

        #region << Public Methods and Functions >>
        public override void Connect()
        {            
            base.Connect();

            Init();
            FireOnConnect();

            RequestBoshSession();
        }

        public override void Disconnect()
        {
            base.Disconnect();

            FireOnDisconnect();

            
            //m_Connected = false;
        }

        public override void Send(byte[] bData)
        {
            base.Send(bData);

            Send(Encoding.UTF8.GetString(bData, 0, bData.Length));
        }


        public override void Send(string data)
        {
            base.Send(data);

            // This are hacks because we send no stream headers and footer in Bosh
            if (data.StartsWith("<stream:stream"))
            {
                if (!streamStarted)
                    streamStarted = true;
                else
                {
                    byte[] bin = Encoding.UTF8.GetBytes(DummyStreamHeader);
                    //byte[] bin = Encoding.UTF8.GetBytes( stanzas);
                    base.FireOnReceive(bin, bin.Length);
                }
                return;
            }
            if (data.EndsWith("</stream:stream>"))
                return;

            m_SendQueue.Enqueue(data);

            if (waitingRequests <= 1)
            {
                //if (m_PollTimer != null)
                //    DestroyPollTimer();
                
                StartWebRequest();
            }
        }
        #endregion

        private string BuildPostData()
        {
            StringBuilder sb = new StringBuilder();
            
            Body body = new Body();
            
            body.Rid        = rid;
            //body.Key = m_Keys[m_CurrentKeyIdx];
            body.Sid        = sid;
            //body.Polling    = 0;
            body.To = new Jid("jwchat.org");
            //body.To         = new Jid("vm-win2k");
            //body.To         = new Jid("vm-xp");

            if (m_SendQueue.Count > 0)
            {
                sb.Append(body.StartTag());

                while (m_SendQueue.Count > 0)
                {
                    string data = m_SendQueue.Dequeue() as string;
                    sb.Append(data);
                }

                sb.Append(body.EndTag());

                return sb.ToString();
            }
            else
                return body.ToString();
        }

        private void StartWebRequest()
        {
            rid++;
            m_CurrentKeyIdx--;
            waitingRequests++;

            lastSend = DateTime.Now;

            //HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://vm-xp:8080/http-bind/");
            //HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://vm-win2k:8080/http-bind/");
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://jwchat.org/JHB/");
            
//POST /JHB/ HTTP/1.1
//Host: jwchat.org

            req.Method      = METHOD;
            req.ContentType = CONTENT_TYPE;
            req.Timeout     = 300000;

            WebRequestState state = new WebRequestState(req);
            
            IAsyncResult result = req.BeginGetRequestStream(new AsyncCallback(this.OnGetRequestStream), state);            
        }

        private void OnGetRequestStream(IAsyncResult ar)
        {
            WebRequestState state = ar.AsyncState as WebRequestState;

            HttpWebRequest req = state.WebRequest as HttpWebRequest;
            
            Stream requestStream = req.EndGetRequestStream(ar);
            state.RequestStream = requestStream;
            byte[] bytes = Encoding.UTF8.GetBytes(BuildPostData());

            IAsyncResult result = requestStream.BeginWrite(bytes, 0, bytes.Length, new AsyncCallback(this.OnEndWrite), state);
        }

        private void OnEndWrite(IAsyncResult ar)
        {
            WebRequestState state = ar.AsyncState as WebRequestState;

            HttpWebRequest req      = state.WebRequest as HttpWebRequest;
            Stream requestStream    = state.RequestStream;

            requestStream.EndWrite(ar);
            requestStream.Close();
            
            IAsyncResult result;
            if (state.IsSessionRequest)
                result = req.BeginGetResponse(new AsyncCallback(this.OnGetSessionRequestResponse), state);            
            else
                result = req.BeginGetResponse(new AsyncCallback(this.OnGetResponse), state);            
                
        }

        private void OnGetResponse(IAsyncResult ar)
        {
            // grab the custom state object
            WebRequestState state = (WebRequestState)ar.AsyncState;
            HttpWebRequest request = (HttpWebRequest)state.WebRequest;

            HttpWebResponse resp = null;

            if (request.HaveResponse)
            {
                // TODO, its crashing mostly here
                // get the Response
                resp = (HttpWebResponse)request.EndGetResponse(ar);

                // The server must always return a 200 response code,
                // sending any session errors as specially-formatted identifiers.
                if (resp.StatusCode != HttpStatusCode.OK)
                {
                    //FireOnError(new PollSocketException("unexpected status code " + resp.StatusCode.ToString()));
                    return;
                }
            }
            else
            {
                Console.WriteLine("No response");
            }

            Stream rs = resp.GetResponseStream();

            int readlen;
            byte[] readbuf = new byte[1024];
            MemoryStream ms = new MemoryStream();
            while ((readlen = rs.Read(readbuf, 0, readbuf.Length)) > 0)
            {
                ms.Write(readbuf, 0, readlen);
            }

            byte[] recv = ms.ToArray();

            if (recv.Length > 0)
            {
                string sbody = null;
                string stanzas = null;

                ParseResponse(Encoding.UTF8.GetString(recv), ref sbody, ref stanzas);
                string res = Encoding.UTF8.GetString(recv);

                if (stanzas != null)
                {
                    byte[] bStanzas = Encoding.UTF8.GetBytes(stanzas);                    
                    base.FireOnReceive(bStanzas, bStanzas.Length);                    
                }
                else
                {
                    Console.WriteLine("Empty Response");
                }
            }

            waitingRequests--;

            Console.WriteLine("GOT RESPONSE");
            Console.WriteLine("REQUESTS: " + waitingRequests.ToString());

            if (waitingRequests == 0)
            {
                StartWebRequest();
                
                //TimeSpan ts = DateTime.Now - lastSend;
                //if (ts.TotalMilliseconds < (polling * 1000))
                //    CreatePollTimer((polling * 1000) - (int) ts.TotalMilliseconds);
                //else
                //    StartWebRequest();
            }
        }

        Timer m_PollTimer = null;

        protected void CreatePollTimer(int interval)
        {
            interval += 1000;
            Console.WriteLine("CREATE TIMER: " + interval.ToString());
            // Create the delegate that invokes methods for the timer.
            TimerCallback timerDelegate = new TimerCallback(PollTimerTick);
            //int interval = m_KeepAliveInterval * 1000;
            // Create a timer that waits x seconds, then invokes every x seconds.
            m_PollTimer = new Timer(timerDelegate, null, interval, interval);
        }

        protected void DestroyPollTimer()
        {
            if (m_PollTimer == null)
                return;

            m_PollTimer.Dispose();
            m_PollTimer = null;
        }

        private void PollTimerTick(Object state)
        {
            Console.WriteLine("POLLTIMER TICK");
            DestroyPollTimer();
            StartWebRequest();
        }
    }
}