package vless

import (
	"encoding/binary"
	"io"
	"net"
	"strconv"

	"github.com/nadoo/glider/pool"
)

// Atyp is vless addr type.
type Atyp byte

// Atyp
const (
	AtypErr    Atyp = 0
	AtypIP4    Atyp = 1
	AtypDomain Atyp = 2
	AtypIP6    Atyp = 3
)

// Addr is vless addr.
type Addr []byte

// MaxHostLen is the maximum size of host in bytes.
const MaxHostLen = 255

// Port is vless addr port.
type Port uint16

// ParseAddr parses the address in string s.
func ParseAddr(s string) (Atyp, Addr, Port, error) {
	var atyp Atyp
	var addr Addr

	host, port, err := net.SplitHostPort(s)
	if err != nil {
		return 0, nil, 0, err
	}

	if ip := net.ParseIP(host); ip != nil {
		if ip4 := ip.To4(); ip4 != nil {
			addr = make([]byte, net.IPv4len)
			atyp = AtypIP4
			copy(addr[:], ip4)
		} else {
			addr = make([]byte, net.IPv6len)
			atyp = AtypIP6
			copy(addr[:], ip)
		}
	} else {
		if len(host) > MaxHostLen {
			return 0, nil, 0, err
		}
		addr = make([]byte, 1+len(host))
		atyp = AtypDomain
		addr[0] = byte(len(host))
		copy(addr[1:], host)
	}

	portnum, err := strconv.ParseUint(port, 10, 16)
	if err != nil {
		return 0, nil, 0, err
	}

	return atyp, addr, Port(portnum), err
}

// ReadAddr reads just enough bytes from r to get addr.
func ReadAddr(r io.Reader) (atyp Atyp, host Addr, port Port, err error) {
	buf := pool.GetBuffer(2)
	defer pool.PutBuffer(buf)

	// port
	_, err = io.ReadFull(r, buf[:2])
	if err != nil {
		return
	}
	port = Port(binary.BigEndian.Uint16(buf[:2]))

	// atyp
	_, err = io.ReadFull(r, buf[:1])
	if err != nil {
		return
	}
	atyp = Atyp(buf[0])

	switch atyp {
	case AtypIP4:
		host = make([]byte, net.IPv4len)
		_, err = io.ReadFull(r, host)
		return
	case AtypIP6:
		host = make([]byte, net.IPv6len)
		_, err = io.ReadFull(r, host)
		return
	case AtypDomain:
		_, err = io.ReadFull(r, buf[:1])
		if err != nil {
			return
		}
		host = make([]byte, int(buf[0]))
		_, err = io.ReadFull(r, host)
		return
	}

	return
}

// ReadAddrString reads just enough bytes from r to get addr string.
func ReadAddrString(r io.Reader) (string, error) {
	atyp, host, port, err := ReadAddr(r)
	if err != nil {
		return "", err
	}
	return AddrString(atyp, host, port), nil
}

// AddrString returns a addr string in format of "host:port".
func AddrString(atyp Atyp, addr Addr, port Port) string {
	var host string

	switch atyp {
	case AtypIP4, AtypIP6:
		host = net.IP(addr).String()
	case AtypDomain:
		host = string(addr)
	}

	return net.JoinHostPort(host, strconv.Itoa(int(port)))
}
