package socks

import (
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net"
	"strconv"
	"time"
)

var socks5Errors = []string{
	"",
	"general failure",
	"connection forbidden",
	"network unreachable",
	"host unreachable",
	"connection refused",
	"TTL expired",
	"command not supported",
	"address type not supported",
}

type Auth struct {
	User, Password string
}
type ClientConn struct {
	user     string
	password string
	conn     *net.Conn
	header   []byte
	timeout  time.Duration
	addr     string
	network  string
	udpAddr  string
}

// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
// with an optional username and password. See RFC 1928 and RFC 1929.
// target must be a canonical address with a host and port.
// network : tcp udp
func NewClientConn(conn *net.Conn, network, target string, timeout time.Duration, auth *Auth, header []byte) *ClientConn {
	s := &ClientConn{
		conn:    conn,
		network: network,
		timeout: timeout,
	}
	if auth != nil {
		s.user = auth.User
		s.password = auth.Password
	}
	if header != nil && len(header) > 0 {
		s.header = header
	}
	if network == "udp" && target == "" {
		target = "0.0.0.0:1"
	}
	s.addr = target
	return s
}

// connect takes an existing connection to a socks5 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *ClientConn) Handshake() error {
	host, portStr, err := net.SplitHostPort(s.addr)
	if err != nil {
		return err
	}
	port, err := strconv.Atoi(portStr)
	if err != nil {
		return errors.New("proxy: failed to parse port number: " + portStr)
	}
	if port < 1 || port > 0xffff {
		return errors.New("proxy: port number out of range: " + portStr)
	}

	if err := s.handshake(host); err != nil {
		return err
	}
	buf := []byte{}
	if s.network == "tcp" {
		buf = append(buf, VERSION_V5, CMD_CONNECT, 0 /* reserved */)

	} else {
		buf = append(buf, VERSION_V5, CMD_ASSOCIATE, 0 /* reserved */)
	}
	if ip := net.ParseIP(host); ip != nil {
		if ip4 := ip.To4(); ip4 != nil {
			buf = append(buf, ATYP_IPV4)
			ip = ip4
		} else {
			buf = append(buf, ATYP_IPV6)
		}
		buf = append(buf, ip...)
	} else {
		if len(host) > 255 {
			return errors.New("proxy: destination host name too long: " + host)
		}
		buf = append(buf, ATYP_DOMAIN)
		buf = append(buf, byte(len(host)))
		buf = append(buf, host...)
	}
	buf = append(buf, byte(port>>8), byte(port))
	(*s.conn).SetDeadline(time.Now().Add(s.timeout))
	if _, err := (*s.conn).Write(buf); err != nil {
		return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
	}
	(*s.conn).SetDeadline(time.Time{})
	(*s.conn).SetDeadline(time.Now().Add(s.timeout))
	if _, err := io.ReadFull((*s.conn), buf[:4]); err != nil {
		return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
	}
	(*s.conn).SetDeadline(time.Time{})
	failure := "unknown error"
	if int(buf[1]) < len(socks5Errors) {
		failure = socks5Errors[buf[1]]
	}

	if len(failure) > 0 {
		return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
	}

	bytesToDiscard := 0
	switch buf[3] {
	case ATYP_IPV4:
		bytesToDiscard = net.IPv4len
	case ATYP_IPV6:
		bytesToDiscard = net.IPv6len
	case ATYP_DOMAIN:
		(*s.conn).SetDeadline(time.Now().Add(s.timeout))
		_, err := io.ReadFull((*s.conn), buf[:1])
		(*s.conn).SetDeadline(time.Time{})
		if err != nil {
			return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
		}
		bytesToDiscard = int(buf[0])
	default:
		return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
	}

	if cap(buf) < bytesToDiscard {
		buf = make([]byte, bytesToDiscard)
	} else {
		buf = buf[:bytesToDiscard]
	}
	(*s.conn).SetDeadline(time.Now().Add(s.timeout))
	if _, err := io.ReadFull((*s.conn), buf); err != nil {
		return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
	}
	(*s.conn).SetDeadline(time.Time{})
	var ip net.IP
	ip = buf
	ipStr := ""
	if bytesToDiscard == net.IPv4len || bytesToDiscard == net.IPv6len {
		if ipv4 := ip.To4(); ipv4 != nil {
			ipStr = ipv4.String()
		} else {
			ipStr = ip.To16().String()
		}
	}
	//log.Printf("%v", ipStr)
	// Also need to discard the port number
	(*s.conn).SetDeadline(time.Now().Add(s.timeout))
	if _, err := io.ReadFull((*s.conn), buf[:2]); err != nil {
		return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
	}
	p := binary.BigEndian.Uint16([]byte{buf[0], buf[1]})
	//log.Printf("%v", p)
	s.udpAddr = net.JoinHostPort(ipStr, fmt.Sprintf("%d", p))
	//log.Printf("%v", s.udpAddr)
	(*s.conn).SetDeadline(time.Time{})
	return nil
}
func (s *ClientConn) SendUDP(data []byte, addr string) (respData []byte, err error) {

	c, err := net.DialTimeout("udp", s.udpAddr, s.timeout)
	if err != nil {
		return
	}
	conn := c.(*net.UDPConn)

	p := NewPacketUDP()
	p.Build(addr, data)
	conn.SetDeadline(time.Now().Add(s.timeout))
	conn.Write(p.Bytes())
	conn.SetDeadline(time.Time{})

	buf := make([]byte, 1024)
	conn.SetDeadline(time.Now().Add(s.timeout))
	n, _, err := conn.ReadFrom(buf)
	conn.SetDeadline(time.Time{})
	if err != nil {
		return
	}
	respData = buf[:n]
	return
}
func (s *ClientConn) handshake(host string) error {

	// the size here is just an estimate
	buf := make([]byte, 0, 6+len(host))

	buf = append(buf, VERSION_V5)
	if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
		buf = append(buf, 2 /* num auth methods */, Method_NO_AUTH, Method_USER_PASS)
	} else {
		buf = append(buf, 1 /* num auth methods */, Method_NO_AUTH)
	}
	(*s.conn).SetDeadline(time.Now().Add(s.timeout))
	if _, err := (*s.conn).Write(buf); err != nil {
		return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
	}
	(*s.conn).SetDeadline(time.Time{})

	(*s.conn).SetDeadline(time.Now().Add(s.timeout))
	if _, err := io.ReadFull((*s.conn), buf[:2]); err != nil {
		return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
	}
	(*s.conn).SetDeadline(time.Time{})

	if buf[0] != 5 {
		return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
	}
	if buf[1] == 0xff {
		return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
	}

	// See RFC 1929
	if buf[1] == Method_USER_PASS {
		buf = buf[:0]
		buf = append(buf, 1 /* password protocol version */)
		buf = append(buf, uint8(len(s.user)))
		buf = append(buf, s.user...)
		buf = append(buf, uint8(len(s.password)))
		buf = append(buf, s.password...)
		(*s.conn).SetDeadline(time.Now().Add(s.timeout))
		if _, err := (*s.conn).Write(buf); err != nil {
			return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
		}
		(*s.conn).SetDeadline(time.Time{})
		(*s.conn).SetDeadline(time.Now().Add(s.timeout))
		if _, err := io.ReadFull((*s.conn), buf[:2]); err != nil {
			return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
		}
		(*s.conn).SetDeadline(time.Time{})
		if buf[1] != 0 {
			return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
		}
	}
	return nil
}
