package hysteria

import (
	"crypto/sha256"
	"math/rand"
	"net"
	"sync"
	"time"

	"github.com/sagernet/sing/common"
	"github.com/sagernet/sing/common/buf"
	"github.com/sagernet/sing/common/bufio"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
)

const xplusSaltLen = 16

func NewXPlusPacketConn(conn net.PacketConn, key []byte) net.PacketConn {
	vectorisedWriter, isVectorised := bufio.CreateVectorisedPacketWriter(conn)
	if isVectorised {
		return &VectorisedXPlusConn{
			XPlusPacketConn: XPlusPacketConn{
				PacketConn: conn,
				key:        key[:len(key):len(key)],
				rand:       rand.New(rand.NewSource(time.Now().UnixNano())),
			},
			writer: vectorisedWriter,
		}
	} else {
		return &XPlusPacketConn{
			PacketConn: conn,
			key:        key[:len(key):len(key)],
			rand:       rand.New(rand.NewSource(time.Now().UnixNano())),
		}
	}
}

type XPlusPacketConn struct {
	net.PacketConn
	key        []byte
	randAccess sync.Mutex
	rand       *rand.Rand
}

func (c *XPlusPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
	n, addr, err = c.PacketConn.ReadFrom(p)
	if err != nil {
		return
	} else if n < xplusSaltLen {
		n = 0
		return
	}
	key := sha256.Sum256(append(c.key, p[:xplusSaltLen]...))
	for i := range n - xplusSaltLen {
		p[i] = p[xplusSaltLen+i] ^ key[i%sha256.Size]
	}
	n -= xplusSaltLen
	return
}

func (c *XPlusPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
	// can't use unsafe buffer on WriteTo
	buffer := buf.NewSize(len(p) + xplusSaltLen)
	defer buffer.Release()
	salt := buffer.Extend(xplusSaltLen)
	c.randAccess.Lock()
	_, _ = c.rand.Read(salt)
	c.randAccess.Unlock()
	key := sha256.Sum256(append(c.key, salt...))
	for i := range p {
		common.Must(buffer.WriteByte(p[i] ^ key[i%sha256.Size]))
	}
	return c.PacketConn.WriteTo(buffer.Bytes(), addr)
}

func (c *XPlusPacketConn) Upstream() any {
	return c.PacketConn
}

type VectorisedXPlusConn struct {
	XPlusPacketConn
	writer N.VectorisedPacketWriter
}

func (c *VectorisedXPlusConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
	header := buf.NewSize(xplusSaltLen)
	defer header.Release()
	salt := header.Extend(xplusSaltLen)
	c.randAccess.Lock()
	_, _ = c.rand.Read(salt)
	c.randAccess.Unlock()
	key := sha256.Sum256(append(c.key, salt...))
	for i := range p {
		p[i] ^= key[i%sha256.Size]
	}
	return bufio.WriteVectorisedPacket(c.writer, [][]byte{header.Bytes(), p}, M.SocksaddrFromNet(addr))
}

func (c *VectorisedXPlusConn) WriteVectorisedPacket(buffers []*buf.Buffer, destination M.Socksaddr) error {
	header := buf.NewSize(xplusSaltLen)
	defer header.Release()
	salt := header.Extend(xplusSaltLen)
	c.randAccess.Lock()
	_, _ = c.rand.Read(salt)
	c.randAccess.Unlock()
	key := sha256.Sum256(append(c.key, salt...))
	var index int
	for _, buffer := range buffers {
		data := buffer.Bytes()
		for i := range data {
			data[i] ^= key[index%sha256.Size]
			index++
		}
	}
	buffers = append([]*buf.Buffer{header}, buffers...)
	return c.writer.WriteVectorisedPacket(buffers, destination)
}

func NewXPlusClientConn(conn net.Conn, key []byte) net.Conn {
	vectorisedWriter, isVectorised := bufio.CreateVectorisedWriter(conn)
	if isVectorised {
		return &VectorisedXPlusClientConn{
			XPlusClientConn: XPlusClientConn{
				Conn: conn,
				key:  key[:len(key):len(key)],
				rand: rand.New(rand.NewSource(time.Now().UnixNano())),
			},
			writer: vectorisedWriter,
		}
	} else {
		return &XPlusClientConn{
			Conn: conn,
			key:  key[:len(key):len(key)],
			rand: rand.New(rand.NewSource(time.Now().UnixNano())),
		}
	}
}

type XPlusClientConn struct {
	net.Conn
	key        []byte
	randAccess sync.Mutex
	rand       *rand.Rand
}

func (c *XPlusClientConn) Read(p []byte) (n int, err error) {
	n, err = c.Conn.Read(p)
	if err != nil {
		return
	} else if n < xplusSaltLen {
		n = 0
		return
	}
	key := sha256.Sum256(append(c.key, p[:xplusSaltLen]...))
	for i := range n - xplusSaltLen {
		p[i] = p[xplusSaltLen+i] ^ key[i%sha256.Size]
	}
	n -= xplusSaltLen
	return
}

func (c *XPlusClientConn) Write(p []byte) (n int, err error) {
	buffer := buf.NewSize(len(p) + xplusSaltLen)
	defer buffer.Release()
	salt := buffer.Extend(xplusSaltLen)
	c.randAccess.Lock()
	_, _ = c.rand.Read(salt)
	c.randAccess.Unlock()
	key := sha256.Sum256(append(c.key, salt...))
	for i := range p {
		common.Must(buffer.WriteByte(p[i] ^ key[i%sha256.Size]))
	}
	_, err = c.Conn.Write(buffer.Bytes())
	if err != nil {
		return
	}
	return len(p), nil
}

func (c *XPlusClientConn) Upstream() any {
	return c.Conn
}

type VectorisedXPlusClientConn struct {
	XPlusClientConn
	writer N.VectorisedWriter
}

func (c *VectorisedXPlusClientConn) Write(p []byte) (n int, err error) {
	header := buf.NewSize(xplusSaltLen)
	defer header.Release()
	salt := header.Extend(xplusSaltLen)
	c.randAccess.Lock()
	_, _ = c.rand.Read(salt)
	c.randAccess.Unlock()
	key := sha256.Sum256(append(c.key, salt...))
	for i := range p {
		p[i] ^= key[i%sha256.Size]
	}
	_, err = bufio.WriteVectorised(c.writer, [][]byte{header.Bytes(), p})
	if err != nil {
		return
	}
	return len(p), nil
}
