package internal

import (
	"net"
	"sync"
	"time"

	v2net "v2ray.com/core/common/net"
	"v2ray.com/core/common/signal"
)

type ConnectionRecyler interface {
	Put(ConnectionId, net.Conn)
}

type ConnectionId struct {
	Local      v2net.Address
	Remote     v2net.Address
	RemotePort v2net.Port
}

func NewConnectionId(source v2net.Address, dest v2net.Destination) ConnectionId {
	return ConnectionId{
		Local:      source,
		Remote:     dest.Address,
		RemotePort: dest.Port,
	}
}

type ExpiringConnection struct {
	conn   net.Conn
	expire time.Time
}

func (o *ExpiringConnection) Expired() bool {
	return o.expire.Before(time.Now())
}

type Pool struct {
	sync.Mutex
	connsByDest map[ConnectionId][]*ExpiringConnection
	cleanupOnce signal.Once
}

func NewConnectionPool() *Pool {
	return &Pool{
		connsByDest: make(map[ConnectionId][]*ExpiringConnection),
	}
}

func (o *Pool) Get(id ConnectionId) net.Conn {
	o.Lock()
	defer o.Unlock()

	list, found := o.connsByDest[id]
	if !found {
		return nil
	}
	connIdx := -1
	for idx, conn := range list {
		if !conn.Expired() {
			connIdx = idx
			break
		}
	}
	if connIdx == -1 {
		return nil
	}
	listLen := len(list)
	conn := list[connIdx]
	if connIdx != listLen-1 {
		list[connIdx] = list[listLen-1]
	}
	list = list[:listLen-1]
	o.connsByDest[id] = list
	return conn.conn
}

func (o *Pool) Cleanup() {
	defer o.cleanupOnce.Reset()

	for len(o.connsByDest) > 0 {
		time.Sleep(time.Second * 5)
		expiredConns := make([]net.Conn, 0, 16)
		o.Lock()
		for dest, list := range o.connsByDest {
			validConns := make([]*ExpiringConnection, 0, len(list))
			for _, conn := range list {
				if conn.Expired() {
					expiredConns = append(expiredConns, conn.conn)
				} else {
					validConns = append(validConns, conn)
				}
			}
			if len(validConns) != len(list) {
				o.connsByDest[dest] = validConns
			}
		}
		o.Unlock()
		for _, conn := range expiredConns {
			conn.Close()
		}
	}
}

func (o *Pool) Put(id ConnectionId, conn net.Conn) {
	expiringConn := &ExpiringConnection{
		conn:   conn,
		expire: time.Now().Add(time.Second * 4),
	}

	o.Lock()
	defer o.Unlock()

	list, found := o.connsByDest[id]
	if !found {
		list = []*ExpiringConnection{expiringConn}
	} else {
		list = append(list, expiringConn)
	}
	o.connsByDest[id] = list

	o.cleanupOnce.Do(func() {
		go o.Cleanup()
	})
}
