using System;
using static System.Diagnostics.Debug;
using Gst;
using WebSocketSharp;
using Gst.WebRTC;
using Newtonsoft.Json;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Gst.Sdp;
using System.Text;
using GLib;

namespace GstWebRTCDemo
{
    class WebRtcClient : IDisposable
    {
        const string SERVER = "wss://127.0.0.1:8443";

        const string PIPELINE_DESC = @"webrtcbin name=sendrecv bundle-policy=max-bundle
 videotestsrc is-live=true pattern=ball ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay !
 queue ! application/x-rtp,media=video,encoding-name=VP8,payload=97 ! sendrecv.
 audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay !
 queue ! application/x-rtp,media=audio,encoding-name=OPUS,payload=96 ! sendrecv.";

        readonly int _id;
        readonly int _peerId;
        readonly string _server;
        readonly WebSocket _conn;
        Pipeline pipe;
        Element webrtc;
        bool terminate;

        public WebRtcClient(int id, int peerId, string server = SERVER)
        {
            _id = id;
            _peerId = peerId;
            _server = server;

            _conn = new WebSocket(_server);
            _conn.SslConfiguration.ServerCertificateValidationCallback = validatCert;
            _conn.OnOpen += OnOpen;
            _conn.OnError += OnError;
            _conn.OnMessage += OnMessage;
            _conn.OnClose += OnClose;

            pipe = (Pipeline)Parse.Launch(PIPELINE_DESC);
        }

        bool validatCert(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }

        public void Connect()
        {
            _conn.ConnectAsync();
        }

        void SetupCall()
        {
            _conn.Send($"SESSION {_peerId}");
        }

        void OnClose(object sender, CloseEventArgs e)
        {
            Console.WriteLine("Closed: " + e.Reason);

            terminate = true;
        }

        void OnError(object sender, ErrorEventArgs e)
        {
            Console.WriteLine("Error " + e.Message);

            terminate = true;
        }

        void OnOpen(object sender, System.EventArgs e)
        {
            var ws = sender as WebSocket;
            ws.SendAsync($"HELLO {_id}", (b) => Console.WriteLine($"Opened {b}"));
        }

        void OnMessage(object sender, MessageEventArgs args)
        {
            var msg = args.Data;
            switch (msg)
            {
                case "HELLO":
                    SetupCall();
                    break;
                case "SESSION_OK":
                    StartPipeline();
                    break;
                default:
                    if (msg.StartsWith("ERROR")) {
                        Console.WriteLine(msg);
                        terminate = true;
                    } else {
                        HandleSdp(msg);
                    }
                    break;
            }
        }

        void StartPipeline()
        {
            webrtc = pipe.GetByName("sendrecv");
            Assert(webrtc != null);
            webrtc.Connect("on-negotiation-needed", OnNegotiationNeeded);
            webrtc.Connect("on-ice-candidate", OnIceCandidate);
            webrtc.Connect("pad-added", OnIncomingStream);
            pipe.SetState(State.Playing);
            Console.WriteLine("Playing");
        }

        #region Webrtc signal handlers
        #region Incoming stream
        void OnIncomingStream(object o, GLib.SignalArgs args)
        {
            var pad = args.Args[0] as Pad;
            if (pad.Direction != PadDirection.Src)
                return;
            var decodebin = ElementFactory.Make("decodebin");
            decodebin.Connect("pad-added", OnIncomingDecodebinStream);
            pipe.Add(decodebin);
            decodebin.SyncStateWithParent();
            webrtc.Link(decodebin);
        }

        void OnIncomingDecodebinStream(object o, SignalArgs args)
        {
            var pad = (Pad)args.Args[0];
            if (!pad.HasCurrentCaps)
            {
                Console.WriteLine($"{pad.Name} has no caps, ignoring");
                return;
            }

            var caps = pad.CurrentCaps;
            Assert(!caps.IsEmpty);
            Structure s = caps[0];
            var name = s.Name;
            if (name.StartsWith("video"))
            {
                var q = ElementFactory.Make("queue");
                var conv = ElementFactory.Make("videoconvert");
                var sink = ElementFactory.Make("autovideosink");
                pipe.Add(q, conv, sink);
                pipe.SyncChildrenStates();
                pad.Link(q.GetStaticPad("sink"));
                Element.Link(q, conv, sink);
            }
            else if (name.StartsWith("audio"))
            {
                var q = ElementFactory.Make("queue");
                var conv = ElementFactory.Make("audioconvert");
                var resample = ElementFactory.Make("audioresample");
                var sink = ElementFactory.Make("autoaudiosink");
                pipe.Add(q, conv, resample, sink);
                pipe.SyncChildrenStates();
                pad.Link(q.GetStaticPad("sink"));
                Element.Link(q, conv, resample, sink);
            }

        }
        #endregion

        void OnIceCandidate(object o, GLib.SignalArgs args)
        {
            var index = (uint)args.Args[0];
            var cand = (string)args.Args[1];
            var obj = new { ice = new { sdpMLineIndex = index, candidate = cand } };
            var iceMsg = JsonConvert.SerializeObject(obj);

            _conn.SendAsync(iceMsg, (b) => { } );
        }

        void OnNegotiationNeeded(object o, GLib.SignalArgs args)
        {
            var webRtc = o as Element;
            Assert(webRtc != null, "not a webrtc object");
            Promise promise = new Promise(OnOfferCreated, webrtc.Handle, null); // webRtc.Handle, null);
            Structure structure = new Structure("struct");
            webrtc.Emit("create-offer", structure, promise);
        }

        void OnOfferCreated(Promise promise)
        {
            promise.Wait();
            var reply = promise.RetrieveReply();
            var gval = reply.GetValue("offer");
            WebRTCSessionDescription offer = (WebRTCSessionDescription)gval.Val;
            promise = new Promise();
            webrtc.Emit("set-local-description", offer, promise);
            promise.Interrupt();
            SendSdpOffer(offer) ;
        }
        #endregion

        void SendSdpOffer(WebRTCSessionDescription offer)
        {
            var text = offer.Sdp.AsText();
            var obj = new { sdp = new { type = "offer", sdp = text } };
            var json = JsonConvert.SerializeObject(obj);
            Console.Write(json);

            _conn.SendAsync(json, (b) => Console.WriteLine($"Send offer completed {b}"));
        }

        void HandleSdp(string message)
        {
            var msg = JsonConvert.DeserializeObject<dynamic>(message);

            if (msg.sdp != null)
            {
                var sdp = msg.sdp;
                if (sdp.type != null && sdp.type != "answer")
                {
                    throw new Exception("Not an answer");
                }
                string sdpAns = sdp.sdp;
                Console.WriteLine($"received answer:\n{sdpAns}");
                SDPMessage.New(out SDPMessage sdpMsg);
                SDPMessage.ParseBuffer(ASCIIEncoding.Default.GetBytes(sdpAns), (uint)sdpAns.Length, sdpMsg);
                var answer = WebRTCSessionDescription.New(WebRTCSDPType.Answer, sdpMsg);
                var promise = new Promise();
                webrtc.Emit("set-remote-description", answer, promise);
            }
            else if (msg.ice != null)
            {
                var ice = msg.ice;
                string candidate = ice.candidate;
                uint sdpMLineIndex = ice.sdpMLineIndex;
                webrtc.Emit("add-ice-candidate", sdpMLineIndex, candidate);
            }
        }

        public void Run()
        {
            // Wait until error, EOS or State Change
            var bus = pipe.Bus;
            do {
                var msg = bus.TimedPopFiltered (Gst.Constants.SECOND, MessageType.Error | MessageType.Eos | MessageType.StateChanged);
                // Parse message
                if (msg != null) {
                    switch (msg.Type) {
                    case MessageType.Error:
                        string debug;
                        GLib.GException exc;
                        msg.ParseError (out exc, out debug);
                        Console.WriteLine ("Error received from element {0}: {1}", msg.Src.Name, exc.Message);
                        Console.WriteLine ("Debugging information: {0}", debug != null ? debug : "none");
                        terminate = true;
                        break;
                    case MessageType.Eos:
                        Console.WriteLine ("End-Of-Stream reached.\n");
                        terminate = true;
                        break;
                    case MessageType.StateChanged:
                        // We are only interested in state-changed messages from the pipeline
                        if (msg.Src == pipe) {
                            State oldState, newState, pendingState;
                            msg.ParseStateChanged (out oldState, out newState, out pendingState);
                            Console.WriteLine ("Pipeline state changed from {0} to {1}:",
                                Element.StateGetName (oldState), Element.StateGetName (newState));
                        }
                        break;
                    default:
                        // We should not reach here because we only asked for ERRORs, EOS and STATE_CHANGED
                        Console.WriteLine ("Unexpected message received.");
                        break;
                    }
                }
            } while (!terminate);
        }
        public void Dispose()
        {
            ((IDisposable)_conn).Dispose();
            pipe.SetState(State.Null);
            pipe.Dispose();
        }
    }

    static class WebRtcSendRcv
    {
        const string SERVER = "wss://webrtc.nirbheek.in:8443";
        static Random random = new Random();

        public static void Main(string[] args)
        {
            // Initialize GStreamer
            Gst.Application.Init (ref args);

            if (args.Length == 0)
                throw new Exception("need peerId");
            int peerId = Int32.Parse(args[0]);
            var server = (args.Length > 1) ? args[1] : SERVER;

            var ourId = random.Next(100, 10000);
            Console.WriteLine($"PeerId:{peerId} OurId:{ourId} ");
            var c = new WebRtcClient(ourId, peerId, server);
            c.Connect();
            c.Run();
            c.Dispose();
        }
    }

}
