//go:build windows

package netroute

// Implementation Warning: mapping of the correct interface ID and index is not
// hooked up.
// Reference:
// https://docs.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getbestroute2
import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
	"unsafe"

	"golang.org/x/sys/windows"
)

var (
	modiphlpapi       = windows.NewLazyDLL("iphlpapi.dll")
	procGetBestRoute2 = modiphlpapi.NewProc("GetBestRoute2")
)

type NetLUID uint64

type AddressPrefix struct {
	*windows.RawSockaddrAny
	PrefixLength byte
}

type RouteProtocol uint32 // MIB_IPFORWARD_PROTO

// https://docs.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2
type mib_row2 struct {
	luid              NetLUID
	index             uint32
	destinationPrefix *AddressPrefix
	nextHop           *windows.RawSockaddrAny
	prefixLength      byte
	lifetime          uint32
	preferredLifetime uint32
	metric            uint32
	protocol          RouteProtocol
	loopback          byte
	autoconfigured    byte
	publish           byte
	immortal          byte
	age               uint32
	origin            byte
}

func callBestRoute(source, dest net.IP) (*mib_row2, net.IP, error) {
	sourceAddr, _, _ := sockaddrToAny(ipAndZoneToSockaddr(source, ""))
	destAddr, _, _ := sockaddrToAny(ipAndZoneToSockaddr(dest, ""))
	bestRoute := make([]byte, 512)
	bestSource := make([]byte, 116)

	err := getBestRoute2(nil, 0, sourceAddr, destAddr, 0, bestRoute, bestSource)
	if err != nil {
		return nil, nil, err
	}

	// interpret best route and best source.
	route, err := parseRoute(bestRoute)
	if err != nil {
		return nil, nil, err
	}

	// per https://docs.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2
	// If the route is to a local loopback address or an IP address on the local link, the next hop is unspecified (all zeros)
	if isZero(route.nextHop) {
		route.nextHop = nil
	}

	var bestSourceRaw windows.RawSockaddrAny
	bestSourceRaw.Addr.Family = binary.LittleEndian.Uint16(bestSource[0:2])
	copyInto(bestSourceRaw.Addr.Data[:], bestSource[2:16])
	copyInto(bestSourceRaw.Pad[:], bestSource[16:])
	addr, _ := bestSourceRaw.Sockaddr()
	bestSrc, _ := sockaddrToIPAndZone(addr)

	return route, bestSrc, nil
}

func copyInto(dst []int8, src []byte) {
	for i, b := range src {
		dst[i] = int8(b)
	}
}

func isZero(addr *windows.RawSockaddrAny) bool {
	for _, b := range addr.Addr.Data {
		if b != 0 {
			return false
		}
	}
	for _, b := range addr.Pad {
		if b != 0 {
			return false
		}
	}
	return true
}

func parseRoute(mib []byte) (*mib_row2, error) {
	var route mib_row2
	var err error

	route.luid = NetLUID(binary.LittleEndian.Uint64(mib[0:]))
	route.index = binary.LittleEndian.Uint32(mib[8:])
	idx := 0
	route.destinationPrefix, idx, err = readDestPrefix(mib, 12)
	if err != nil {
		return nil, err
	}
	route.nextHop, idx, err = readSockAddr(mib, idx)
	if err != nil {
		return nil, err
	}
	route.prefixLength = mib[idx]
	idx += 1
	route.lifetime = binary.LittleEndian.Uint32(mib[idx : idx+4])
	idx += 4
	route.preferredLifetime = binary.LittleEndian.Uint32(mib[idx : idx+4])
	idx += 4
	route.metric = binary.LittleEndian.Uint32(mib[idx : idx+4])
	idx += 4
	route.protocol = RouteProtocol(binary.LittleEndian.Uint32(mib[idx : idx+4]))
	idx += 4
	route.loopback = mib[idx]
	idx += 1
	route.autoconfigured = mib[idx]
	idx += 1
	route.publish = mib[idx]
	idx += 1
	route.immortal = mib[idx]
	idx += 1
	route.age = binary.LittleEndian.Uint32(mib[idx : idx+4])
	idx += 4
	route.origin = mib[idx]

	return &route, err
}

func readDestPrefix(buffer []byte, idx int) (*AddressPrefix, int, error) {
	sock, idx2, err := readSockAddr(buffer, idx)
	if err != nil {
		return nil, 0, err
	}
	pfixLen := buffer[idx2]
	if idx2-idx > 32 {
		return nil, idx, fmt.Errorf("Unexpectedly large internal sockaddr struct")
	}
	return &AddressPrefix{sock, pfixLen}, idx + 32, nil
}

func readSockAddr(buffer []byte, idx int) (*windows.RawSockaddrAny, int, error) {
	var rsa windows.RawSockaddrAny
	rsa.Addr.Family = binary.LittleEndian.Uint16(buffer[idx : idx+2])
	if rsa.Addr.Family == windows.AF_INET || rsa.Addr.Family == windows.AF_UNSPEC {
		copyInto(rsa.Addr.Data[:], buffer[idx+2:idx+16])
		return &rsa, idx + 16, nil
	} else if rsa.Addr.Family == windows.AF_INET6 {
		copyInto(rsa.Addr.Data[:], buffer[idx+2:idx+16])
		copyInto(rsa.Pad[:], buffer[idx+16:idx+28])
		return &rsa, idx + 28, nil
	} else {
		return nil, 0, fmt.Errorf("Unknown windows addr family %d", rsa.Addr.Family)
	}
}

func getBestRoute2(interfaceLuid *NetLUID, interfaceIndex uint32, sourceAddress, destinationAddress *windows.RawSockaddrAny, addressSortOptions uint32, bestRoute []byte, bestSourceAddress []byte) (errcode error) {
	r0, _, _ := procGetBestRoute2.Call(
		uintptr(unsafe.Pointer(interfaceLuid)),
		uintptr(interfaceIndex),
		uintptr(unsafe.Pointer(sourceAddress)),
		uintptr(unsafe.Pointer(destinationAddress)),
		uintptr(addressSortOptions),
		uintptr(unsafe.Pointer(&bestRoute[0])),
		uintptr(unsafe.Pointer(&bestSourceAddress[0])))
	if r0 != 0 {
		errcode = windows.Errno(r0)
	}
	return
}

func getIface(index uint32) *net.Interface {
	var ifRow windows.MibIfRow
	ifRow.Index = index
	err := windows.GetIfEntry(&ifRow)
	if err != nil {
		return nil
	}

	physAddrLen := int(ifRow.PhysAddrLen)
	if len(ifRow.PhysAddr) < physAddrLen && physAddrLen >= 0 {
		physAddrLen = len(ifRow.PhysAddr)
	}
	physAddr := ifRow.PhysAddr[:physAddrLen]

	ifaces, err := net.Interfaces()
	if err != nil {
		return nil
	}
	for _, iface := range ifaces {
		if bytes.Equal(iface.HardwareAddr, physAddr) {
			return &iface
		}
	}
	return nil
}

type winRouter struct{}

func (r *winRouter) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) {
	return r.RouteWithSrc(nil, nil, dst)
}

func (r *winRouter) RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) {
	route, pref, err := callBestRoute(src, dst)
	if err != nil {
		return nil, nil, nil, err
	}
	iface = getIface(route.index)

	if route.nextHop == nil || route.nextHop.Addr.Family == 0 /* AF_UNDEF */ {
		return iface, nil, pref, nil
	}
	addr, err := route.nextHop.Sockaddr()
	if err != nil {
		return nil, nil, nil, err
	}
	nextHop, _ := sockaddrToIPAndZone(addr)

	return iface, nextHop, pref, nil
}

func New() (Router, error) {
	rtr := &winRouter{}
	return rtr, nil
}
