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

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

package webrtc

import (
	"fmt"
	"syscall/js"

	"github.com/pion/datachannel"
)

const dataChannelBufferSize = 16384 // Lowest common denominator among browsers

// DataChannel represents a WebRTC DataChannel
// The DataChannel interface represents a network channel
// which can be used for bidirectional peer-to-peer transfers of arbitrary data
type DataChannel 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.
	onOpenHandler       *js.Func
	onCloseHandler      *js.Func
	onMessageHandler    *js.Func
	onBufferedAmountLow *js.Func

	// A reference to the associated api object used by this datachannel
	api *API
}

// OnOpen sets an event handler which is invoked when
// the underlying data transport has been established (or re-established).
func (d *DataChannel) OnOpen(f func()) {
	if d.onOpenHandler != nil {
		oldHandler := d.onOpenHandler
		defer oldHandler.Release()
	}
	onOpenHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		go f()
		return js.Undefined()
	})
	d.onOpenHandler = &onOpenHandler
	d.underlying.Set("onopen", onOpenHandler)
}

// OnClose sets an event handler which is invoked when
// the underlying data transport has been closed.
func (d *DataChannel) OnClose(f func()) {
	if d.onCloseHandler != nil {
		oldHandler := d.onCloseHandler
		defer oldHandler.Release()
	}
	onCloseHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		go f()
		return js.Undefined()
	})
	d.onCloseHandler = &onCloseHandler
	d.underlying.Set("onclose", onCloseHandler)
}

// OnMessage sets an event handler which is invoked on a binary message arrival
// from a remote peer. Note that browsers may place limitations on message size.
func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) {
	if d.onMessageHandler != nil {
		oldHandler := d.onMessageHandler
		defer oldHandler.Release()
	}
	onMessageHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		// pion/webrtc/projects/15
		data := args[0].Get("data")
		go func() {
			// valueToDataChannelMessage may block when handling 'Blob' data
			// so we need to call it from a new routine. See:
			// https://pkg.go.dev/syscall/js#FuncOf
			msg := valueToDataChannelMessage(data)
			f(msg)
		}()
		return js.Undefined()
	})
	d.onMessageHandler = &onMessageHandler
	d.underlying.Set("onmessage", onMessageHandler)
}

// Send sends the binary message to the DataChannel peer
func (d *DataChannel) Send(data []byte) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	array := js.Global().Get("Uint8Array").New(len(data))
	js.CopyBytesToJS(array, data)
	d.underlying.Call("send", array)
	return nil
}

// SendText sends the text message to the DataChannel peer
func (d *DataChannel) SendText(s string) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()
	d.underlying.Call("send", s)
	return nil
}

// Detach allows you to detach the underlying datachannel. This provides
// an idiomatic API to work with, however it disables the OnMessage callback.
// Before calling Detach you have to enable this behavior by calling
// webrtc.DetachDataChannels(). Combining detached and normal data channels
// is not supported.
// Please reffer to the data-channels-detach example and the
// pion/datachannel documentation for the correct way to handle the
// resulting DataChannel object.
func (d *DataChannel) Detach() (datachannel.ReadWriteCloser, error) {
	if !d.api.settingEngine.detach.DataChannels {
		return nil, fmt.Errorf("enable detaching by calling webrtc.DetachDataChannels()")
	}

	detached := newDetachedDataChannel(d)
	return detached, nil
}

// Close Closes the DataChannel. It may be called regardless of whether
// the DataChannel object was created by this peer or the remote peer.
func (d *DataChannel) Close() (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = recoveryToError(e)
		}
	}()

	d.underlying.Call("close")

	// Release any handlers as required by the syscall/js API.
	if d.onOpenHandler != nil {
		d.onOpenHandler.Release()
	}
	if d.onCloseHandler != nil {
		d.onCloseHandler.Release()
	}
	if d.onMessageHandler != nil {
		d.onMessageHandler.Release()
	}
	if d.onBufferedAmountLow != nil {
		d.onBufferedAmountLow.Release()
	}

	return nil
}

// Label represents a label that can be used to distinguish this
// DataChannel object from other DataChannel objects. Scripts are
// allowed to create multiple DataChannel objects with the same label.
func (d *DataChannel) Label() string {
	return d.underlying.Get("label").String()
}

// Ordered represents if the DataChannel is ordered, and false if
// out-of-order delivery is allowed.
func (d *DataChannel) Ordered() bool {
	ordered := d.underlying.Get("ordered")
	if ordered.IsUndefined() {
		return true // default is true
	}
	return ordered.Bool()
}

// MaxPacketLifeTime represents the length of the time window (msec) during
// which transmissions and retransmissions may occur in unreliable mode.
func (d *DataChannel) MaxPacketLifeTime() *uint16 {
	if !d.underlying.Get("maxPacketLifeTime").IsUndefined() {
		return valueToUint16Pointer(d.underlying.Get("maxPacketLifeTime"))
	}

	// See https://bugs.chromium.org/p/chromium/issues/detail?id=696681
	// Chrome calls this "maxRetransmitTime"
	return valueToUint16Pointer(d.underlying.Get("maxRetransmitTime"))
}

// MaxRetransmits represents the maximum number of retransmissions that are
// attempted in unreliable mode.
func (d *DataChannel) MaxRetransmits() *uint16 {
	return valueToUint16Pointer(d.underlying.Get("maxRetransmits"))
}

// Protocol represents the name of the sub-protocol used with this
// DataChannel.
func (d *DataChannel) Protocol() string {
	return d.underlying.Get("protocol").String()
}

// Negotiated represents whether this DataChannel was negotiated by the
// application (true), or not (false).
func (d *DataChannel) Negotiated() bool {
	return d.underlying.Get("negotiated").Bool()
}

// ID represents the ID for this DataChannel. The value is initially
// null, which is what will be returned if the ID was not provided at
// channel creation time. Otherwise, it will return the ID that was either
// selected by the script or generated. After the ID is set to a non-null
// value, it will not change.
func (d *DataChannel) ID() *uint16 {
	return valueToUint16Pointer(d.underlying.Get("id"))
}

// ReadyState represents the state of the DataChannel object.
func (d *DataChannel) ReadyState() DataChannelState {
	return newDataChannelState(d.underlying.Get("readyState").String())
}

// BufferedAmount represents the number of bytes of application data
// (UTF-8 text and binary data) that have been queued using send(). Even
// though the data transmission can occur in parallel, the returned value
// MUST NOT be decreased before the current task yielded back to the event
// loop to prevent race conditions. The value does not include framing
// overhead incurred by the protocol, or buffering done by the operating
// system or network hardware. The value of BufferedAmount slot will only
// increase with each call to the send() method as long as the ReadyState is
// open; however, BufferedAmount does not reset to zero once the channel
// closes.
func (d *DataChannel) BufferedAmount() uint64 {
	return uint64(d.underlying.Get("bufferedAmount").Int())
}

// BufferedAmountLowThreshold represents the threshold at which the
// bufferedAmount is considered to be low. When the bufferedAmount decreases
// from above this threshold to equal or below it, the bufferedamountlow
// event fires. BufferedAmountLowThreshold is initially zero on each new
// DataChannel, but the application may change its value at any time.
func (d *DataChannel) BufferedAmountLowThreshold() uint64 {
	return uint64(d.underlying.Get("bufferedAmountLowThreshold").Int())
}

// SetBufferedAmountLowThreshold is used to update the threshold.
// See BufferedAmountLowThreshold().
func (d *DataChannel) SetBufferedAmountLowThreshold(th uint64) {
	d.underlying.Set("bufferedAmountLowThreshold", th)
}

// OnBufferedAmountLow sets an event handler which is invoked when
// the number of bytes of outgoing data becomes lower than the
// BufferedAmountLowThreshold.
func (d *DataChannel) OnBufferedAmountLow(f func()) {
	if d.onBufferedAmountLow != nil {
		oldHandler := d.onBufferedAmountLow
		defer oldHandler.Release()
	}
	onBufferedAmountLow := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		go f()
		return js.Undefined()
	})
	d.onBufferedAmountLow = &onBufferedAmountLow
	d.underlying.Set("onbufferedamountlow", onBufferedAmountLow)
}

// valueToDataChannelMessage converts the given value to a DataChannelMessage.
// val should be obtained from MessageEvent.data where MessageEvent is received
// via the RTCDataChannel.onmessage callback.
func valueToDataChannelMessage(val js.Value) DataChannelMessage {
	// If val is of type string, the conversion is straightforward.
	if val.Type() == js.TypeString {
		return DataChannelMessage{
			IsString: true,
			Data:     []byte(val.String()),
		}
	}

	// For other types, we need to first determine val.constructor.name.
	constructorName := val.Get("constructor").Get("name").String()
	var data []byte
	switch constructorName {
	case "Uint8Array":
		// We can easily convert Uint8Array to []byte
		data = uint8ArrayValueToBytes(val)
	case "Blob":
		// Convert the Blob to an ArrayBuffer and then convert the ArrayBuffer
		// to a Uint8Array.
		// See: https://developer.mozilla.org/en-US/docs/Web/API/Blob

		// The JavaScript API for reading from the Blob is asynchronous. We use a
		// channel to signal when reading is done.
		reader := js.Global().Get("FileReader").New()
		doneChan := make(chan struct{})
		reader.Call("addEventListener", "loadend", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
			go func() {
				// Signal that the FileReader is done reading/loading by sending through
				// the doneChan.
				doneChan <- struct{}{}
			}()
			return js.Undefined()
		}))

		reader.Call("readAsArrayBuffer", val)

		// Wait for the FileReader to finish reading/loading.
		<-doneChan

		// At this point buffer.result is a typed array, which we know how to
		// handle.
		buffer := reader.Get("result")
		uint8Array := js.Global().Get("Uint8Array").New(buffer)
		data = uint8ArrayValueToBytes(uint8Array)
	default:
		// Assume we have an ArrayBufferView type which we can convert to a
		// Uint8Array in JavaScript.
		// See: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView
		uint8Array := js.Global().Get("Uint8Array").New(val)
		data = uint8ArrayValueToBytes(uint8Array)
	}

	return DataChannelMessage{
		IsString: false,
		Data:     data,
	}
}
