package v5

import (
	"bytes"
	"sync"

	"github.com/Dreamacro/clash/common/cache"

	"github.com/metacubex/quic-go"
)

// MaxFragSize is a safe udp relay packet size
// because tuicv5 support udp fragment so we unneeded to do a magic modify for quic-go to increase MaxDatagramFrameSize
// it may not work fine in some platform
var MaxFragSize = 1200 - PacketOverHead

func fragWriteNative(quicConn quic.Connection, packet Packet, buf *bytes.Buffer, fragSize int) (err error) {
	fullPayload := packet.DATA
	off := 0
	fragID := uint8(0)
	fragCount := uint8((len(fullPayload) + fragSize - 1) / fragSize) // round up
	packet.FRAG_TOTAL = fragCount
	for off < len(fullPayload) {
		payloadSize := len(fullPayload) - off
		if payloadSize > fragSize {
			payloadSize = fragSize
		}
		frag := packet
		frag.FRAG_ID = fragID
		frag.SIZE = uint16(payloadSize)
		frag.DATA = fullPayload[off : off+payloadSize]
		off += payloadSize
		fragID++
		buf.Reset()
		err = frag.WriteTo(buf)
		if err != nil {
			return
		}
		data := buf.Bytes()
		err = quicConn.SendMessage(data)
		if err != nil {
			return
		}
		packet.ADDR.TYPE = AtypNone // avoid "fragment 2/2: address in non-first fragment"
	}
	return
}

type deFragger struct {
	lru  *cache.LruCache[uint16, *packetBag]
	once sync.Once
}

type packetBag struct {
	frags []*Packet
	count uint8
	mutex sync.Mutex
}

func newPacketBag() *packetBag {
	return new(packetBag)
}

func (d *deFragger) init() {
	if d.lru == nil {
		d.lru = cache.New(
			cache.WithAge[uint16, *packetBag](10),
			cache.WithUpdateAgeOnGet[uint16, *packetBag](),
		)
	}
}

func (d *deFragger) Feed(m *Packet) *Packet {
	if m.FRAG_TOTAL <= 1 {
		return m
	}
	if m.FRAG_ID >= m.FRAG_TOTAL {
		// wtf is this?
		return nil
	}
	d.once.Do(d.init) // lazy init
	bag, _ := d.lru.GetOrStore(m.PKT_ID, newPacketBag)
	bag.mutex.Lock()
	defer bag.mutex.Unlock()
	if int(m.FRAG_TOTAL) != len(bag.frags) {
		// new message, clear previous state
		bag.frags = make([]*Packet, m.FRAG_TOTAL)
		bag.count = 1
		bag.frags[m.FRAG_ID] = m
		return nil
	}
	if bag.frags[m.FRAG_ID] != nil {
		return nil
	}
	bag.frags[m.FRAG_ID] = m
	bag.count++
	if int(bag.count) != len(bag.frags) {
		return nil
	}

	// all fragments received, assemble
	var data []byte
	for _, frag := range bag.frags {
		data = append(data, frag.DATA...)
	}
	p := *bag.frags[0] // recover from first fragment
	p.SIZE = uint16(len(data))
	p.DATA = data
	p.FRAG_ID = 0
	p.FRAG_TOTAL = 1
	bag.frags = nil
	d.lru.Delete(m.PKT_ID)
	return &p
}
