module net

import io.util
import os

union AddrData {
	Unix
	Ip
	Ip6
}

const (
	addr_ip6_any = [16]byte{init: byte(0)}
	addr_ip_any  = [4]byte{init: byte(0)}
)

fn new_ip6(port u16, addr [16]byte) Addr {
	a := Addr{
		f: u16(AddrFamily.ip6)
		addr: AddrData{
			Ip6: Ip6{
				port: u16(C.htons(port))
			}
		}
	}

	copy(a.addr.Ip6.addr[0..], addr[0..])

	return a
}

fn new_ip(port u16, addr [4]byte) Addr {
	a := Addr{
		f: u16(AddrFamily.ip)
		addr: AddrData{
			Ip: Ip{
				port: u16(C.htons(port))
			}
		}
	}

	copy(a.addr.Ip6.addr[0..], addr[0..])

	return a
}

fn temp_unix() ?Addr {
	// create a temp file to get a filename
	// close it
	// remove it
	// then reuse the filename
	mut file, filename := util.temp_file() ?
	file.close()
	os.rm(filename) ?
	addrs := resolve_addrs(filename, .unix, .udp) ?
	return addrs[0]
}

pub fn (a Addr) family() AddrFamily {
	return AddrFamily(a.f)
}

const (
	max_ip_len  = 24
	max_ip6_len = 46
)

fn (a Ip) str() string {
	buf := []byte{len: net.max_ip_len, init: 0}

	res := &char(C.inet_ntop(.ip, &a.addr, buf.data, buf.len))

	if res == 0 {
		return '<Unknown>'
	}

	saddr := buf.bytestr()
	port := C.ntohs(a.port)

	return '$saddr:$port'
}

fn (a Ip6) str() string {
	buf := []byte{len: net.max_ip6_len, init: 0}

	res := &char(C.inet_ntop(.ip6, &a.addr, buf.data, buf.len))

	if res == 0 {
		return '<Unknown>'
	}

	saddr := buf.bytestr()
	port := C.ntohs(a.port)

	return '[$saddr]:$port'
}

const aoffset = __offsetof(Addr, addr)

fn (a Addr) len() u32 {
	match a.family() {
		.ip {
			return sizeof(Ip) + net.aoffset
		}
		.ip6 {
			return sizeof(Ip6) + net.aoffset
		}
		.unix {
			return sizeof(Unix) + net.aoffset
		}
		else {
			panic('Unknown address family')
		}
	}
}

pub fn resolve_addrs(addr string, family AddrFamily, @type SocketType) ?[]Addr {
	match family {
		.ip, .ip6, .unspec {
			return resolve_ipaddrs(addr, family, @type)
		}
		.unix {
			resolved := Unix{}

			if addr.len > max_unix_path {
				return error('net: resolve_addrs Unix socket address is too long')
			}

			// Copy the unix path into the address struct
			unsafe {
				C.memcpy(&resolved.path, addr.str, addr.len)
			}

			return [Addr{
				f: u16(AddrFamily.unix)
				addr: AddrData{
					Unix: resolved
				}
			}]
		}
	}
}

pub fn resolve_addrs_fuzzy(addr string, @type SocketType) ?[]Addr {
	if addr.len == 0 {
		return none
	}

	// Use a small heuristic to figure out what address family this is
	// (out of the ones that we support)

	if addr.contains(':') {
		// Colon is a reserved character in unix paths
		// so this must be an ip address
		return resolve_addrs(addr, .unspec, @type)
	}

	return resolve_addrs(addr, .unix, @type)
}

pub fn resolve_ipaddrs(addr string, family AddrFamily, typ SocketType) ?[]Addr {
	address, port := split_address(addr) ?

	if addr[0] == `:` {
		// Use in6addr_any
		return [new_ip6(port, net.addr_ip6_any)]
	}

	mut hints := C.addrinfo{
		// ai_family: int(family)
		// ai_socktype: int(typ)
		// ai_flags: C.AI_PASSIVE
	}
	hints.ai_family = int(family)
	hints.ai_socktype = int(typ)
	hints.ai_flags = C.AI_PASSIVE
	hints.ai_protocol = 0
	hints.ai_addrlen = 0
	hints.ai_addr = voidptr(0)
	hints.ai_canonname = voidptr(0)
	hints.ai_next = voidptr(0)
	results := &C.addrinfo(0)

	sport := '$port'

	// This might look silly but is recommended by MSDN
	$if windows {
		socket_error(0 - C.getaddrinfo(&char(address.str), &char(sport.str), &hints, &results)) ?
	} $else {
		x := C.getaddrinfo(&char(address.str), &char(sport.str), &hints, &results)
		wrap_error(x) ?
	}

	defer {
		C.freeaddrinfo(results)
	}

	// Now that we have our linked list of addresses
	// convert them into an array
	mut addresses := []Addr{}

	for result := results; !isnil(result); result = result.ai_next {
		match AddrFamily(result.ai_family) {
			.ip, .ip6 {
				new_addr := Addr{
					addr: AddrData{
						Ip6: Ip6{}
					}
				}
				unsafe {
					C.memcpy(&new_addr, result.ai_addr, result.ai_addrlen)
				}
				addresses << new_addr
			}
			else {
				panic('Unexpected address family $result.ai_family')
			}
		}
	}

	return addresses
}

fn (a Addr) str() string {
	match AddrFamily(a.f) {
		.ip {
			unsafe {
				return a.addr.Ip.str()
			}
		}
		.ip6 {
			unsafe {
				return a.addr.Ip6.str()
			}
		}
		.unix {
			unsafe {
				return tos_clone(a.addr.Unix.path[0..max_unix_path].data)
			}
		}
		.unspec {
			return '<.unspec>'
		}
	}
}

pub fn addr_from_socket_handle(handle int) Addr {
	addr := Addr{
		addr: AddrData{
			Ip6: Ip6{}
		}
	}
	size := sizeof(addr)

	C.getsockname(handle, voidptr(&addr), &size)

	return addr
}
