import ast
import json
import logging
import platform
from aiortc import VideoStreamTrack
from av import VideoFrame
# import pyaudio as pyaudio
import socketio
from aiortc import RTCPeerConnection, RTCRtpSender, RTCConfiguration, RTCIceServer, RTCSessionDescription
from aiortc.contrib.media import MediaPlayer, MediaRelay, MediaBlackhole

import asyncio
import collections
import time

pcs = set()
relay = None
webcam = None
import numpy as np

# p = pyaudio.PyAudio()
# stream = p.open(format=pyaudio.paFloat32,
#                 channels=1,
#                 rate=48000,
#                 output=True)

video_tracklist = []


class FPS:
    def __init__(self, avarageof=50):
        self.frametimestamps = collections.deque(maxlen=avarageof)

    def __call__(self):
        self.frametimestamps.append(time.time())
        if len(self.frametimestamps) > 1:
            return len(self.frametimestamps) / (self.frametimestamps[-1] - self.frametimestamps[0])
        else:
            return 0.0


async def checkTerminatorQueue(opqueue):
    while True:

        data = await opqueue.get()
        if data is not None:
            for track in video_tracklist:
                await track.write(data, 0)


class TerminatorStreamTrack(VideoStreamTrack):
    """
    A video track that returns an animated flag.
    """

    def __init__(self):
        super().__init__()  # don't forget this!
        self.counter = 0
        height, width = 480, 640
        self.framerate = 0
        self.nal_queue = asyncio.Queue(3)

    async def write(self, frame, framerate=0):
        self.framerate = framerate
        if not self.nal_queue.full():
            await self.nal_queue.put(frame)

    async def recv(self):
        pts, time_base = await self.next_timestamp()

        frame = VideoFrame.from_ndarray(await self.nal_queue.get())
        frame.pts = pts
        frame.time_base = time_base
        self.counter += 1
        return frame


def create_local_tracks(play_from, decode, device, rtbufsize):
    global relay, webcam

    if play_from:
        player = MediaPlayer(play_from, decode=decode)
        return player.audio, player.video
    else:
        options = {"framerate": "30", "video_size": "640x480", "rtbufsize": rtbufsize}
        if relay is None:
            if platform.system() == "Darwin":
                webcam = MediaPlayer(
                    "default:none", format="avfoundation", options=options
                )
            elif platform.system() == "Windows":
                webcam = MediaPlayer(
                    f"video={device}", format="dshow", options=options
                )
            else:
                webcam = MediaPlayer(f"{device}", format="v4l2", options=options)
            relay = MediaRelay()
        return None, relay.subscribe(webcam.video)


def force_codec(pc, sender, forced_codec):
    kind = forced_codec.split("/")[0]
    codecs = RTCRtpSender.getCapabilities(kind).codecs
    transceiver = next(t for t in pc.getTransceivers() if t.sender == sender)
    transceiver.setCodecPreferences(
        [codec for codec in codecs if codec.mimeType == forced_codec]
    )


# class MicStreamTrack(MediaStreamTrack):
#     """
#     An audio stream object for the mic audio from the client
#     """
#     kind = "audio"
#
#     def __init__(self, track):
#         super().__init__()
#         self.track = track
#         self._logger = logging.getLogger(__name__)
#
#     async def recv(self):
#         self._logger.error("mic recv()")
#
#         # Get a new PyAV frame
#         frame = await self.track.recv()
#
#         # Convert to float32 numpy array
#         floatArray = frame.to_ndarray(format="float32")
#         samples = np.sin(np.arange(50000) / 20)
#         # stream.write(floatArray.tostring())
#         stream.write(samples.astype(np.float32).tostring())
#
#         # Put these samples into the mic queue
#         # micSampleQueue.put_nowait(floatArray)
#
#         self._logger.error("Put {} samples to mic queue".format(len(floatArray)))


class GSPeerConnectionBroadcasterUniversal:

    @classmethod
    async def create(cls, gsdbs, target, videostream=True, addChannel=None, oncommand=None):
        self = GSPeerConnectionBroadcasterUniversal()
        self.rtconfiList = []
        self.gsdbs = gsdbs
        self.target = target
        self.videostream = videostream
        self.oncommand = oncommand
        self.addChannel = addChannel

        if self.gsdbs.credentials["stunenable"]:
            self.rtconfiList.append(RTCIceServer(self.gsdbs.credentials["stunserver"]))
        if self.gsdbs.credentials["turnenable"]:
            self.rtconfiList.append(RTCIceServer(urls=self.gsdbs.credentials["turnserver"],
                                                 username=self.gsdbs.credentials["turnuser"],
                                                 credential=self.gsdbs.credentials["turnpw"]
                                                 ))

        self.sio = socketio.AsyncClient()
        self.peerConnections = {}
        self._logger = logging.getLogger(__name__)
        self.webcam = None
        self.relay = None

        @self.sio.event
        async def connect():
            self._logger.info('connection established')

        @self.sio.event
        async def joined(id):
            await self.sio.emit("broadcaster", id)

        @self.sio.event
        async def watcher(id, description):
            if type(description) == str:
                description = ast.literal_eval(description)
            desc = type('new_dict', (object,), description)

            offer = RTCSessionDescription(sdp=description["sdp"], type=description["type"])

            if len(self.rtconfiList) > 0:
                pc = RTCPeerConnection(configuration=RTCConfiguration(self.rtconfiList))
            else:
                pc = RTCPeerConnection()

            pcs.add(pc)

            @pc.on("connectionstatechange")
            async def on_connectionstatechange():
                print("Connection state is %s" % pc.connectionState)
                if pc.connectionState == "failed":
                    await pc.close()
                    pcs.discard(pc)

            @pc.on("track")
            async def on_track(track):
                if track.kind == "video":
                    mediablackhole = MediaBlackhole()
                    mediablackhole.addTrack(track)

            if self.addChannel is not None:
                sendchannel = pc.createDataChannel("terminatorview")
                self.addChannel(sendchannel)

            if videostream:
                audio, video = create_local_tracks(False,
                                                   decode=not True,
                                                   device=self.gsdbs.credentials["webcam"],
                                                   rtbufsize=self.gsdbs.credentials["rtbufsize"])
                if video:
                    video_sender = pc.addTrack(video)
                    force_codec(pc, video_sender, "video/H264")

            await pc.setRemoteDescription(offer)

            answer = await pc.createAnswer()
            await pc.setLocalDescription(answer)

            await self.sio.emit("answer", {"id": id,
                                           "message": json.dumps(
                                               {"type": pc.localDescription.type,
                                                "sdp": pc.localDescription.sdp})})

        @self.sio.event
        async def command(commandcall):
            self.oncommand(self.gsdbs, commandcall)

        @self.sio.event
        async def disconnectPeer(id):
            if id in self.peerConnections:
                await self.peerConnections[id].close()
                self.peerConnections.pop(id, None)

        @self.sio.event
        async def disconnect():
            self._logger.info('disconnected from server')

        connectURL = ""

        if "localhost" in self.gsdbs.credentials["signalserver"]:
            connectURL = f'{self.gsdbs.credentials["signalserver"]}:{str(self.gsdbs.credentials["signalport"])}'
        else:
            connectURL = self.gsdbs.credentials["signalserver"]

        await self.sio.connect(
            f'{connectURL}?gssession={self.gsdbs.cookiejar.get("session")}.{self.gsdbs.cookiejar.get("signature")}{self.target}')
        await self.sio.wait()


def channel_log(logger, channel, t, message):
    logger.info("channel(%s) %s %s" % (channel.label, t, message))


def channel_send(logger, channel, message):
    channel_log(logger, channel, ">", message)
    channel.send(message)


class GSPeerConnectionBroadcasterUniversalNew:

    @classmethod
    async def create(cls,
                     gsdbs,
                     target,
                     videostream=True,
                     addChannel=None,
                     oncommand=None,
                     videotrack=None,
                     signalingserver=None
                     ):
        self = GSPeerConnectionBroadcasterUniversalNew()
        self.addChannel = addChannel
        self.rtconfiList = []
        self.target = target
        self.gsdbs = gsdbs
        self.videostream = videostream
        self.oncommand = oncommand
        self.videotrack = videotrack
        self.signalingserver = signalingserver

        if self.gsdbs.credentials["stunenable"]:
            self.rtconfiList.append(RTCIceServer(self.gsdbs.credentials["stunserver"]))
        if self.gsdbs.credentials["turnenable"]:
            self.rtconfiList.append(RTCIceServer(urls=self.gsdbs.credentials["turnserver"],
                                                 username=self.gsdbs.credentials["turnuser"],
                                                 credential=self.gsdbs.credentials["turnpw"]
                                                 ))

        self.sio = socketio.AsyncClient()
        self.peerConnections = {}
        self._logger = logging.getLogger(__name__)
        self.webcam = None
        self.relay = None

        @self.sio.event
        async def connect():
            self._logger.info('connection established')

        @self.sio.event
        async def wjoined(id):
            await self.sio.emit("joined", self.sio.sid)

        @self.sio.event
        async def broadcaster(id, description, emitter):

            if len(self.rtconfiList) > 0:
                pc = RTCPeerConnection(configuration=RTCConfiguration(self.rtconfiList))
            else:
                pc = RTCPeerConnection()
            self.peerConnections[id] = pc

            @pc.on("track")
            async def on_track(track):
                if track.kind == "video":
                    mediablackhole = MediaBlackhole()
                    mediablackhole.addTrack(track)
                # if track.kind == "audio":
                #     micTrack = MicStreamTrack(track)
                #     blackHole = MediaBlackhole()
                #     blackHole.addTrack(micTrack)
                #     await blackHole.start()

            async def on_iceconnectionstatechange():
                print("ICE connection state is ", self.peerConnections[id].iceConnectionState)
                if self.peerConnections[id].iceConnectionState == "failed":
                    await self.peerConnections[id].close()
                if self.peerConnections[id].iceConnectionState == "completed":
                    print("completed")

            self.peerConnections[id]._add_event_handler("iceconnectionstatechange", on_iceconnectionstatechange,
                                                        on_iceconnectionstatechange)

            @pc.on("signalingstatechange")
            async def signalingchange():
                print("change" + pc.signalingState)

            if self.videostream:
                audio, video = create_local_tracks(False,
                                                   decode=not True,
                                                   device=self.gsdbs.credentials["webcam"],
                                                   rtbufsize=self.gsdbs.credentials["rtbufsize"])
                if video:
                    video_sender = pc.addTrack(video)
                    force_codec(pc, video_sender, "video/H264")
            if self.videotrack:
                terminatorviewtrack = TerminatorStreamTrack()
                video_tracklist.append(terminatorviewtrack)
                pc.addTrack(terminatorviewtrack)

            # if self.addChannel is not None:
            #     sendchannel = pc.createDataChannel("terminatorview")
            #     self.addChannel(sendchannel)
            #     channel_log(self._logger, sendchannel, "-", "created by local party")
            #
            #     @sendchannel.on("open")
            #     def on_open():
            #         self._logger.info("datachannel open")

            # @sendchannel.on("message")
            # def on_message(message):
            #     def subprocess_cmd(command):
            #         process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
            #         proc_stdout = process.communicate()[0].strip()
            #         # for line in proc_stdout.decode().split('\n'):
            #         channel_send(self._logger, sendchannel, json.dumps({"response": proc_stdout.decode()}))
            #
            #     channel_log(self._logger, sendchannel, "<", message)
            #     if isinstance(message, str) and message.startswith("ping"):
            #         # pass
            #         # reply
            #         channel_send(self._logger, sendchannel, f'{{"ping":"pong","ts":"{datetime.utcnow()} UTC"}}')
            #     if isinstance(message, str) and message.startswith('{"type'):
            #         # reply
            #         self.oncommand(self.gsdbs, json.loads(message))
            #     if isinstance(message, str) and message.startswith('{"command'):
            #         subprocess_cmd(json.loads(message)["command"])

            await pc.setLocalDescription(await pc.createOffer())
            await self.sio.emit("watcher", {"target": description,
                                            "id": id,
                                            "sdp": json.dumps(
                                                {"type": pc.localDescription.type,
                                                 "sdp": pc.localDescription.sdp})})

        @self.sio.event
        async def answer(id, description):
            if id in self.peerConnections:
                if self.peerConnections[id].signalingState != "stable":
                    if type(description) == str:
                        description = ast.literal_eval(description)
                    desc = type('new_dict', (object,), description)
                    answer = RTCSessionDescription(sdp=description["sdp"], type=description["type"])
                    await self.peerConnections[id].setRemoteDescription(answer)

        @self.sio.event
        async def disconnectBroadcaster(id, desc):
            if id in self.peerConnections:
                self._logger.info("Disconnected Peer")
                await self.peerConnections[id].close()
                self.peerConnections.pop(id, None)

        @self.sio.event
        async def disconnect():
            self._logger.info('disconnected from server')

        connectURL = ""

        if self.signalingserver is not None:
            connectURL = self.signalingserver

        else:
            if "localhost" in self.gsdbs.credentials["signalserver"]:
                connectURL = f'{self.gsdbs.credentials["signalserver"]}:{str(self.gsdbs.credentials["signalport"])}'
            else:
                connectURL = self.gsdbs.credentials["signalserver"]

        await self.sio.connect(
            f'{connectURL}?gssession={self.gsdbs.cookiejar.get("session")}.{self.gsdbs.cookiejar.get("signature")}&target={self.target}&terminator=true')
        await self.sio.wait()


def createTerminatorView(gsdbs, aioqueue: asyncio.Queue, target, signalingserver=None
                         ):
    return asyncio.gather(checkTerminatorQueue(aioqueue),
                          GSPeerConnectionBroadcasterUniversalNew.create(gsdbs=gsdbs,
                                                                         target=target + "terminatorview",
                                                                         videostream=False,
                                                                         videotrack=True,
                                                                         signalingserver=signalingserver
                                                                         ))
