package com.omixlab.lckcontrol.p2p.webrtc

import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import org.webrtc.*
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class WebRtcPeerManager @Inject constructor(
    @ApplicationContext private val context: Context,
) {
    companion object {
        private const val TAG = "WebRtcPeerManager"
    }

    private var peerConnectionFactory: PeerConnectionFactory? = null
    private var peerConnection: PeerConnection? = null
    private var controlChannel: DataChannel? = null
    private var fileChannel: DataChannel? = null
    private var eglBase: EglBase? = null

    var onControlMessage: ((String) -> Unit)? = null
    var onFileData: ((ByteArray) -> Unit)? = null

    fun initialize() {
        PeerConnectionFactory.initialize(
            PeerConnectionFactory.InitializationOptions.builder(context)
                .setEnableInternalTracer(false)
                .createInitializationOptions()
        )

        eglBase = EglBase.create()

        peerConnectionFactory = PeerConnectionFactory.builder()
            .setVideoEncoderFactory(DefaultVideoEncoderFactory(eglBase!!.eglBaseContext, true, true))
            .setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBase!!.eglBaseContext))
            .createPeerConnectionFactory()
    }

    fun handleOffer(
        offerSdp: String,
        remoteCandidates: List<IceCandidateDto>,
    ): OfferResponseBody? {
        val config = PeerConnection.RTCConfiguration(emptyList()).apply {
            sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
        }

        var answerSdp: SessionDescription? = null
        val localCandidates = mutableListOf<IceCandidate>()

        peerConnection = peerConnectionFactory?.createPeerConnection(config, object : PeerConnection.Observer {
            override fun onSignalingChange(state: PeerConnection.SignalingState?) {}
            override fun onIceConnectionChange(state: PeerConnection.IceConnectionState?) {
                Log.d(TAG, "ICE state: $state")
            }
            override fun onIceConnectionReceivingChange(receiving: Boolean) {}
            override fun onIceGatheringChange(state: PeerConnection.IceGatheringState?) {}
            override fun onIceCandidate(candidate: IceCandidate?) {
                candidate?.let { localCandidates.add(it) }
            }
            override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>?) {}
            override fun onAddStream(stream: MediaStream?) {}
            override fun onRemoveStream(stream: MediaStream?) {}
            override fun onDataChannel(dc: DataChannel?) {
                dc ?: return
                when (dc.label()) {
                    "control" -> {
                        controlChannel = dc
                        dc.registerObserver(createControlObserver())
                    }
                    "files" -> {
                        fileChannel = dc
                        dc.registerObserver(createFileObserver())
                    }
                }
            }
            override fun onRenegotiationNeeded() {}
            override fun onAddTrack(receiver: RtpReceiver?, streams: Array<out MediaStream>?) {}
        })

        // Set remote offer
        val offer = SessionDescription(SessionDescription.Type.OFFER, offerSdp)
        val setRemoteLatch = java.util.concurrent.CountDownLatch(1)
        peerConnection?.setRemoteDescription(object : SdpObserver {
            override fun onCreateSuccess(p0: SessionDescription?) {}
            override fun onSetSuccess() { setRemoteLatch.countDown() }
            override fun onCreateFailure(p0: String?) { setRemoteLatch.countDown() }
            override fun onSetFailure(p0: String?) { setRemoteLatch.countDown() }
        }, offer)

        if (!setRemoteLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) {
            return null
        }

        // Add remote ICE candidates
        for (candidate in remoteCandidates) {
            peerConnection?.addIceCandidate(
                IceCandidate(candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp)
            )
        }

        // Create answer
        val answerLatch = java.util.concurrent.CountDownLatch(1)
        peerConnection?.createAnswer(object : SdpObserver {
            override fun onCreateSuccess(sdp: SessionDescription?) {
                sdp?.let { answer ->
                    answerSdp = answer
                    peerConnection?.setLocalDescription(object : SdpObserver {
                        override fun onCreateSuccess(p0: SessionDescription?) {}
                        override fun onSetSuccess() { answerLatch.countDown() }
                        override fun onCreateFailure(p0: String?) { answerLatch.countDown() }
                        override fun onSetFailure(p0: String?) { answerLatch.countDown() }
                    }, answer)
                }
            }
            override fun onSetSuccess() {}
            override fun onCreateFailure(error: String?) {
                Log.e(TAG, "Create answer failed: $error")
                answerLatch.countDown()
            }
            override fun onSetFailure(error: String?) { answerLatch.countDown() }
        }, MediaConstraints())

        if (!answerLatch.await(5, java.util.concurrent.TimeUnit.SECONDS) || answerSdp == null) {
            return null
        }

        // Wait briefly for ICE candidates
        Thread.sleep(500)

        return OfferResponseBody(
            sdp = answerSdp!!.description,
            type = "answer",
            iceCandidates = localCandidates.map {
                IceCandidateDto(it.sdpMid, it.sdpMLineIndex, it.sdp)
            },
        )
    }

    fun sendControlMessage(json: String) {
        controlChannel?.send(DataChannel.Buffer(
            java.nio.ByteBuffer.wrap(json.toByteArray()),
            false,
        ))
    }

    fun sendFileData(data: ByteArray) {
        fileChannel?.send(DataChannel.Buffer(
            java.nio.ByteBuffer.wrap(data),
            true,
        ))
    }

    fun addVideoTrack(videoTrack: VideoTrack) {
        peerConnection?.addTrack(videoTrack)
    }

    fun disconnect() {
        controlChannel?.close()
        fileChannel?.close()
        peerConnection?.close()
        peerConnection = null
        controlChannel = null
        fileChannel = null
    }

    fun release() {
        disconnect()
        peerConnectionFactory?.dispose()
        peerConnectionFactory = null
        eglBase?.release()
        eglBase = null
    }

    private fun createControlObserver() = object : DataChannel.Observer {
        override fun onBufferedAmountChange(previous: Long) {}
        override fun onStateChange() {}
        override fun onMessage(buffer: DataChannel.Buffer?) {
            buffer?.let {
                val bytes = ByteArray(it.data.remaining())
                it.data.get(bytes)
                onControlMessage?.invoke(String(bytes))
            }
        }
    }

    private fun createFileObserver() = object : DataChannel.Observer {
        override fun onBufferedAmountChange(previous: Long) {}
        override fun onStateChange() {}
        override fun onMessage(buffer: DataChannel.Buffer?) {
            buffer?.let {
                val bytes = ByteArray(it.data.remaining())
                it.data.get(bytes)
                onFileData?.invoke(bytes)
            }
        }
    }
}
