package manet

import (
	"fmt"
	"net"

	ma "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
	mautp "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr-net/utp"
)

// Conn is the equivalent of a net.Conn object. It is the
// result of calling the Dial or Listen functions in this
// package, with associated local and remote Multiaddrs.
type Conn interface {
	net.Conn

	// LocalMultiaddr returns the local Multiaddr associated
	// with this connection
	LocalMultiaddr() ma.Multiaddr

	// RemoteMultiaddr returns the remote Multiaddr associated
	// with this connection
	RemoteMultiaddr() ma.Multiaddr
}

// WrapNetConn wraps a net.Conn object with a Multiaddr
// friendly Conn.
func WrapNetConn(nconn net.Conn) (Conn, error) {
	if nconn == nil {
		return nil, fmt.Errorf("failed to convert nconn.LocalAddr: nil")
	}

	laddr, err := FromNetAddr(nconn.LocalAddr())
	if err != nil {
		return nil, fmt.Errorf("failed to convert nconn.LocalAddr: %s", err)
	}

	raddr, err := FromNetAddr(nconn.RemoteAddr())
	if err != nil {
		return nil, fmt.Errorf("failed to convert nconn.RemoteAddr: %s", err)
	}

	return &maConn{
		Conn:  nconn,
		laddr: laddr,
		raddr: raddr,
	}, nil
}

// maConn implements the Conn interface. It's a thin wrapper
// around a net.Conn
type maConn struct {
	net.Conn
	laddr ma.Multiaddr
	raddr ma.Multiaddr
}

// LocalMultiaddr returns the local address associated with
// this connection
func (c *maConn) LocalMultiaddr() ma.Multiaddr {
	return c.laddr
}

// RemoteMultiaddr returns the remote address associated with
// this connection
func (c *maConn) RemoteMultiaddr() ma.Multiaddr {
	return c.raddr
}

// Dialer contains options for connecting to an address. It
// is effectively the same as net.Dialer, but its LocalAddr
// and RemoteAddr options are Multiaddrs, instead of net.Addrs.
type Dialer struct {

	// Dialer is just an embedded net.Dialer, with all its options.
	net.Dialer

	// LocalAddr is the local address to use when dialing an
	// address. The address must be of a compatible type for the
	// network being dialed.
	// If nil, a local address is automatically chosen.
	LocalAddr ma.Multiaddr
}

// Dial connects to a remote address, using the options of the
// Dialer. Dialer uses an underlying net.Dialer to Dial a
// net.Conn, then wraps that in a Conn object (with local and
// remote Multiaddrs).
func (d *Dialer) Dial(remote ma.Multiaddr) (Conn, error) {

	// if a LocalAddr is specified, use it on the embedded dialer.
	if d.LocalAddr != nil {
		// convert our multiaddr to net.Addr friendly
		naddr, err := ToNetAddr(d.LocalAddr)
		if err != nil {
			return nil, err
		}

		// set the dialer's LocalAddr as naddr
		d.Dialer.LocalAddr = naddr
	}

	// get the net.Dial friendly arguments from the remote addr
	rnet, rnaddr, err := DialArgs(remote)
	if err != nil {
		return nil, err
	}

	// ok, Dial!
	var nconn net.Conn
	switch rnet {
	case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
		nconn, err = d.Dialer.Dial(rnet, rnaddr)
		if err != nil {
			return nil, err
		}
	case "utp", "utp4", "utp6":
		utpd := mautp.Dialer{
			Timeout:   d.Timeout,
			LocalAddr: d.Dialer.LocalAddr,
		}
		// construct utp dialer, with options on our net.Dialer
		nconn, err = utpd.Dial(rnet, rnaddr)
		if err != nil {
			return nil, err
		}
	}

	// get local address (pre-specified or assigned within net.Conn)
	local := d.LocalAddr
	if local == nil {
		local, err = FromNetAddr(nconn.LocalAddr())
		if err != nil {
			return nil, err
		}
	}

	return &maConn{
		Conn:  nconn,
		laddr: local,
		raddr: remote,
	}, nil
}

// Dial connects to a remote address. It uses an underlying net.Conn,
// then wraps it in a Conn object (with local and remote Multiaddrs).
func Dial(remote ma.Multiaddr) (Conn, error) {
	return (&Dialer{}).Dial(remote)
}

// A Listener is a generic network listener for stream-oriented protocols.
// it uses an embedded net.Listener, overriding net.Listener.Accept to
// return a Conn and providing Multiaddr.
type Listener interface {

	// NetListener returns the embedded net.Listener. Use with caution.
	NetListener() net.Listener

	// Accept waits for and returns the next connection to the listener.
	// Returns a Multiaddr friendly Conn
	Accept() (Conn, error)

	// Close closes the listener.
	// Any blocked Accept operations will be unblocked and return errors.
	Close() error

	// Multiaddr returns the listener's (local) Multiaddr.
	Multiaddr() ma.Multiaddr

	// Addr returns the net.Listener's network address.
	Addr() net.Addr
}

// maListener implements Listener
type maListener struct {
	net.Listener
	laddr ma.Multiaddr
}

// NetListener returns the embedded net.Listener. Use with caution.
func (l *maListener) NetListener() net.Listener {
	return l.Listener
}

// Accept waits for and returns the next connection to the listener.
// Returns a Multiaddr friendly Conn
func (l *maListener) Accept() (Conn, error) {
	nconn, err := l.Listener.Accept()
	if err != nil {
		return nil, err
	}

	raddr, err := FromNetAddr(nconn.RemoteAddr())
	if err != nil {
		return nil, fmt.Errorf("failed to convert connn.RemoteAddr: %s", err)
	}

	return &maConn{
		Conn:  nconn,
		laddr: l.laddr,
		raddr: raddr,
	}, nil
}

// Multiaddr returns the listener's (local) Multiaddr.
func (l *maListener) Multiaddr() ma.Multiaddr {
	return l.laddr
}

// Addr returns the listener's network address.
func (l *maListener) Addr() net.Addr {
	return l.Listener.Addr()
}

// Listen announces on the local network address laddr.
// The Multiaddr must be a "ThinWaist" stream-oriented network:
// ip4/tcp, ip6/tcp, (TODO: unix, unixpacket)
// See Dial for the syntax of laddr.
func Listen(laddr ma.Multiaddr) (Listener, error) {

	// get the net.Listen friendly arguments from the remote addr
	lnet, lnaddr, err := DialArgs(laddr)
	if err != nil {
		return nil, err
	}

	var nl net.Listener
	switch lnet {
	case "utp", "utp4", "utp6":
		nl, err = mautp.Listen(lnet, lnaddr)
	default:
		nl, err = net.Listen(lnet, lnaddr)
	}
	if err != nil {
		return nil, err
	}

	// we want to fetch the new multiaddr from the listener, as it may
	// have resolved to some other value. WrapNetListener does it for us.
	return WrapNetListener(nl)
}

// WrapNetListener wraps a net.Listener with a manet.Listener.
func WrapNetListener(nl net.Listener) (Listener, error) {
	laddr, err := FromNetAddr(nl.Addr())
	if err != nil {
		return nil, err
	}

	return &maListener{
		Listener: nl,
		laddr:    laddr,
	}, nil
}

// InterfaceMultiaddrs will return the addresses matching net.InterfaceAddrs
func InterfaceMultiaddrs() ([]ma.Multiaddr, error) {
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return nil, err
	}

	maddrs := make([]ma.Multiaddr, len(addrs))
	for i, a := range addrs {
		maddrs[i], err = FromNetAddr(a)
		if err != nil {
			return nil, err
		}
	}
	return maddrs, nil
}

// AddrMatch returns the Multiaddrs that match the protocol stack on addr
func AddrMatch(match ma.Multiaddr, addrs []ma.Multiaddr) []ma.Multiaddr {

	// we should match transports entirely.
	p1s := match.Protocols()

	out := make([]ma.Multiaddr, 0, len(addrs))
	for _, a := range addrs {
		p2s := a.Protocols()
		if len(p1s) != len(p2s) {
			continue
		}

		match := true
		for i, p2 := range p2s {
			if p1s[i].Code != p2.Code {
				match = false
				break
			}
		}
		if match {
			out = append(out, a)
		}
	}
	return out
}
