// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

//go:build js && wasm
// +build js,wasm

// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
package webrtc

import (
	"syscall/js"

	"github.com/pion/ice/v2"
	"github.com/pion/webrtc/v3/pkg/rtcerr"
)

// PeerConnection represents a WebRTC connection that establishes a
// peer-to-peer communications with another PeerConnection instance in a
// browser, or to another endpoint implementing the required protocols.
type PeerConnection struct {
	// Pointer to the underlying JavaScript RTCPeerConnection object.
	underlying js.Value

	// Keep track of handlers/callbacks so we can call Release as required by the
	// syscall/js API. Initially nil.
	onSignalingStateChangeHandler     *js.Func
	onDataChannelHandler              *js.Func
	onNegotiationNeededHandler        *js.Func
	onConnectionStateChangeHandler    *js.Func
	onICEConnectionStateChangeHandler *js.Func
	onICECandidateHandler             *js.Func
	onICEGatheringStateChangeHandler  *js.Func

	// Used by GatheringCompletePromise
	onGatherCompleteHandler func()

	// A reference to the associated API state used by this connection
	api *API
}

// NewPeerConnection creates a peerconnection.
func NewPeerConnection(configuration Configuration) (*PeerConnection, error) {
	api := NewAPI()
	return api.NewPeerConnection(configuration)
}

// NewPeerConnection creates a new PeerConnection with the provided configuration against the received API object
func (api *API) NewPeerConnection(configuration Configuration) (_ *PeerConnection, err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	configMap := configurationToValue(configuration)
	underlying := js.Global().Get("window").Get("RTCPeerConnection").New(configMap)
	return &PeerConnection{
		underlying: underlying,
		api:        api,
	}, nil
}

// JSValue returns the underlying PeerConnection
func (pc *PeerConnection) JSValue() js.Value {
	return pc.underlying
}

// OnSignalingStateChange sets an event handler which is invoked when the
// peer connection's signaling state changes
func (pc *PeerConnection) OnSignalingStateChange(f func(SignalingState)) {
	if pc.onSignalingStateChangeHandler != nil {
		oldHandler := pc.onSignalingStateChangeHandler
		defer oldHandler.Release()
	}
	onSignalingStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		state := newSignalingState(args[0].String())
		go f(state)
		return js.Undefined()
	})
	pc.onSignalingStateChangeHandler = &onSignalingStateChangeHandler
	pc.underlying.Set("onsignalingstatechange", onSignalingStateChangeHandler)
}

// OnDataChannel sets an event handler which is invoked when a data
// channel message arrives from a remote peer.
func (pc *PeerConnection) OnDataChannel(f func(*DataChannel)) {
	if pc.onDataChannelHandler != nil {
		oldHandler := pc.onDataChannelHandler
		defer oldHandler.Release()
	}
	onDataChannelHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		// pion/webrtc/projects/15
		// This reference to the underlying DataChannel doesn't know
		// about any other references to the same DataChannel. This might result in
		// memory leaks where we don't clean up handler functions. Could possibly fix
		// by keeping a mutex-protected list of all DataChannel references as a
		// property of this PeerConnection, but at the cost of additional overhead.
		dataChannel := &DataChannel{
			underlying: args[0].Get("channel"),
			api:        pc.api,
		}
		go f(dataChannel)
		return js.Undefined()
	})
	pc.onDataChannelHandler = &onDataChannelHandler
	pc.underlying.Set("ondatachannel", onDataChannelHandler)
}

// OnNegotiationNeeded sets an event handler which is invoked when
// a change has occurred which requires session negotiation
func (pc *PeerConnection) OnNegotiationNeeded(f func()) {
	if pc.onNegotiationNeededHandler != nil {
		oldHandler := pc.onNegotiationNeededHandler
		defer oldHandler.Release()
	}
	onNegotiationNeededHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		go f()
		return js.Undefined()
	})
	pc.onNegotiationNeededHandler = &onNegotiationNeededHandler
	pc.underlying.Set("onnegotiationneeded", onNegotiationNeededHandler)
}

// OnICEConnectionStateChange sets an event handler which is called
// when an ICE connection state is changed.
func (pc *PeerConnection) OnICEConnectionStateChange(f func(ICEConnectionState)) {
	if pc.onICEConnectionStateChangeHandler != nil {
		oldHandler := pc.onICEConnectionStateChangeHandler
		defer oldHandler.Release()
	}
	onICEConnectionStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		connectionState := NewICEConnectionState(pc.underlying.Get("iceConnectionState").String())
		go f(connectionState)
		return js.Undefined()
	})
	pc.onICEConnectionStateChangeHandler = &onICEConnectionStateChangeHandler
	pc.underlying.Set("oniceconnectionstatechange", onICEConnectionStateChangeHandler)
}

// OnConnectionStateChange sets an event handler which is called
// when an PeerConnectionState is changed.
func (pc *PeerConnection) OnConnectionStateChange(f func(PeerConnectionState)) {
	if pc.onConnectionStateChangeHandler != nil {
		oldHandler := pc.onConnectionStateChangeHandler
		defer oldHandler.Release()
	}
	onConnectionStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		connectionState := newPeerConnectionState(pc.underlying.Get("connectionState").String())
		go f(connectionState)
		return js.Undefined()
	})
	pc.onConnectionStateChangeHandler = &onConnectionStateChangeHandler
	pc.underlying.Set("onconnectionstatechange", onConnectionStateChangeHandler)
}

func (pc *PeerConnection) checkConfiguration(configuration Configuration) error {
	// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2)
	if pc.ConnectionState() == PeerConnectionStateClosed {
		return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
	}

	existingConfig := pc.GetConfiguration()
	// https://www.w3.org/TR/webrtc/#set-the-configuration (step #3)
	if configuration.PeerIdentity != "" {
		if configuration.PeerIdentity != existingConfig.PeerIdentity {
			return &rtcerr.InvalidModificationError{Err: ErrModifyingPeerIdentity}
		}
	}

	// https://github.com/pion/webrtc/issues/513
	// https://www.w3.org/TR/webrtc/#set-the-configuration (step #4)
	// if len(configuration.Certificates) > 0 {
	// 	if len(configuration.Certificates) != len(existingConfiguration.Certificates) {
	// 		return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}
	// 	}

	// 	for i, certificate := range configuration.Certificates {
	// 		if !pc.configuration.Certificates[i].Equals(certificate) {
	// 			return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}
	// 		}
	// 	}
	// 	pc.configuration.Certificates = configuration.Certificates
	// }

	// https://www.w3.org/TR/webrtc/#set-the-configuration (step #5)
	if configuration.BundlePolicy != BundlePolicy(Unknown) {
		if configuration.BundlePolicy != existingConfig.BundlePolicy {
			return &rtcerr.InvalidModificationError{Err: ErrModifyingBundlePolicy}
		}
	}

	// https://www.w3.org/TR/webrtc/#set-the-configuration (step #6)
	if configuration.RTCPMuxPolicy != RTCPMuxPolicy(Unknown) {
		if configuration.RTCPMuxPolicy != existingConfig.RTCPMuxPolicy {
			return &rtcerr.InvalidModificationError{Err: ErrModifyingRTCPMuxPolicy}
		}
	}

	// https://www.w3.org/TR/webrtc/#set-the-configuration (step #7)
	if configuration.ICECandidatePoolSize != 0 {
		if configuration.ICECandidatePoolSize != existingConfig.ICECandidatePoolSize &&
			pc.LocalDescription() != nil {
			return &rtcerr.InvalidModificationError{Err: ErrModifyingICECandidatePoolSize}
		}
	}

	// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11)
	if len(configuration.ICEServers) > 0 {
		// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3)
		for _, server := range configuration.ICEServers {
			if _, err := server.validate(); err != nil {
				return err
			}
		}
	}
	return nil
}

// SetConfiguration updates the configuration of this PeerConnection object.
func (pc *PeerConnection) SetConfiguration(configuration Configuration) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	if err := pc.checkConfiguration(configuration); err != nil {
		return err
	}
	configMap := configurationToValue(configuration)
	pc.underlying.Call("setConfiguration", configMap)
	return nil
}

// GetConfiguration returns a Configuration object representing the current
// configuration of this PeerConnection object. The returned object is a
// copy and direct mutation on it will not take affect until SetConfiguration
// has been called with Configuration passed as its only argument.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-getconfiguration
func (pc *PeerConnection) GetConfiguration() Configuration {
	return valueToConfiguration(pc.underlying.Call("getConfiguration"))
}

// CreateOffer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateOffer(options *OfferOptions) (_ SessionDescription, err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	promise := pc.underlying.Call("createOffer", offerOptionsToValue(options))
	desc, err := awaitPromise(promise)
	if err != nil {
		return SessionDescription{}, err
	}
	return *valueToSessionDescription(desc), nil
}

// CreateAnswer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (_ SessionDescription, err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	promise := pc.underlying.Call("createAnswer", answerOptionsToValue(options))
	desc, err := awaitPromise(promise)
	if err != nil {
		return SessionDescription{}, err
	}
	return *valueToSessionDescription(desc), nil
}

// SetLocalDescription sets the SessionDescription of the local peer
func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	promise := pc.underlying.Call("setLocalDescription", sessionDescriptionToValue(&desc))
	_, err = awaitPromise(promise)
	return err
}

// LocalDescription returns PendingLocalDescription if it is not null and
// otherwise it returns CurrentLocalDescription. This property is used to
// determine if setLocalDescription has already been called.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-localdescription
func (pc *PeerConnection) LocalDescription() *SessionDescription {
	return valueToSessionDescription(pc.underlying.Get("localDescription"))
}

// SetRemoteDescription sets the SessionDescription of the remote peer
func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	promise := pc.underlying.Call("setRemoteDescription", sessionDescriptionToValue(&desc))
	_, err = awaitPromise(promise)
	return err
}

// RemoteDescription returns PendingRemoteDescription if it is not null and
// otherwise it returns CurrentRemoteDescription. This property is used to
// determine if setRemoteDescription has already been called.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-remotedescription
func (pc *PeerConnection) RemoteDescription() *SessionDescription {
	return valueToSessionDescription(pc.underlying.Get("remoteDescription"))
}

// AddICECandidate accepts an ICE candidate string and adds it
// to the existing set of candidates
func (pc *PeerConnection) AddICECandidate(candidate ICECandidateInit) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	promise := pc.underlying.Call("addIceCandidate", iceCandidateInitToValue(candidate))
	_, err = awaitPromise(promise)
	return err
}

// ICEConnectionState returns the ICE connection state of the
// PeerConnection instance.
func (pc *PeerConnection) ICEConnectionState() ICEConnectionState {
	return NewICEConnectionState(pc.underlying.Get("iceConnectionState").String())
}

// OnICECandidate sets an event handler which is invoked when a new ICE
// candidate is found.
func (pc *PeerConnection) OnICECandidate(f func(candidate *ICECandidate)) {
	if pc.onICECandidateHandler != nil {
		oldHandler := pc.onICECandidateHandler
		defer oldHandler.Release()
	}
	onICECandidateHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		candidate := valueToICECandidate(args[0].Get("candidate"))
		if candidate == nil && pc.onGatherCompleteHandler != nil {
			go pc.onGatherCompleteHandler()
		}

		go f(candidate)
		return js.Undefined()
	})
	pc.onICECandidateHandler = &onICECandidateHandler
	pc.underlying.Set("onicecandidate", onICECandidateHandler)
}

// OnICEGatheringStateChange sets an event handler which is invoked when the
// ICE candidate gathering state has changed.
func (pc *PeerConnection) OnICEGatheringStateChange(f func()) {
	if pc.onICEGatheringStateChangeHandler != nil {
		oldHandler := pc.onICEGatheringStateChangeHandler
		defer oldHandler.Release()
	}
	onICEGatheringStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		go f()
		return js.Undefined()
	})
	pc.onICEGatheringStateChangeHandler = &onICEGatheringStateChangeHandler
	pc.underlying.Set("onicegatheringstatechange", onICEGatheringStateChangeHandler)
}

// CreateDataChannel creates a new DataChannel object with the given label
// and optional DataChannelInit used to configure properties of the
// underlying channel such as data reliability.
func (pc *PeerConnection) CreateDataChannel(label string, options *DataChannelInit) (_ *DataChannel, err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	channel := pc.underlying.Call("createDataChannel", label, dataChannelInitToValue(options))
	return &DataChannel{
		underlying: channel,
		api:        pc.api,
	}, nil
}

// SetIdentityProvider is used to configure an identity provider to generate identity assertions
func (pc *PeerConnection) SetIdentityProvider(provider string) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	pc.underlying.Call("setIdentityProvider", provider)
	return nil
}

// Close ends the PeerConnection
func (pc *PeerConnection) Close() (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()

	pc.underlying.Call("close")

	// Release any handlers as required by the syscall/js API.
	if pc.onSignalingStateChangeHandler != nil {
		pc.onSignalingStateChangeHandler.Release()
	}
	if pc.onDataChannelHandler != nil {
		pc.onDataChannelHandler.Release()
	}
	if pc.onNegotiationNeededHandler != nil {
		pc.onNegotiationNeededHandler.Release()
	}
	if pc.onConnectionStateChangeHandler != nil {
		pc.onConnectionStateChangeHandler.Release()
	}
	if pc.onICEConnectionStateChangeHandler != nil {
		pc.onICEConnectionStateChangeHandler.Release()
	}
	if pc.onICECandidateHandler != nil {
		pc.onICECandidateHandler.Release()
	}
	if pc.onICEGatheringStateChangeHandler != nil {
		pc.onICEGatheringStateChangeHandler.Release()
	}

	return nil
}

// CurrentLocalDescription represents the local description that was
// successfully negotiated the last time the PeerConnection transitioned
// into the stable state plus any local candidates that have been generated
// by the ICEAgent since the offer or answer was created.
func (pc *PeerConnection) CurrentLocalDescription() *SessionDescription {
	desc := pc.underlying.Get("currentLocalDescription")
	return valueToSessionDescription(desc)
}

// PendingLocalDescription represents a local description that is in the
// process of being negotiated plus any local candidates that have been
// generated by the ICEAgent since the offer or answer was created. If the
// PeerConnection is in the stable state, the value is null.
func (pc *PeerConnection) PendingLocalDescription() *SessionDescription {
	desc := pc.underlying.Get("pendingLocalDescription")
	return valueToSessionDescription(desc)
}

// CurrentRemoteDescription represents the last remote description that was
// successfully negotiated the last time the PeerConnection transitioned
// into the stable state plus any remote candidates that have been supplied
// via AddICECandidate() since the offer or answer was created.
func (pc *PeerConnection) CurrentRemoteDescription() *SessionDescription {
	desc := pc.underlying.Get("currentRemoteDescription")
	return valueToSessionDescription(desc)
}

// PendingRemoteDescription represents a remote description that is in the
// process of being negotiated, complete with any remote candidates that
// have been supplied via AddICECandidate() since the offer or answer was
// created. If the PeerConnection is in the stable state, the value is
// null.
func (pc *PeerConnection) PendingRemoteDescription() *SessionDescription {
	desc := pc.underlying.Get("pendingRemoteDescription")
	return valueToSessionDescription(desc)
}

// SignalingState returns the signaling state of the PeerConnection instance.
func (pc *PeerConnection) SignalingState() SignalingState {
	rawState := pc.underlying.Get("signalingState").String()
	return newSignalingState(rawState)
}

// ICEGatheringState attribute the ICE gathering state of the PeerConnection
// instance.
func (pc *PeerConnection) ICEGatheringState() ICEGatheringState {
	rawState := pc.underlying.Get("iceGatheringState").String()
	return NewICEGatheringState(rawState)
}

// ConnectionState attribute the connection state of the PeerConnection
// instance.
func (pc *PeerConnection) ConnectionState() PeerConnectionState {
	rawState := pc.underlying.Get("connectionState").String()
	return newPeerConnectionState(rawState)
}

func (pc *PeerConnection) setGatherCompleteHandler(handler func()) {
	pc.onGatherCompleteHandler = handler

	// If no onIceCandidate handler has been set provide an empty one
	// otherwise our onGatherCompleteHandler will not be executed
	if pc.onICECandidateHandler == nil {
		pc.OnICECandidate(func(i *ICECandidate) {})
	}
}

// AddTransceiverFromKind Create a new RtpTransceiver and adds it to the set of transceivers.
func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RTPTransceiverInit) (transceiver *RTPTransceiver, err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()

	if len(init) == 1 {
		return &RTPTransceiver{
			underlying: pc.underlying.Call("addTransceiver", kind.String(), rtpTransceiverInitInitToValue(init[0])),
		}, err
	}

	return &RTPTransceiver{
		underlying: pc.underlying.Call("addTransceiver", kind.String()),
	}, err
}

// GetTransceivers returns the RtpTransceiver that are currently attached to this PeerConnection
func (pc *PeerConnection) GetTransceivers() (transceivers []*RTPTransceiver) {
	rawTransceivers := pc.underlying.Call("getTransceivers")
	transceivers = make([]*RTPTransceiver, rawTransceivers.Length())

	for i := 0; i < rawTransceivers.Length(); i++ {
		transceivers[i] = &RTPTransceiver{
			underlying: rawTransceivers.Index(i),
		}
	}

	return
}

// SCTP returns the SCTPTransport for this PeerConnection
//
// The SCTP transport over which SCTP data is sent and received. If SCTP has not been negotiated, the value is nil.
// https://www.w3.org/TR/webrtc/#attributes-15
func (pc *PeerConnection) SCTP() *SCTPTransport {
	underlying := pc.underlying.Get("sctp")
	if underlying.IsNull() || underlying.IsUndefined() {
		return nil
	}

	return &SCTPTransport{
		underlying: underlying,
	}
}

// Converts a Configuration to js.Value so it can be passed
// through to the JavaScript WebRTC API. Any zero values are converted to
// js.Undefined(), which will result in the default value being used.
func configurationToValue(configuration Configuration) js.Value {
	return js.ValueOf(map[string]interface{}{
		"iceServers":           iceServersToValue(configuration.ICEServers),
		"iceTransportPolicy":   stringEnumToValueOrUndefined(configuration.ICETransportPolicy.String()),
		"bundlePolicy":         stringEnumToValueOrUndefined(configuration.BundlePolicy.String()),
		"rtcpMuxPolicy":        stringEnumToValueOrUndefined(configuration.RTCPMuxPolicy.String()),
		"peerIdentity":         stringToValueOrUndefined(configuration.PeerIdentity),
		"iceCandidatePoolSize": uint8ToValueOrUndefined(configuration.ICECandidatePoolSize),

		// Note: Certificates are not currently supported.
		// "certificates": configuration.Certificates,
	})
}

func iceServersToValue(iceServers []ICEServer) js.Value {
	if len(iceServers) == 0 {
		return js.Undefined()
	}
	maps := make([]interface{}, len(iceServers))
	for i, server := range iceServers {
		maps[i] = iceServerToValue(server)
	}
	return js.ValueOf(maps)
}

func oauthCredentialToValue(o OAuthCredential) js.Value {
	out := map[string]interface{}{
		"MACKey":      o.MACKey,
		"AccessToken": o.AccessToken,
	}
	return js.ValueOf(out)
}

func iceServerToValue(server ICEServer) js.Value {
	out := map[string]interface{}{
		"urls": stringsToValue(server.URLs), // required
	}
	if server.Username != "" {
		out["username"] = stringToValueOrUndefined(server.Username)
	}
	if server.Credential != nil {
		switch t := server.Credential.(type) {
		case string:
			out["credential"] = stringToValueOrUndefined(t)
		case OAuthCredential:
			out["credential"] = oauthCredentialToValue(t)
		}
	}
	out["credentialType"] = stringEnumToValueOrUndefined(server.CredentialType.String())
	return js.ValueOf(out)
}

func valueToConfiguration(configValue js.Value) Configuration {
	if configValue.IsNull() || configValue.IsUndefined() {
		return Configuration{}
	}
	return Configuration{
		ICEServers:           valueToICEServers(configValue.Get("iceServers")),
		ICETransportPolicy:   NewICETransportPolicy(valueToStringOrZero(configValue.Get("iceTransportPolicy"))),
		BundlePolicy:         newBundlePolicy(valueToStringOrZero(configValue.Get("bundlePolicy"))),
		RTCPMuxPolicy:        newRTCPMuxPolicy(valueToStringOrZero(configValue.Get("rtcpMuxPolicy"))),
		PeerIdentity:         valueToStringOrZero(configValue.Get("peerIdentity")),
		ICECandidatePoolSize: valueToUint8OrZero(configValue.Get("iceCandidatePoolSize")),

		// Note: Certificates are not supported.
		// Certificates []Certificate
	}
}

func valueToICEServers(iceServersValue js.Value) []ICEServer {
	if iceServersValue.IsNull() || iceServersValue.IsUndefined() {
		return nil
	}
	iceServers := make([]ICEServer, iceServersValue.Length())
	for i := 0; i < iceServersValue.Length(); i++ {
		iceServers[i] = valueToICEServer(iceServersValue.Index(i))
	}
	return iceServers
}

func valueToICECredential(iceCredentialValue js.Value) interface{} {
	if iceCredentialValue.IsNull() || iceCredentialValue.IsUndefined() {
		return nil
	}
	if iceCredentialValue.Type() == js.TypeString {
		return iceCredentialValue.String()
	}
	if iceCredentialValue.Type() == js.TypeObject {
		return OAuthCredential{
			MACKey:      iceCredentialValue.Get("MACKey").String(),
			AccessToken: iceCredentialValue.Get("AccessToken").String(),
		}
	}
	return nil
}

func valueToICEServer(iceServerValue js.Value) ICEServer {
	tpe, err := newICECredentialType(valueToStringOrZero(iceServerValue.Get("credentialType")))
	if err != nil {
		tpe = ICECredentialTypePassword
	}
	s := ICEServer{
		URLs:     valueToStrings(iceServerValue.Get("urls")), // required
		Username: valueToStringOrZero(iceServerValue.Get("username")),
		// Note: Credential and CredentialType are not currently supported.
		Credential:     valueToICECredential(iceServerValue.Get("credential")),
		CredentialType: tpe,
	}

	return s
}

func valueToICECandidate(val js.Value) *ICECandidate {
	if val.IsNull() || val.IsUndefined() {
		return nil
	}
	if val.Get("protocol").IsUndefined() && !val.Get("candidate").IsUndefined() {
		// Missing some fields, assume it's Firefox and parse SDP candidate.
		c, err := ice.UnmarshalCandidate(val.Get("candidate").String())
		if err != nil {
			return nil
		}

		iceCandidate, err := newICECandidateFromICE(c)
		if err != nil {
			return nil
		}

		return &iceCandidate
	}
	protocol, _ := NewICEProtocol(val.Get("protocol").String())
	candidateType, _ := NewICECandidateType(val.Get("type").String())
	return &ICECandidate{
		Foundation:     val.Get("foundation").String(),
		Priority:       valueToUint32OrZero(val.Get("priority")),
		Address:        val.Get("address").String(),
		Protocol:       protocol,
		Port:           valueToUint16OrZero(val.Get("port")),
		Typ:            candidateType,
		Component:      stringToComponentIDOrZero(val.Get("component").String()),
		RelatedAddress: val.Get("relatedAddress").String(),
		RelatedPort:    valueToUint16OrZero(val.Get("relatedPort")),
	}
}

func stringToComponentIDOrZero(val string) uint16 {
	// See: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceComponent
	switch val {
	case "rtp":
		return 1
	case "rtcp":
		return 2
	}
	return 0
}

func sessionDescriptionToValue(desc *SessionDescription) js.Value {
	if desc == nil {
		return js.Undefined()
	}
	return js.ValueOf(map[string]interface{}{
		"type": desc.Type.String(),
		"sdp":  desc.SDP,
	})
}

func valueToSessionDescription(descValue js.Value) *SessionDescription {
	if descValue.IsNull() || descValue.IsUndefined() {
		return nil
	}
	return &SessionDescription{
		Type: NewSDPType(descValue.Get("type").String()),
		SDP:  descValue.Get("sdp").String(),
	}
}

func offerOptionsToValue(offerOptions *OfferOptions) js.Value {
	if offerOptions == nil {
		return js.Undefined()
	}
	return js.ValueOf(map[string]interface{}{
		"iceRestart":             offerOptions.ICERestart,
		"voiceActivityDetection": offerOptions.VoiceActivityDetection,
	})
}

func answerOptionsToValue(answerOptions *AnswerOptions) js.Value {
	if answerOptions == nil {
		return js.Undefined()
	}
	return js.ValueOf(map[string]interface{}{
		"voiceActivityDetection": answerOptions.VoiceActivityDetection,
	})
}

func iceCandidateInitToValue(candidate ICECandidateInit) js.Value {
	return js.ValueOf(map[string]interface{}{
		"candidate":        candidate.Candidate,
		"sdpMid":           stringPointerToValue(candidate.SDPMid),
		"sdpMLineIndex":    uint16PointerToValue(candidate.SDPMLineIndex),
		"usernameFragment": stringPointerToValue(candidate.UsernameFragment),
	})
}

func dataChannelInitToValue(options *DataChannelInit) js.Value {
	if options == nil {
		return js.Undefined()
	}

	maxPacketLifeTime := uint16PointerToValue(options.MaxPacketLifeTime)
	return js.ValueOf(map[string]interface{}{
		"ordered":           boolPointerToValue(options.Ordered),
		"maxPacketLifeTime": maxPacketLifeTime,
		// See https://bugs.chromium.org/p/chromium/issues/detail?id=696681
		// Chrome calls this "maxRetransmitTime"
		"maxRetransmitTime": maxPacketLifeTime,
		"maxRetransmits":    uint16PointerToValue(options.MaxRetransmits),
		"protocol":          stringPointerToValue(options.Protocol),
		"negotiated":        boolPointerToValue(options.Negotiated),
		"id":                uint16PointerToValue(options.ID),
	})
}

func rtpTransceiverInitInitToValue(init RTPTransceiverInit) js.Value {
	return js.ValueOf(map[string]interface{}{
		"direction": init.Direction.String(),
	})
}
