package multicast

import (
	
	
	
	
	

	
	
)

// Conn is multicast connection.
type Conn struct {
	laddr *net.UDPAddr
	pconn *ipv4.PacketConn

	// ifps stores pointers of multicast interface.
	ifps []*net.Interface
}

type connConfig struct {
	ttl   int
	sysIf bool
}

// Listen starts to receiving multicast messages.
func ( *AddrResolver,  ...ConnOption) (*Conn, error) {
	// prepare parameters.
	,  := .resolve()
	if  != nil {
		return nil, 
	}
	// configure connection
	var  connConfig
	for ,  := range  {
		.apply(&)
	}
	// connect.
	,  := net.ListenUDP("udp4", )
	if  != nil {
		return nil, 
	}
	// configure socket to use with multicast.
	, ,  := newIPv4MulticastConn(, .sysIf)
	if  != nil {
		.Close()
		return nil, 
	}
	// set TTL
	if .ttl > 0 {
		 := .SetTTL(.ttl)
		if  != nil {
			.Close()
			return nil, 
		}
	}
	return &Conn{
		laddr: ,
		pconn: ,
		ifps:  ,
	}, nil
}

// newIPv4MulticastConn create a new multicast connection.
// 2nd return parameter will be nil when sysIf is true.
func newIPv4MulticastConn( *net.UDPConn,  bool) (*ipv4.PacketConn, []*net.Interface, error) {
	// sysIf: use system assigned multicast interface.
	// the empty iflist indicate it.
	var  []*net.Interface
	if ! {
		,  := interfaces()
		if  != nil {
			return nil, nil, 
		}
		 = make([]*net.Interface, 0, len())
		for  := range  {
			 = append(, &[])
		}
	}
	,  := SendAddr()
	if  != nil {
		return nil, nil, 
	}
	,  := joinGroupIPv4(, , )
	if  != nil {
		return nil, nil, 
	}
	return , , nil
}

// joinGroupIPv4 makes the connection join to a group on interfaces.
// This trys to use system assigned when iflist is nil or empty.
func joinGroupIPv4( *net.UDPConn,  []*net.Interface,  net.Addr) (*ipv4.PacketConn, error) {
	 := ipv4.NewPacketConn()
	.SetMulticastLoopback(true)

	// try to use the system assigned multicast interface when iflist is empty.
	if len() == 0 {
		if  := .JoinGroup(nil, );  != nil {
			ssdplog.Printf("failed to join group %s on system assigned multicast interface: %s", .String(), )
			return nil, errors.New("no system assigned multicast interfaces had joined to group")
		}
		ssdplog.Printf("joined group %s on system assigned multicast interface", .String())
		return , nil
	}

	// add interfaces to multicast group.
	 := 0
	for ,  := range  {
		if  := .JoinGroup(, );  != nil {
			ssdplog.Printf("failed to join group %s on %s: %s", .String(), .Name, )
			continue
		}
		++
		ssdplog.Printf("joined group %s on %s (#%d)", .String(), .Name, .Index)
	}
	if  == 0 {
		return nil, errors.New("no interfaces had joined to group")
	}
	return , nil
}

// Close closes a multicast connection.
func ( *Conn) () error {
	// based net.UDPConn will be closed by mc.pconn.Close()
	return .pconn.Close()
}

// DataProvider provides a body of multicast message to send.
type DataProvider interface {
	Bytes(*net.Interface) []byte
}

type BytesDataProvider []byte

func ( BytesDataProvider) ( *net.Interface) []byte {
	return []byte()
}

// WriteTo sends a multicast message to interfaces.
func ( *Conn) ( DataProvider,  net.Addr) (int, error) {
	// Send a multicast message directory when recipient "to" address is not multicast.
	if ,  := .(*net.UDPAddr);  && !.IP.IsMulticast() {
		return .writeToIfi(, , nil)
	}
	// Send a multicast message to all interfaces (iflist).
	 := 0
	for ,  := range .ifps {
		ssdplog.Printf("WriteTo: ifi=%+v", )
		,  := .writeToIfi(, , )
		if  != nil {
			return 0, 
		}
		 += 
	}
	return , nil
}

func ( *Conn) ( DataProvider,  net.Addr,  *net.Interface) (int, error) {
	if  := .pconn.SetMulticastInterface();  != nil {
		return 0, 
	}
	return .pconn.WriteTo(.Bytes(), nil, )
}

// LocalAddr returns local address to listen multicast packets.
func ( *Conn) () net.Addr {
	return .laddr
}

// ReadPackets reads multicast packets.
func ( *Conn) ( time.Duration,  PacketHandler) error {
	 := make([]byte, 65535)
	if  > 0 {
		.pconn.SetReadDeadline(time.Now().Add())
	}
	for {
		, , ,  := .pconn.ReadFrom()
		if  != nil {
			if ,  := .(net.Error);  && .Timeout() {
				return nil
			}
			if strings.Contains(.Error(), "use of closed network connection") {
				return io.EOF
			}
			return 
		}
		if  := (, [:]);  != nil {
			return 
		}
	}
}

// ConnOption is option for Listen()
type ConnOption interface {
	apply(cfg *connConfig)
}

type connOptFunc func(*connConfig)

func ( connOptFunc) ( *connConfig) {
	()
}

// ConnTTL returns as ConnOption that set default TTL to the connection.
func ( int) ConnOption {
	return connOptFunc(func( *connConfig) {
		.ttl = 
	})
}

func () ConnOption {
	return connOptFunc(func( *connConfig) {
		.sysIf = true
	})
}