package dhcpv6

import (
	"fmt"
	"net"

	"github.com/u-root/uio/uio"
)

// Opt4RD represents a 4RD option. It is only a container for 4RD_*_RULE options
type Opt4RD Options

// Code returns the Option Code for this option
func (op *Opt4RD) Code() OptionCode {
	return Option4RD
}

// ToBytes serializes this option
func (op *Opt4RD) ToBytes() []byte {
	return (*Options)(op).ToBytes()
}

// String returns a human-readable representation of the option
func (op *Opt4RD) String() string {
	return fmt.Sprintf("Opt4RD{%v}", (*Options)(op))
}

// ParseOpt4RD builds an Opt4RD structure from a sequence of bytes.
// The input data does not include option code and length bytes
func ParseOpt4RD(data []byte) (*Opt4RD, error) {
	var opt Options
	err := opt.FromBytes(data)
	return (*Opt4RD)(&opt), err
}

// Opt4RDMapRule represents a 4RD Mapping Rule option
// The option is described in https://tools.ietf.org/html/rfc7600#section-4.9
// The 4RD mapping rules are described in https://tools.ietf.org/html/rfc7600#section-4.2
type Opt4RDMapRule struct {
	// Prefix4 is the IPv4 prefix mapped by this rule
	Prefix4 net.IPNet
	// Prefix6 is the IPv6 prefix mapped by this rule
	Prefix6 net.IPNet
	// EABitsLength is the number of bits of an address used in constructing the mapped address
	EABitsLength uint8
	// WKPAuthorized determines if well-known ports are assigned to addresses in an A+P mapping
	// It can only be set if the length of Prefix4 + EABits > 32
	WKPAuthorized bool
}

const (
	// opt4RDWKPAuthorizedMask is the mask for the WKPAuthorized flag in its
	// byte in Opt4RDMapRule
	opt4RDWKPAuthorizedMask = 1 << 7
	// opt4RDHubAndSpokeMask is the mask for the HubAndSpoke flag in its
	// byte in Opt4RDNonMapRule
	opt4RDHubAndSpokeMask = 1 << 7
	// opt4RDTrafficClassMask is the mask for the TrafficClass flag in its
	// byte in Opt4RDNonMapRule
	opt4RDTrafficClassMask = 1 << 0
)

// Code returns the option code representing this option
func (op *Opt4RDMapRule) Code() OptionCode { return Option4RDMapRule }

// ToBytes serializes this option
func (op *Opt4RDMapRule) ToBytes() []byte {
	buf := uio.NewBigEndianBuffer(nil)
	p4Len, _ := op.Prefix4.Mask.Size()
	p6Len, _ := op.Prefix6.Mask.Size()
	buf.Write8(uint8(p4Len))
	buf.Write8(uint8(p6Len))
	buf.Write8(op.EABitsLength)
	if op.WKPAuthorized {
		buf.Write8(opt4RDWKPAuthorizedMask)
	} else {
		buf.Write8(0)
	}
	if op.Prefix4.IP.To4() == nil {
		// The API prevents us from returning an error here
		// We just write zeros instead, which is pretty bad behaviour
		buf.Write32(0)
	} else {
		buf.WriteBytes(op.Prefix4.IP.To4())
	}
	if op.Prefix6.IP.To16() == nil {
		buf.Write64(0)
		buf.Write64(0)
	} else {
		buf.WriteBytes(op.Prefix6.IP.To16())
	}
	return buf.Data()
}

// String returns a human-readable description of this option
func (op *Opt4RDMapRule) String() string {
	return fmt.Sprintf("Opt4RDMapRule{Prefix4=%s, Prefix6=%s, EA-Bits=%d, WKPAuthorized=%t}",
		op.Prefix4.String(), op.Prefix6.String(), op.EABitsLength, op.WKPAuthorized)
}

// ParseOpt4RDMapRule builds an Opt4RDMapRule structure from a sequence of bytes.
// The input data does not include option code and length bytes.
func ParseOpt4RDMapRule(data []byte) (*Opt4RDMapRule, error) {
	var opt Opt4RDMapRule
	buf := uio.NewBigEndianBuffer(data)
	opt.Prefix4.Mask = net.CIDRMask(int(buf.Read8()), 32)
	opt.Prefix6.Mask = net.CIDRMask(int(buf.Read8()), 128)
	opt.EABitsLength = buf.Read8()
	opt.WKPAuthorized = (buf.Read8() & opt4RDWKPAuthorizedMask) != 0
	opt.Prefix4.IP = net.IP(buf.CopyN(net.IPv4len))
	opt.Prefix6.IP = net.IP(buf.CopyN(net.IPv6len))
	return &opt, buf.FinError()
}

// Opt4RDNonMapRule represents 4RD parameters other than mapping rules
type Opt4RDNonMapRule struct {
	// HubAndSpoke is whether the network topology is hub-and-spoke or meshed
	HubAndSpoke bool
	// TrafficClass is an optional 8-bit tunnel traffic class identifier
	TrafficClass *uint8
	// DomainPMTU is the Path MTU for this 4RD domain
	DomainPMTU uint16
}

// Code returns the option code for this option
func (op *Opt4RDNonMapRule) Code() OptionCode {
	return Option4RDNonMapRule
}

// ToBytes serializes this option
func (op *Opt4RDNonMapRule) ToBytes() []byte {
	buf := uio.NewBigEndianBuffer(nil)
	var flags uint8
	var trafficClassValue uint8
	if op.HubAndSpoke {
		flags |= opt4RDHubAndSpokeMask
	}
	if op.TrafficClass != nil {
		flags |= opt4RDTrafficClassMask
		trafficClassValue = *op.TrafficClass
	}

	buf.Write8(flags)
	buf.Write8(trafficClassValue)
	buf.Write16(op.DomainPMTU)

	return buf.Data()
}

// String returns a human-readable description of this option
func (op *Opt4RDNonMapRule) String() string {
	var tClass interface{} = false
	if op.TrafficClass != nil {
		tClass = *op.TrafficClass
	}

	return fmt.Sprintf("Opt4RDNonMapRule{HubAndSpoke=%t, TrafficClass=%v, DomainPMTU=%d}",
		op.HubAndSpoke, tClass, op.DomainPMTU)
}

// ParseOpt4RDNonMapRule builds an Opt4RDNonMapRule structure from a sequence of bytes.
// The input data does not include option code and length bytes
func ParseOpt4RDNonMapRule(data []byte) (*Opt4RDNonMapRule, error) {
	var opt Opt4RDNonMapRule
	buf := uio.NewBigEndianBuffer(data)
	flags := buf.Read8()

	opt.HubAndSpoke = flags&opt4RDHubAndSpokeMask != 0

	tClass := buf.Read8()
	if flags&opt4RDTrafficClassMask != 0 {
		opt.TrafficClass = &tClass
	}

	opt.DomainPMTU = buf.Read16()

	return &opt, buf.FinError()
}
