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

package stun

import (
	
	
	
	
	
	
	
	
	
	
	

	
	
	
)

// ErrUnsupportedURI is an error thrown if the user passes an unsupported STUN or TURN URI
var ErrUnsupportedURI = fmt.Errorf("invalid schema or transport")

// Dial connects to the address on the named network and then
// initializes Client on that connection, returning error if any.
func (,  string) (*Client, error) {
	,  := net.Dial(, )
	if  != nil {
		return nil, 
	}
	return NewClient()
}

// DialConfig is used to pass configuration to DialURI()
type DialConfig struct {
	DTLSConfig dtls.Config
	TLSConfig  tls.Config

	Net transport.Net
}

// DialURI connect to the STUN/TURN URI and then
// initializes Client on that connection, returning error if any.
func ( *URI,  *DialConfig) (*Client, error) {
	var  Connection
	var  error

	 := .Net
	if  == nil {
		,  = stdnet.NewNet()
		if  != nil {
			return nil, fmt.Errorf("failed to create net: %w", )
		}
	}

	 := net.JoinHostPort(.Host, strconv.Itoa(.Port))

	switch {
	case .Scheme == SchemeTypeSTUN:
		if ,  = .Dial("udp", );  != nil {
			return nil, fmt.Errorf("failed to listen: %w", )
		}

	case .Scheme == SchemeTypeTURN:
		 := "udp" //nolint:goconst
		if .Proto == ProtoTypeTCP {
			 = "tcp" //nolint:goconst
		}

		if ,  = .Dial(, );  != nil {
			return nil, fmt.Errorf("failed to dial: %w", )
		}

	case .Scheme == SchemeTypeTURNS && .Proto == ProtoTypeUDP:
		 := .DTLSConfig // Copy
		.ServerName = .Host

		,  := .Dial("udp", )
		if  != nil {
			return nil, fmt.Errorf("failed to dial: %w", )
		}

		if ,  = dtls.Client(, &);  != nil {
			return nil, fmt.Errorf("failed to connect to '%s': %w", , )
		}

	case (.Scheme == SchemeTypeTURNS || .Scheme == SchemeTypeSTUNS) && .Proto == ProtoTypeTCP:
		 := .TLSConfig //nolint:govet
		.ServerName = .Host

		,  := .Dial("tcp", )
		if  != nil {
			return nil, fmt.Errorf("failed to dial: %w", )
		}

		 = tls.Client(, &)

	default:
		return nil, ErrUnsupportedURI
	}

	return NewClient()
}

// ErrNoConnection means that ClientOptions.Connection is nil.
var ErrNoConnection = errors.New("no connection provided")

// ClientOption sets some client option.
type ClientOption func(c *Client)

// WithHandler sets client handler which is called if Agent emits the Event
// with TransactionID that is not currently registered by Client.
// Useful for handling Data indications from TURN server.
func ( Handler) ClientOption {
	return func( *Client) {
		.handler = 
	}
}

// WithRTO sets client RTO as defined in STUN RFC.
func ( time.Duration) ClientOption {
	return func( *Client) {
		.rto = int64()
	}
}

// WithClock sets Clock of client, the source of current time.
// Also clock is passed to default collector if set.
func ( Clock) ClientOption {
	return func( *Client) {
		.clock = 
	}
}

// WithTimeoutRate sets RTO timer minimum resolution.
func ( time.Duration) ClientOption {
	return func( *Client) {
		.rtoRate = 
	}
}

// WithAgent sets client STUN agent.
//
// Defaults to agent implementation in current package,
// see agent.go.
func ( ClientAgent) ClientOption {
	return func( *Client) {
		.a = 
	}
}

// WithCollector rests client timeout collector, the implementation
// of ticker which calls function on each tick.
func ( Collector) ClientOption {
	return func( *Client) {
		.collector = 
	}
}

// WithNoConnClose prevents client from closing underlying connection when
// the Close() method is called.
func () ClientOption {
	return func( *Client) {
		.closeConn = false
	}
}

// WithNoRetransmit disables retransmissions and sets RTO to
// defaultMaxAttempts * defaultRTO which will be effectively time out
// if not set.
//
// Useful for TCP connections where transport handles RTO.
func ( *Client) {
	.maxAttempts = 0
	if .rto == 0 {
		.rto = defaultMaxAttempts * int64(defaultRTO)
	}
}

const (
	defaultTimeoutRate = time.Millisecond * 5
	defaultRTO         = time.Millisecond * 300
	defaultMaxAttempts = 7
)

// NewClient initializes new Client from provided options,
// starting internal goroutines and using default options fields
// if necessary. Call Close method after using Client to close conn and
// release resources.
//
// The conn will be closed on Close call. Use WithNoConnClose option to
// prevent that.
//
// Note that user should handle the protocol multiplexing, client does not
// provide any API for it, so if you need to read application data, wrap the
// connection with your (de-)multiplexer and pass the wrapper as conn.
func ( Connection,  ...ClientOption) (*Client, error) {
	 := &Client{
		close:       make(chan struct{}),
		c:           ,
		clock:       systemClock(),
		rto:         int64(defaultRTO),
		rtoRate:     defaultTimeoutRate,
		t:           make(map[transactionID]*clientTransaction, 100),
		maxAttempts: defaultMaxAttempts,
		closeConn:   true,
	}
	for ,  := range  {
		()
	}
	if .c == nil {
		return nil, ErrNoConnection
	}
	if .a == nil {
		.a = NewAgent(nil)
	}
	if  := .a.SetHandler(.handleAgentCallback);  != nil {
		return nil, 
	}
	if .collector == nil {
		.collector = &tickerCollector{
			close: make(chan struct{}),
			clock: .clock,
		}
	}
	if  := .collector.Start(.rtoRate, func( time.Time) {
		closedOrPanic(.a.Collect())
	});  != nil {
		return nil, 
	}
	.wg.Add(1)
	go .readUntilClosed()
	runtime.SetFinalizer(, clientFinalizer)
	return , nil
}

func clientFinalizer( *Client) {
	if  == nil {
		return
	}
	 := .Close()
	if errors.Is(, ErrClientClosed) {
		return
	}
	if  == nil {
		log.Println("client: called finalizer on non-closed client") // nolint
		return
	}
	log.Println("client: called finalizer on non-closed client:", ) // nolint
}

// Connection wraps Reader, Writer and Closer interfaces.
type Connection interface {
	io.Reader
	io.Writer
	io.Closer
}

// ClientAgent is Agent implementation that is used by Client to
// process transactions.
type ClientAgent interface {
	Process(*Message) error
	Close() error
	Start(id [TransactionIDSize]byte, deadline time.Time) error
	Stop(id [TransactionIDSize]byte) error
	Collect(time.Time) error
	SetHandler(h Handler) error
}

// Client simulates "connection" to STUN server.
type Client struct {
	rto         int64 // time.Duration
	a           ClientAgent
	c           Connection
	close       chan struct{}
	rtoRate     time.Duration
	maxAttempts int32
	closed      bool
	closeConn   bool // should call c.Close() while closing
	wg          sync.WaitGroup
	clock       Clock
	handler     Handler
	collector   Collector
	t           map[transactionID]*clientTransaction

	// mux guards closed and t
	mux sync.RWMutex
}

// clientTransaction represents transaction in progress.
// If transaction is succeed or failed, f will be called
// provided by event.
// Concurrent access is invalid.
type clientTransaction struct {
	id      transactionID
	attempt int32
	calls   int32
	h       Handler
	start   time.Time
	rto     time.Duration
	raw     []byte
}

func ( *clientTransaction) ( Event) {
	if atomic.AddInt32(&.calls, 1) == 1 {
		.h()
	}
}

var clientTransactionPool = &sync.Pool{ //nolint:gochecknoglobals
	New: func() interface{} {
		return &clientTransaction{
			raw: make([]byte, 1500),
		}
	},
}

func acquireClientTransaction() *clientTransaction {
	return clientTransactionPool.Get().(*clientTransaction) //nolint:forcetypeassert
}

func putClientTransaction( *clientTransaction) {
	.raw = .raw[:0]
	.start = time.Time{}
	.attempt = 0
	.id = transactionID{}
	clientTransactionPool.Put()
}

func ( *clientTransaction) ( time.Time) time.Time {
	return .Add(time.Duration(.attempt+1) * .rto)
}

// start registers transaction.
//
// Could return ErrClientClosed, ErrTransactionExists.
func ( *Client) ( *clientTransaction) error {
	.mux.Lock()
	defer .mux.Unlock()
	if .closed {
		return ErrClientClosed
	}
	,  := .t[.id]
	if  {
		return ErrTransactionExists
	}
	.t[.id] = 
	return nil
}

// Clock abstracts the source of current time.
type Clock interface {
	Now() time.Time
}

type systemClockService struct{}

func (systemClockService) () time.Time { return time.Now() }

func systemClock() systemClockService {
	return systemClockService{}
}

// SetRTO sets current RTO value.
func ( *Client) ( time.Duration) {
	atomic.StoreInt64(&.rto, int64())
}

// StopErr occurs when Client fails to stop transaction while
// processing error.
//
//nolint:errname
type StopErr struct {
	Err   error // value returned by Stop()
	Cause error // error that caused Stop() call
}

func ( StopErr) () string {
	return fmt.Sprintf("error while stopping due to %s: %s", sprintErr(.Cause), sprintErr(.Err))
}

// CloseErr indicates client close failure.
//
//nolint:errname
type CloseErr struct {
	AgentErr      error
	ConnectionErr error
}

func sprintErr( error) string {
	if  == nil {
		return "<nil>" //nolint:goconst
	}
	return .Error()
}

func ( CloseErr) () string {
	return fmt.Sprintf("failed to close: %s (connection), %s (agent)", sprintErr(.ConnectionErr), sprintErr(.AgentErr))
}

func ( *Client) () {
	defer .wg.Done()
	 := new(Message)
	.Raw = make([]byte, 1024)
	for {
		select {
		case <-.close:
			return
		default:
		}
		,  := .ReadFrom(.c)
		if  == nil {
			if  := .a.Process(); errors.Is(, ErrAgentClosed) {
				return
			}
		}
	}
}

func closedOrPanic( error) {
	if  == nil || errors.Is(, ErrAgentClosed) {
		return
	}
	panic() //nolint
}

type tickerCollector struct {
	close chan struct{}
	wg    sync.WaitGroup
	clock Clock
}

// Collector calls function f with constant rate.
//
// The simple Collector is ticker which calls function on each tick.
type Collector interface {
	Start(rate time.Duration, f func(now time.Time)) error
	Close() error
}

func ( *tickerCollector) ( time.Duration,  func( time.Time)) error {
	 := time.NewTicker()
	.wg.Add(1)
	go func() {
		defer .wg.Done()
		for {
			select {
			case <-.close:
				.Stop()
				return
			case <-.C:
				(.clock.Now())
			}
		}
	}()
	return nil
}

func ( *tickerCollector) () error {
	close(.close)
	.wg.Wait()
	return nil
}

// ErrClientClosed indicates that client is closed.
var ErrClientClosed = errors.New("client is closed")

// Close stops internal connection and agent, returning CloseErr on error.
func ( *Client) () error {
	if  := .checkInit();  != nil {
		return 
	}
	.mux.Lock()
	if .closed {
		.mux.Unlock()
		return ErrClientClosed
	}
	.closed = true
	.mux.Unlock()
	if  := .collector.Close();  != nil {
		return 
	}
	var  error
	 := .a.Close()
	if .closeConn {
		 = .c.Close()
	}
	close(.close)
	.wg.Wait()
	if  == nil &&  == nil {
		return nil
	}
	return CloseErr{
		AgentErr:      ,
		ConnectionErr: ,
	}
}

// Indicate sends indication m to server. Shorthand to Start call
// with zero deadline and callback.
func ( *Client) ( *Message) error {
	return .Start(, nil)
}

// callbackWaitHandler blocks on wait() call until callback is called.
type callbackWaitHandler struct {
	handler   Handler
	callback  func(event Event)
	cond      *sync.Cond
	processed bool
}

func ( *callbackWaitHandler) ( Event) {
	.cond.L.Lock()
	if .callback == nil {
		panic("s.callback is nil") //nolint
	}
	.callback()
	.processed = true
	.cond.Broadcast()
	.cond.L.Unlock()
}

func ( *callbackWaitHandler) () {
	.cond.L.Lock()
	for !.processed {
		.cond.Wait()
	}
	.processed = false
	.callback = nil
	.cond.L.Unlock()
}

func ( *callbackWaitHandler) ( func( Event)) {
	if  == nil {
		panic("f is nil") //nolint
	}
	.cond.L.Lock()
	.callback = 
	if .handler == nil {
		.handler = .HandleEvent
	}
	.cond.L.Unlock()
}

var callbackWaitHandlerPool = sync.Pool{ //nolint:gochecknoglobals
	New: func() interface{} {
		return &callbackWaitHandler{
			cond: sync.NewCond(new(sync.Mutex)),
		}
	},
}

// ErrClientNotInitialized means that client connection or agent is nil.
var ErrClientNotInitialized = errors.New("client not initialized")

func ( *Client) () error {
	if  == nil || .c == nil || .a == nil || .close == nil {
		return ErrClientNotInitialized
	}
	return nil
}

// Do is Start wrapper that waits until callback is called. If no callback
// provided, Indicate is called instead.
//
// Do has cpu overhead due to blocking, see BenchmarkClient_Do.
// Use Start method for less overhead.
func ( *Client) ( *Message,  func(Event)) error {
	if  := .checkInit();  != nil {
		return 
	}
	if  == nil {
		return .Indicate()
	}
	 := callbackWaitHandlerPool.Get().(*callbackWaitHandler) //nolint:forcetypeassert
	.setCallback()
	defer func() {
		callbackWaitHandlerPool.Put()
	}()
	if  := .Start(, .handler);  != nil {
		return 
	}
	.wait()
	return nil
}

func ( *Client) ( transactionID) {
	.mux.Lock()
	if .t != nil {
		delete(.t, )
	}
	.mux.Unlock()
}

type buffer struct {
	buf []byte
}

var bufferPool = &sync.Pool{ //nolint:gochecknoglobals
	New: func() interface{} {
		return &buffer{buf: make([]byte, 2048)}
	},
}

func ( *Client) ( Event) {
	.mux.Lock()
	if .closed {
		.mux.Unlock()
		return
	}
	,  := .t[.TransactionID]
	if  {
		delete(.t, .id)
	}
	.mux.Unlock()
	if ! {
		if .handler != nil && !errors.Is(.Error, ErrTransactionStopped) {
			.handler()
		}
		// Ignoring.
		return
	}
	if atomic.LoadInt32(&.maxAttempts) <= .attempt || .Error == nil {
		// Transaction completed.
		.handle()
		putClientTransaction()
		return
	}
	// Doing re-transmission.
	.attempt++
	 := bufferPool.Get().(*buffer) //nolint:forcetypeassert
	.buf = .buf[:copy(.buf[:cap(.buf)], .raw)]
	defer bufferPool.Put()
	var (
		     = .clock.Now()
		 = .nextTimeout()
		      = .id
	)
	// Starting client transaction.
	if  := .start();  != nil {
		.delete()
		.Error = 
		.handle()
		putClientTransaction()
		return
	}
	// Starting agent transaction.
	if  := .a.Start(, );  != nil {
		.delete()
		.Error = 
		.handle()
		putClientTransaction()
		return
	}
	// Writing message to connection again.
	,  := .c.Write(.buf)
	if  != nil {
		.delete()
		.Error = 
		// Stopping agent transaction instead of waiting until it's deadline.
		// This will call handleAgentCallback with "ErrTransactionStopped" error
		// which will be ignored.
		if  := .a.Stop();  != nil {
			// Failed to stop agent transaction. Wrapping the error in StopError.
			.Error = StopErr{
				Err:   ,
				Cause: ,
			}
		}
		.handle()
		putClientTransaction()
		return
	}
}

// Start starts transaction (if h set) and writes message to server, handler
// is called asynchronously.
func ( *Client) ( *Message,  Handler) error {
	if  := .checkInit();  != nil {
		return 
	}
	.mux.RLock()
	 := .closed
	.mux.RUnlock()
	if  {
		return ErrClientClosed
	}
	if  != nil {
		// Starting transaction only if h is set. Useful for indications.
		 := acquireClientTransaction()
		.id = .TransactionID
		.start = .clock.Now()
		.h = 
		.rto = time.Duration(atomic.LoadInt64(&.rto))
		.attempt = 0
		.raw = append(.raw[:0], .Raw...)
		.calls = 0
		 := .nextTimeout(.start)
		if  := .start();  != nil {
			return 
		}
		if  := .a.Start(.TransactionID, );  != nil {
			return 
		}
	}
	,  := .WriteTo(.c)
	if  != nil &&  != nil {
		.delete(.TransactionID)
		// Stopping transaction instead of waiting until deadline.
		if  := .a.Stop(.TransactionID);  != nil {
			return StopErr{
				Err:   ,
				Cause: ,
			}
		}
	}
	return 
}