using System;
using System.Threading;
using System.Net;
using System.Text;
using System.Linq;
using System.Collections.Specialized;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.WebRTC;
using UnityEngine.UI;
using Phoenix;
using DesktopVision.DVSocket;
using Unity.Plastic.Newtonsoft.Json;
using UnityEngine.Assertions;

namespace DesktopVision.DVWebRTC {

/// <summary>
/// The PeerMessage class is used to format messages sent to & from webrtc peers
/// </summary>
public class PeerMessage
{
    /// <summary>
    /// The message description
    /// </summary>
    public RTCSessionDescription description;
    /// <summary>
    /// The Ice candidate
    /// </summary>
    public RTCIceCandidate candidate;
    /// <summary>
    /// The message type
    /// </summary>
    public string type;
}

/// <summary>
/// The Peer class is used to handle a webrtc peer connection
/// It can be either an inititator or a receiver
/// </summary>
public class Peer : IDisposable
{
    private readonly DVConnectionManager parent;
    private readonly GameObject receive;
    private readonly bool polite;
    private readonly RTCPeerConnection peerConnection;
    private VideoStreamTrack videoTrack;
    private AudioStreamTrack audioTrack;
    private DelegateOnOpen onDataChannelOpen;
    private DelegateOnClose onDataChannelClose;
    private DelegateOnDataChannel onDataChannel;
    private DelegateOnMessage onDataChannelMessage;
    private RTCDataChannel dataChannel;
    private RTCRtpTransceiver videoTransceiever, audioReceiver;
    public readonly string uid;
    public readonly bool initiator;
    private bool makingOffer, ignoreOffer;
    private bool srdAnswerPending, sldGetBackStable;
    private bool negotiated, dataChannelOpen;
    private RoomOptions _roomOptions;
    private const int width = 128;
    private const int height = 128;

    /// <summary>
    /// The DVPeer class is used to handle a webrtc peer connection
    /// It can be either an inititator or a receiver
    /// </summary>
    /// <param name="parent">The parent ConnectionManager class</param>
    /// <param name="uid">The peer uid</param>
    /// <param name="initiator">Is the peer an initiator</param>
    /// <param name="receive">The receive object</param>
    /// <param name="roomOptions">The room options</param>
    /// <param name="audio">The audio track</param>
    public Peer(
        DVConnectionManager parent,
        GameObject receive,
        RTCConfiguration config,
        string uid,
        bool initiator,
        RawImage overlay,
        AudioSource audio
        )
    {
        this.receive = receive;
        this.parent = parent;
        this.uid = uid;
        this.initiator = initiator;
        Debug.Log($"CREATE PEER servers: {config.iceServers.Length} {config.iceServers}");
        peerConnection = new RTCPeerConnection(ref config);
        Debug.Log("PEER CREATED " + peerConnection);
        videoTransceiever = peerConnection.AddTransceiver(TrackKind.Video);
        audioReceiver = peerConnection.AddTransceiver(TrackKind.Audio);
        peerConnection.OnTrack = track =>
        {
            Debug.Log($"TRACK ADDED");
            if (track.Track is VideoStreamTrack video)
            {
                try
                {
                    videoTrack.Dispose();
                }
                catch (Exception exception)
                {
                    Debug.Log(exception.Message);
                }
                videoTrack = video;
                video.OnVideoReceived += texture =>
                {
                    Debug.Log("VIDEO ADDED");
                    var renderer = receive.GetComponent<Renderer>();
                    float height = texture.height;
                    float width = texture.width;
                    float aspectRatio = width / height;
                    Debug.Log($"ASPECT RATIO: {aspectRatio}");
                    var originalHeight = receive.transform.localScale.y;
                    var newWidth = originalHeight * aspectRatio;
                    Vector3 newScale = new Vector3(newWidth, originalHeight, 1);
                    receive.transform.localScale = newScale;
                    overlay.texture = texture;
                    Debug.Log($"SET TEXTURE {height} {width}");
                    
                    parent.DispatchEvent(new ConnectionEventArgs { message = "stream-added" });
                };
            }
            else if (track.Track is AudioStreamTrack audioStream)
            {
                audioTrack = audioStream;
                Debug.Log($"Audio added {audioStream.Id}");
                audio.SetTrack(audioStream);
                audio.loop = true;
                audio.Play();
            };
        };

        peerConnection.OnIceCandidate = candidate =>
        {
            var message = new PeerMessage { candidate = candidate };
            Debug.Log($"CANDIDATE ADDED: {candidate.Address}");
            this.parent.PostCandidate(message, this);
        };

        peerConnection.OnNegotiationNeeded = () =>
        {
            Debug.Log("NEGOTIATION NEEDED");
            if (!initiator)
            {
                Debug.Log("PEER EXPECTING SIGNAL");
                peerConnection.OnDataChannel = (RTCDataChannel channel) =>
                {
                    dataChannel = channel;
                    dataChannel.OnMessage = onDataChannelMessage;
                    dataChannel.OnOpen = onDataChannelOpen;
                };
                return;
            }
            else
            {
                Debug.Log("PEER CREATING SIGNAL");
                this.parent.StartCoroutine(NegotiationProcess());
                openDataChannel();
            }
        };

        onDataChannelMessage = bytes =>
        {
            string data = System.Text.Encoding.UTF8.GetString(bytes);
            Debug.Log(data);
        };

        onDataChannelOpen = () =>
        {
            dataChannelOpen = true;
            Debug.Log("DATA CHANNEL OPEN");
        };

    }

    private void openDataChannel()
    {
        Debug.Log("OPEN DATA CHANNEL");
        if (dataChannel != null)
        {
            try
            {
                dataChannel.Close();
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
            }
        }
        RTCDataChannelInit conf = new RTCDataChannelInit();
        dataChannel = peerConnection.CreateDataChannel("data", conf);
        dataChannel.OnMessage = onDataChannelMessage;
        dataChannel.OnOpen = onDataChannelOpen;
    }

    private IEnumerator NegotiationProcess()
    {
        Debug.Log("START NEGOTIATION PROCESS");
        yield return new WaitWhile(() => sldGetBackStable);
        Assert.AreEqual(peerConnection.SignalingState, RTCSignalingState.Stable,
            $"{this} negotiationneeded always fires in stable state");
        Assert.AreEqual(makingOffer, false, $"{this} negotiationneeded not already in progress");

        makingOffer = true;
        RTCSessionDescriptionAsyncOperation offer = peerConnection.CreateOffer();
        yield return offer;

        RTCSessionDescription localDescription = offer.Desc;
        var op = peerConnection.SetLocalDescription(ref localDescription);
        yield return op;

        if (op.IsError)
        {
            Debug.LogError($"{this} {op.Error.message}");
            makingOffer = false;
            yield break;
        }

        Assert.AreEqual(peerConnection.SignalingState, RTCSignalingState.HaveLocalOffer,
            $"{this} negotiationneeded always fires in stable state");
        Assert.AreEqual(peerConnection.LocalDescription.type, RTCSdpType.Offer, $"{this} negotiationneeded SLD worked");
        makingOffer = false;
        Debug.Log("OFFER CREATED");
        var message = new PeerMessage { description = peerConnection.LocalDescription, type = "offer" };
        this.parent.PostMessage(message, this);
        this.negotiated = true;
    }

    /// <summary>
    /// The add a video transceiver to the peer connection
    /// </summary>
    public void addTransceiver()
    {
        if (videoTrack == null)
        {
            audioReceiver = peerConnection.AddTransceiver(TrackKind.Audio);
            videoTransceiever = peerConnection.AddTransceiver(TrackKind.Video);
            return;
        }
        else
        {
            try
            {
                if (videoTransceiever != null)
                {
                    videoTransceiever.Dispose();
                    audioReceiver.Dispose();
                    videoTransceiever = null;
                    audioReceiver = null;
                };
            }
            catch (Exception e)
            {
                Debug.Log("ERROR REMOVING TRANSCEIVER" + e.Message);
            }
            try
            {
                audioReceiver = peerConnection.AddTransceiver(audioTrack);
                videoTransceiever = peerConnection.AddTransceiver(videoTrack);
            }
            catch (Exception e)
            {
                Debug.Log("ERROR ADDING TRANSCEIVER" + e.Message);
            }
        }
    }

    /// <summary>
    /// Create offer if this peer should be the intitiator
    /// </summary>
    public void createOffer()
    {
        Debug.Log("CREATE OFFER");
        this.parent.StartCoroutine(NegotiationProcess());
    }
    void OnDestroy()
    {
        Dispose();
    }

    /// <summary>
    /// Dispose of the peer connection
    /// </summary>
    public void Dispose()
    {
        Debug.Log("DISPOSE PEER");
        try
        {
            dataChannel.Close();
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message);
        }
        try
        {
            videoTrack.Dispose();
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message);
        }
        try
        {
            peerConnection.Dispose();
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message);
        }
    }

    /// <summary>
    /// Send a message from the peer
    /// Open a datachannel if one does not already exist
    /// </summary>
    public void SendMsg(string msg)
    {
        try
        {
            dataChannel.Send($"{msg}");
            dataChannel.Send("data");
            Debug.Log($"SENT: {msg} DATAC: {dataChannel.Label}");
        }
        catch (Exception e)
        {
            if (e.Message == "DataChannel is not open")
            {
                openDataChannel();
            }
        }
    }

    /// <summary>
    /// Handle receiving a peer message
    /// </summary>
    public void OnMessage(PeerMessage message)
    {
        if (message.candidate != null)
        {
            try
            {
                Debug.Log($"CANDIDATE RECEIEVED: {message.candidate.Address}");
                Debug.Log($"CANDIDATE RECEIEVED: {message.candidate.SdpMid}");
                Debug.Log($"CANDIDATE RECEIEVED: {message.candidate.SdpMLineIndex}");
                peerConnection.AddIceCandidate(message.candidate);
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
            }
            return;
        }
        parent.StartCoroutine(OfferAnswerProcess(message.description));
    }

    private IEnumerator OfferAnswerProcess(RTCSessionDescription description)
    {
        var isStable =
            peerConnection.SignalingState == RTCSignalingState.Stable ||
            (peerConnection.SignalingState == RTCSignalingState.HaveLocalOffer && srdAnswerPending);
        ignoreOffer =
            description.type == RTCSdpType.Offer && !polite && (makingOffer || !isStable);
        if (ignoreOffer)
        {
            Debug.Log($"{this} glare - ignoring offer");
            yield break;
        }

        yield return new WaitWhile(() => makingOffer);

        srdAnswerPending = description.type == RTCSdpType.Answer;
        Debug.Log($"OFFER/ANSWER PROCESS: {description.type} {this.uid}");
        var op1 = peerConnection.SetRemoteDescription(ref description);
        yield return op1;
        Assert.IsFalse(op1.IsError, $"{this} {op1.Error.message}");
        srdAnswerPending = false;
        if (description.type == RTCSdpType.Offer)
        {
            Assert.AreEqual(peerConnection.RemoteDescription.type, RTCSdpType.Offer, $"{this} SRD worked");
            Assert.AreEqual(peerConnection.SignalingState, RTCSignalingState.HaveRemoteOffer, $"{this} Remote offer");
            Debug.Log($"{this} SLD to get back to stable");
            sldGetBackStable = true;

            var op2 = peerConnection.SetLocalDescription();
            yield return op2;
            Assert.IsFalse(op2.IsError, $"{this} {op2.Error.message}");

            Assert.AreEqual(peerConnection.LocalDescription.type, RTCSdpType.Answer, $"{this} onmessage SLD worked");
            Assert.AreEqual(peerConnection.SignalingState, RTCSignalingState.Stable,
                $"{this} onmessage not racing with negotiationneeded");
            sldGetBackStable = false;

            var message = new PeerMessage { description = peerConnection.LocalDescription, type = "answer" };
            Debug.Log("CREATED ANSWER");
            this.parent.PostMessage(message, this);
        }
        else
        {
            Assert.AreEqual(peerConnection.RemoteDescription.type, RTCSdpType.Answer, $"{this} Answer was set");
            Assert.AreEqual(peerConnection.SignalingState, RTCSignalingState.Stable, $"{this} answered");
        }

        Debug.Log("COMPLETE OFFER ANSWER PROCESS");
        Debug.Log($"LTYPE: {peerConnection.LocalDescription.type} \n RTYPE: {peerConnection.RemoteDescription.type}");
        Debug.Log($"LSDP: {peerConnection.LocalDescription.sdp} \n RSDP: {peerConnection.RemoteDescription.sdp}");
        WebRTC.Update();
        this.negotiated = true;
    }
}

static class WebRTCSettings
{
    private static bool s_enableHWCodec = false;
    private static bool s_limitTextureSize = true;

    public static bool EnableHWCodec
    {
        get { return s_enableHWCodec; }
        set { s_enableHWCodec = value; }
    }

    public static bool LimitTextureSize
    {
        get { return s_limitTextureSize; }
        set { s_limitTextureSize = value; }
    }

}

}