package manet

import (
	"errors"
	"fmt"
	"net"
	"path/filepath"
	"runtime"
	"strings"

	ma "github.com/multiformats/go-multiaddr"
)

var errIncorrectNetAddr = fmt.Errorf("incorrect network addr conversion")
var errNotIP = fmt.Errorf("multiaddr does not start with an IP address")

// FromNetAddr converts a net.Addr type to a Multiaddr.
func FromNetAddr(a net.Addr) (ma.Multiaddr, error) {
	return defaultCodecs.FromNetAddr(a)
}

// FromNetAddr converts a net.Addr to Multiaddress.
func (cm *CodecMap) FromNetAddr(a net.Addr) (ma.Multiaddr, error) {
	if a == nil {
		return nil, fmt.Errorf("nil multiaddr")
	}
	p, err := cm.getAddrParser(a.Network())
	if err != nil {
		return nil, err
	}

	return p(a)
}

// ToNetAddr converts a Multiaddr to a net.Addr
// Must be ThinWaist. acceptable protocol stacks are:
// /ip{4,6}/{tcp, udp}
func ToNetAddr(maddr ma.Multiaddr) (net.Addr, error) {
	return defaultCodecs.ToNetAddr(maddr)
}

// ToNetAddr converts a Multiaddress to a standard net.Addr.
func (cm *CodecMap) ToNetAddr(maddr ma.Multiaddr) (net.Addr, error) {
	protos := maddr.Protocols()
	final := protos[len(protos)-1]

	p, err := cm.getMaddrParser(final.Name)
	if err != nil {
		return nil, err
	}

	return p(maddr)
}

// MultiaddrToIPNet converts a multiaddr to an IPNet. Useful for seeing if another IP address is contained within this multiaddr network+mask
func MultiaddrToIPNet(m ma.Multiaddr) (*net.IPNet, error) {
	var ipString string
	var mask string

	ma.ForEach(m, func(c ma.Component) bool {
		if c.Protocol().Code == ma.P_IP4 || c.Protocol().Code == ma.P_IP6 {
			ipString = c.Value()
		}
		if c.Protocol().Code == ma.P_IPCIDR {
			mask = c.Value()
		}
		return ipString == "" || mask == ""
	})

	if ipString == "" {
		return nil, errors.New("no ip protocol found")
	}

	if mask == "" {
		return nil, errors.New("no mask found")
	}

	_, ipnet, err := net.ParseCIDR(ipString + "/" + string(mask))
	return ipnet, err
}

func parseBasicNetMaddr(maddr ma.Multiaddr) (net.Addr, error) {
	network, host, err := DialArgs(maddr)
	if err != nil {
		return nil, err
	}

	switch network {
	case "tcp", "tcp4", "tcp6":
		return net.ResolveTCPAddr(network, host)
	case "udp", "udp4", "udp6":
		return net.ResolveUDPAddr(network, host)
	case "ip", "ip4", "ip6":
		return net.ResolveIPAddr(network, host)
	case "unix":
		return net.ResolveUnixAddr(network, host)
	}

	return nil, fmt.Errorf("network not supported: %s", network)
}

func FromIPAndZone(ip net.IP, zone string) (ma.Multiaddr, error) {
	switch {
	case ip.To4() != nil:
		return ma.NewComponent("ip4", ip.String())
	case ip.To16() != nil:
		ip6, err := ma.NewComponent("ip6", ip.String())
		if err != nil {
			return nil, err
		}
		if zone == "" {
			return ip6, nil
		} else {
			zone, err := ma.NewComponent("ip6zone", zone)
			if err != nil {
				return nil, err
			}
			return zone.Encapsulate(ip6), nil
		}
	default:
		return nil, errIncorrectNetAddr
	}
}

// FromIP converts a net.IP type to a Multiaddr.
func FromIP(ip net.IP) (ma.Multiaddr, error) {
	return FromIPAndZone(ip, "")
}

// ToIP converts a Multiaddr to a net.IP when possible
func ToIP(addr ma.Multiaddr) (net.IP, error) {
	var ip net.IP
	ma.ForEach(addr, func(c ma.Component) bool {
		switch c.Protocol().Code {
		case ma.P_IP6ZONE:
			// we can't return these anyways.
			return true
		case ma.P_IP6, ma.P_IP4:
			ip = net.IP(c.RawValue())
			return false
		}
		return false
	})
	if ip == nil {
		return nil, errNotIP
	}
	return ip, nil
}

// DialArgs is a convenience function that returns network and address as
// expected by net.Dial. See https://godoc.org/net#Dial for an overview of
// possible return values (we do not support the unixpacket ones yet). Unix
// addresses do not, at present, compose.
func DialArgs(m ma.Multiaddr) (string, string, error) {
	zone, network, ip, port, hostname, err := dialArgComponents(m)
	if err != nil {
		return "", "", err
	}

	// If we have a hostname (dns*), we don't want any fancy ipv6 formatting
	// logic (zone, brackets, etc.).
	if hostname {
		switch network {
		case "ip", "ip4", "ip6":
			return network, ip, nil
		case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
			return network, ip + ":" + port, nil
		}
		// Hostname is only true when network is one of the above.
		panic("unreachable")
	}

	switch network {
	case "ip6":
		if zone != "" {
			ip += "%" + zone
		}
		fallthrough
	case "ip4":
		return network, ip, nil
	case "tcp4", "udp4":
		return network, ip + ":" + port, nil
	case "tcp6", "udp6":
		if zone != "" {
			ip += "%" + zone
		}
		return network, "[" + ip + "]" + ":" + port, nil
	case "unix":
		if runtime.GOOS == "windows" {
			// convert /c:/... to c:\...
			ip = filepath.FromSlash(strings.TrimLeft(ip, "/"))
		}
		return network, ip, nil
	default:
		return "", "", fmt.Errorf("%s is not a 'thin waist' address", m)
	}
}

// dialArgComponents extracts the raw pieces used in dialing a Multiaddr
func dialArgComponents(m ma.Multiaddr) (zone, network, ip, port string, hostname bool, err error) {
	ma.ForEach(m, func(c ma.Component) bool {
		switch network {
		case "":
			switch c.Protocol().Code {
			case ma.P_IP6ZONE:
				if zone != "" {
					err = fmt.Errorf("%s has multiple zones", m)
					return false
				}
				zone = c.Value()
				return true
			case ma.P_IP6:
				network = "ip6"
				ip = c.Value()
				return true
			case ma.P_IP4:
				if zone != "" {
					err = fmt.Errorf("%s has ip4 with zone", m)
					return false
				}
				network = "ip4"
				ip = c.Value()
				return true
			case ma.P_DNS:
				network = "ip"
				hostname = true
				ip = c.Value()
				return true
			case ma.P_DNS4:
				network = "ip4"
				hostname = true
				ip = c.Value()
				return true
			case ma.P_DNS6:
				network = "ip6"
				hostname = true
				ip = c.Value()
				return true
			case ma.P_UNIX:
				network = "unix"
				ip = c.Value()
				return false
			}
		case "ip":
			switch c.Protocol().Code {
			case ma.P_UDP:
				network = "udp"
			case ma.P_TCP:
				network = "tcp"
			default:
				return false
			}
			port = c.Value()
		case "ip4":
			switch c.Protocol().Code {
			case ma.P_UDP:
				network = "udp4"
			case ma.P_TCP:
				network = "tcp4"
			default:
				return false
			}
			port = c.Value()
		case "ip6":
			switch c.Protocol().Code {
			case ma.P_UDP:
				network = "udp6"
			case ma.P_TCP:
				network = "tcp6"
			default:
				return false
			}
			port = c.Value()
		}
		// Done.
		return false
	})
	return
}

func parseTCPNetAddr(a net.Addr) (ma.Multiaddr, error) {
	ac, ok := a.(*net.TCPAddr)
	if !ok {
		return nil, errIncorrectNetAddr
	}

	// Get IP Addr
	ipm, err := FromIPAndZone(ac.IP, ac.Zone)
	if err != nil {
		return nil, errIncorrectNetAddr
	}

	// Get TCP Addr
	tcpm, err := ma.NewMultiaddr(fmt.Sprintf("/tcp/%d", ac.Port))
	if err != nil {
		return nil, errIncorrectNetAddr
	}

	// Encapsulate
	return ipm.Encapsulate(tcpm), nil
}

func parseUDPNetAddr(a net.Addr) (ma.Multiaddr, error) {
	ac, ok := a.(*net.UDPAddr)
	if !ok {
		return nil, errIncorrectNetAddr
	}

	// Get IP Addr
	ipm, err := FromIPAndZone(ac.IP, ac.Zone)
	if err != nil {
		return nil, errIncorrectNetAddr
	}

	// Get UDP Addr
	udpm, err := ma.NewMultiaddr(fmt.Sprintf("/udp/%d", ac.Port))
	if err != nil {
		return nil, errIncorrectNetAddr
	}

	// Encapsulate
	return ipm.Encapsulate(udpm), nil
}

func parseIPNetAddr(a net.Addr) (ma.Multiaddr, error) {
	ac, ok := a.(*net.IPAddr)
	if !ok {
		return nil, errIncorrectNetAddr
	}
	return FromIPAndZone(ac.IP, ac.Zone)
}

func parseIPPlusNetAddr(a net.Addr) (ma.Multiaddr, error) {
	ac, ok := a.(*net.IPNet)
	if !ok {
		return nil, errIncorrectNetAddr
	}
	return FromIP(ac.IP)
}

func parseUnixNetAddr(a net.Addr) (ma.Multiaddr, error) {
	ac, ok := a.(*net.UnixAddr)
	if !ok {
		return nil, errIncorrectNetAddr
	}

	path := ac.Name
	if runtime.GOOS == "windows" {
		// Convert c:\foobar\... to c:/foobar/...
		path = filepath.ToSlash(path)
	}
	if len(path) == 0 || path[0] != '/' {
		// convert "" and "c:/..." to "/..."
		path = "/" + path
	}

	return ma.NewComponent("unix", path)
}
