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

package vnet

import (
	
	
	
	
)

// UDPProxy is a proxy between real server(net.UDPConn) and vnet.UDPConn.
//
// High level design:
//
//	                        ..............................................
//	                        :         Virtual Network (vnet)             :
//	                        :                                            :
//	+-------+ *         1 +----+         +--------+                      :
//	| :App  |------------>|:Net|--o<-----|:Router |          .............................
//	+-------+             +----+         |        |          :        UDPProxy           :
//	                        :            |        |       +----+     +---------+     +---------+     +--------+
//	                        :            |        |--->o--|:Net|-->o-| vnet.   |-->o-|  net.   |--->-| :Real  |
//	                        :            |        |       +----+     | UDPConn |     | UDPConn |     | Server |
//	                        :            |        |          :       +---------+     +---------+     +--------+
//	                        :            |        |          ............................:
//	                        :            +--------+                       :
//	                        ...............................................
type UDPProxy struct {
	// The router bind to.
	router *Router

	// Each vnet source, bind to a real socket to server.
	// key is real server addr, which is net.Addr
	// value is *aUDPProxyWorker
	workers sync.Map

	// For each endpoint, we never know when to start and stop proxy,
	// so we stop the endpoint when timeout.
	timeout time.Duration

	// For utest, to mock the target real server.
	// Optional, use the address of received client packet.
	mockRealServerAddr *net.UDPAddr
}

// NewProxy create a proxy, the router for this proxy belongs/bind to. If need to proxy for
// please create a new proxy for each router. For all addresses we proxy, we will create a
// vnet.Net in this router and proxy all packets.
func ( *Router) (*UDPProxy, error) {
	 := &UDPProxy{router: , timeout: 2 * time.Minute}
	return , nil
}

// Close the proxy, stop all workers.
func ( *UDPProxy) () error {
	.workers.Range(func(,  interface{}) bool {
		_ = .(*aUDPProxyWorker).Close() //nolint:forcetypeassert
		return true
	})
	return nil
}

// Proxy starts a worker for server, ignore if already started.
func ( *UDPProxy) ( *Net,  *net.UDPAddr) error {
	// Note that even if the worker exists, it's also ok to create a same worker,
	// because the router will use the last one, and the real server will see a address
	// change event after we switch to the next worker.
	if ,  := .workers.Load(.String());  {
		// nolint:godox // TODO: Need to restart the stopped worker?
		return nil
	}

	// Not exists, create a new one.
	 := &aUDPProxyWorker{
		router: .router, mockRealServerAddr: .mockRealServerAddr,
	}

	// Create context for cleanup.
	var  context.Context
	, .ctxDisposeCancel = context.WithCancel(context.Background())

	.workers.Store(.String(), )

	return .Proxy(, , )
}

// A proxy worker for a specified proxy server.
type aUDPProxyWorker struct {
	router             *Router
	mockRealServerAddr *net.UDPAddr

	// Each vnet source, bind to a real socket to server.
	// key is vnet client addr, which is net.Addr
	// value is *net.UDPConn
	endpoints sync.Map

	// For cleanup.
	ctxDisposeCancel context.CancelFunc
	wg               sync.WaitGroup
}

func ( *aUDPProxyWorker) () error {
	// Notify all goroutines to dispose.
	.ctxDisposeCancel()

	// Wait for all goroutines quit.
	.wg.Wait()

	return nil
}

func ( *aUDPProxyWorker) ( context.Context,  *Net,  *net.UDPAddr) error { // nolint:gocognit
	// Create vnet for real server by serverAddr.
	,  := NewNet(&NetConfig{
		StaticIP: .IP.String(),
	})
	if  != nil {
		return 
	}

	if  = .router.AddNet();  != nil {
		return 
	}

	// We must create a "same" vnet.UDPConn as the net.UDPConn,
	// which has the same ip:port, to copy packets between them.
	,  := .ListenUDP("udp4", )
	if  != nil {
		return 
	}

	// User stop proxy, we should close the socket.
	go func() {
		<-.Done()
		_ = .Close()
	}()

	// Got new vnet client, start a new endpoint.
	 := func( net.Addr) (*net.UDPConn, error) {
		// Exists binding.
		if ,  := .endpoints.Load(.String());  {
			// Exists endpoint, reuse it.
			return .(*net.UDPConn), nil //nolint:forcetypeassert
		}

		// The real server we proxy to, for utest to mock it.
		 := 
		if .mockRealServerAddr != nil {
			 = .mockRealServerAddr
		}

		// Got new vnet client, create new endpoint.
		,  := net.DialUDP("udp4", nil, )
		if  != nil {
			return nil, 
		}

		// User stop proxy, we should close the socket.
		go func() {
			<-.Done()
			_ = .Close()
		}()

		// Bind address.
		.endpoints.Store(.String(), )

		// Got packet from real serverAddr, we should proxy it to vnet.
		.wg.Add(1)
		go func( net.Addr) {
			defer .wg.Done()

			 := make([]byte, 1500)
			for {
				, ,  := .ReadFrom()
				if  != nil {
					return
				}

				if  <= 0 {
					continue // Drop packet
				}

				if ,  := .WriteTo([:], );  != nil {
					return
				}
			}
		}()

		return , nil
	}

	// Start a proxy goroutine.
	.wg.Add(1)
	go func() {
		defer .wg.Done()

		 := make([]byte, 1500)

		for {
			, ,  := .ReadFrom()
			if  != nil {
				return
			}

			if  <= 0 ||  == nil {
				continue // Drop packet
			}

			,  := ()
			if  != nil {
				continue // Drop packet.
			}

			if ,  := .Write([:]);  != nil {
				return
			}
		}
	}()

	return nil
}