// Code generated from file "11-allmethods_tmpl.go"; DO NOT EDIT.

// Copyright (c) 2025 Karl Gaissmaier
// SPDX-License-Identifier: MIT

package bart

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"iter"
	"net/netip"
	"slices"
	"strings"

	"github.com/gaissmai/bart/internal/nodes"
)

func (t *Fast[V]) sizeUpdate(is4 bool, delta int) {
	if is4 {
		t.size4 += delta
		return
	}
	t.size6 += delta
}

// Insert adds or updates a prefix-value pair in the routing table.
// If the prefix already exists, its value is updated; otherwise a new entry is created.
// Invalid prefixes are silently ignored.
//
// The prefix is automatically canonicalized using pfx.Masked() to ensure
// consistent behavior regardless of host bits in the input.
func (t *Fast[V]) Insert(pfx netip.Prefix, val V) {
	if !pfx.IsValid() {
		return
	}

	// canonicalize prefix
	pfx = pfx.Masked()

	is4 := pfx.Addr().Is4()
	n := t.rootNodeByVersion(is4)

	if exists := n.Insert(pfx, val, 0); exists {
		return
	}

	// true insert, update size
	t.sizeUpdate(is4, 1)
}

// InsertPersist is similar to Insert but the receiver isn't modified.
//
// All nodes touched during insert are cloned and a new Fast is returned.
// This is not a full [Fast.Clone], all untouched nodes are still referenced
// from both Tables.
//
// If the payload type V contains pointers or needs deep copying,
// it must implement the [bart.Cloner] interface to support correct cloning.
//
// Due to cloning overhead this is significantly slower than Insert,
// typically taking μsec instead of nsec.
func (t *Fast[V]) InsertPersist(pfx netip.Prefix, val V) *Fast[V] {
	if !pfx.IsValid() {
		return t
	}

	// canonicalize prefix
	pfx = pfx.Masked()
	is4 := pfx.Addr().Is4()

	// share size counters; root nodes cloned selectively.
	pt := &Fast[V]{
		size4: t.size4,
		size6: t.size6,
	}

	// Create a cloning function for deep copying values;
	// returns nil if V does not implement the Cloner interface.
	cloneFn := cloneFnFactory[V]()

	// Clone root node corresponding to the IP version, for copy-on-write.
	n := &pt.root4

	if is4 {
		pt.root4 = *t.root4.CloneFlat(cloneFn)
		pt.root6 = t.root6
	} else {
		pt.root4 = t.root4
		pt.root6 = *t.root6.CloneFlat(cloneFn)

		n = &pt.root6
	}

	if !n.InsertPersist(cloneFn, pfx, val, 0) {
		pt.sizeUpdate(is4, 1)
	}

	return pt
}

// Delete removes the exact prefix pfx from the table in-place.
//
// This is an exact-match operation (no LPM). If pfx exists, the entry is
// removed. If pfx does not exist or pfx is invalid, the table is left unchanged.
//
// The prefix is canonicalized (Masked) before lookup.
func (t *Fast[V]) Delete(pfx netip.Prefix) {
	if !pfx.IsValid() {
		return
	}

	// canonicalize prefix
	pfx = pfx.Masked()
	is4 := pfx.Addr().Is4()

	n := t.rootNodeByVersion(is4)
	if exists := n.Delete(pfx); exists {
		t.sizeUpdate(is4, -1)
	}
}

// Get performs an exact-prefix lookup and returns whether the exact
// prefix exists. The prefix is canonicalized (Masked) before lookup.
//
// This is an exact-match operation (no LPM). The prefix must match exactly
// in both address and prefix length to be found. If pfx exists, the
// associated value (zero value for Lite) and found=true is returned.
// If pfx does not exist or pfx is invalid, the zero value for V and
// exists=false is returned.
//
// For longest-prefix-match (LPM) lookups, use Contains(ip), Lookup(ip),
// LookupPrefix(pfx) or LookupPrefixLPM(pfx) instead.
func (t *Fast[V]) Get(pfx netip.Prefix) (val V, exists bool) {
	if !pfx.IsValid() {
		return val, exists
	}
	// canonicalize prefix
	pfx = pfx.Masked()

	is4 := pfx.Addr().Is4()
	n := t.rootNodeByVersion(is4)

	return n.Get(pfx)
}

// DeletePersist is similar to Delete but does not modify the receiver.
//
// It performs a copy-on-write delete operation, cloning all nodes touched during
// deletion and returning a new Fast reflecting the change.
//
// If the prefix is invalid or doesn't exist, the original table is
// returned unchanged.
//
// If the payload type V contains pointers or requires deep copying,
// it must implement the [bart.Cloner] interface for correct cloning.
//
// Due to cloning overhead this is significantly slower than Delete,
// typically taking μsec instead of nsec.
func (t *Fast[V]) DeletePersist(pfx netip.Prefix) *Fast[V] {
	if !pfx.IsValid() {
		return t
	}

	// canonicalize prefix
	pfx = pfx.Masked()
	is4 := pfx.Addr().Is4()

	// Preflight check: avoid cloning if prefix doesn't exist
	n := t.rootNodeByVersion(is4)
	if _, found := n.Get(pfx); !found {
		return t
	}

	// share size counters; root nodes cloned selectively.
	pt := &Fast[V]{
		size4: t.size4,
		size6: t.size6,
	}

	// Create a cloning function for deep copying values;
	// returns nil if V does not implement the Cloner interface.
	cloneFn := cloneFnFactory[V]()

	// Clone root node corresponding to the IP version, for copy-on-write.
	if is4 {
		pt.root4 = *t.root4.CloneFlat(cloneFn)
		pt.root6 = t.root6
		n = &pt.root4
	} else {
		pt.root4 = t.root4
		pt.root6 = *t.root6.CloneFlat(cloneFn)
		n = &pt.root6
	}

	if exists := n.DeletePersist(cloneFn, pfx); exists {
		pt.sizeUpdate(is4, -1)
	}

	return pt
}

// Modify applies an insert, update, or delete operation for the value
// associated with the given prefix. The supplied callback decides the
// operation: it is called with the current value (or zero if not found)
// and a boolean indicating whether the prefix exists. The callback must
// return a new value and a delete flag: del == false inserts or updates,
// del == true deletes the entry if it exists (otherwise no-op).
//
// The operation is determined by the callback function, which is called with:
//
//	val:   the current value (or zero value if not found)
//	found: true if the prefix currently exists, false otherwise
//
// The callback returns:
//
//	val: the new value to insert or update (ignored if del == true)
//	del: true to delete the entry, false to insert or update
//
// Summary of callback semantics:
//
//	| cb-input        | cb-return       | Ops    |
//	------------------------------------- --------
//	| (zero,   false) | (_,      true)  | no-op  |
//	| (zero,   false) | (newVal, false) | insert |
//	| (oldVal, true)  | (newVal, false) | update |
//	| (oldVal, true)  | (_,      true)  | delete |
//	------------------------------------- --------
func (t *Fast[V]) Modify(pfx netip.Prefix, cb func(_ V, ok bool) (_ V, del bool)) {
	if !pfx.IsValid() {
		return
	}

	// canonicalize prefix
	pfx = pfx.Masked()

	is4 := pfx.Addr().Is4()

	n := t.rootNodeByVersion(is4)

	delta := n.Modify(pfx, cb)
	t.sizeUpdate(is4, delta)
}

// ModifyPersist is similar to Modify but the receiver isn't modified and
// a new *Fast is returned.
func (t *Fast[V]) ModifyPersist(pfx netip.Prefix, cb func(_ V, ok bool) (_ V, del bool)) *Fast[V] {
	if !pfx.IsValid() {
		return t
	}

	// make a cheap test in front of expensive operation
	oldVal, ok := t.Get(pfx)
	val := oldVal

	// to clone or not to clone ...
	cloneFn := cloneFnFactory[V]()
	if cloneFn != nil && ok {
		val = cloneFn(oldVal)
	}

	newVal, del := cb(val, ok)

	switch {
	case !ok && del: // no-op
		return t

	case !ok && !del: // insert
		return t.InsertPersist(pfx, newVal)

	case ok && !del: // update
		return t.InsertPersist(pfx, newVal)

	case ok && del: // delete
		return t.DeletePersist(pfx)
	}

	panic("unreachable")
}

// Supernets returns an iterator over all supernet routes that cover the given prefix pfx.
//
// The traversal searches both exact-length and shorter (less specific) prefixes that
// include pfx. Starting from the most specific position in the trie,
// it walks upward through parent nodes and yields any matching entries found at each level.
//
// The iteration order is reverse-CIDR: from longest prefix match (LPM) towards
// least-specific routes.
//
// This can be used to enumerate all covering supernet routes in routing-based
// policy engines, diagnostics tools, or fallback resolution logic.
//
// Example:
//
//	for supernet, val := range table.Supernets(netip.MustParsePrefix("192.0.2.128/25")) {
//	    fmt.Println("Covered by:", supernet, "->", val)
//	}
//
// The iteration can be stopped early by breaking from the range loop.
// Returns an empty iterator if the prefix is invalid.
func (t *Fast[V]) Supernets(pfx netip.Prefix) iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		if !pfx.IsValid() {
			return
		}

		// canonicalize the prefix
		pfx = pfx.Masked()

		is4 := pfx.Addr().Is4()
		n := t.rootNodeByVersion(is4)

		n.Supernets(pfx, yield)
	}
}

// Subnets returns an iterator over all subnets of the given prefix
// in natural CIDR sort order. This includes prefixes of the same length
// (exact match) and longer (more specific) prefixes that are contained
// within the given prefix.
//
// Example:
//
//	for sub, val := range table.Subnets(netip.MustParsePrefix("10.0.0.0/8")) {
//	    fmt.Println("Covered:", sub, "->", val)
//	}
//
// The iteration can be stopped early by breaking from the range loop.
// Returns an empty iterator if the prefix is invalid.
func (t *Fast[V]) Subnets(pfx netip.Prefix) iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		if !pfx.IsValid() {
			return
		}

		pfx = pfx.Masked()
		is4 := pfx.Addr().Is4()

		n := t.rootNodeByVersion(is4)
		n.Subnets(pfx, yield)
	}
}

// OverlapsPrefix reports whether any prefix in the routing table overlaps with
// the given prefix. Two prefixes overlap if they share any IP addresses.
//
// The check is bidirectional: it returns true if the input prefix is covered by an existing
// route, or if any stored route is itself contained within the input prefix.
//
// Internally, the function normalizes the prefix and descends the relevant trie branch,
// using stride-based logic to identify overlap without performing a full lookup.
//
// This is useful for containment tests, route validation, or policy checks using prefix
// semantics without retrieving exact matches.
func (t *Fast[V]) OverlapsPrefix(pfx netip.Prefix) bool {
	if !pfx.IsValid() {
		return false
	}

	// canonicalize the prefix
	pfx = pfx.Masked()

	is4 := pfx.Addr().Is4()
	n := t.rootNodeByVersion(is4)

	return n.OverlapsPrefixAtDepth(pfx, 0)
}

// Overlaps reports whether any route in the receiver table overlaps
// with a route in the other table, in either direction.
//
// The overlap check is bidirectional: it returns true if any IP prefix
// in the receiver is covered by the other table, or vice versa.
// This includes partial overlaps, exact matches, and supernet/subnet relationships.
//
// Both IPv4 and IPv6 route trees are compared independently. If either
// tree has overlapping routes, the function returns true.
//
// This is useful for conflict detection, policy enforcement,
// or validating mutually exclusive routing domains.
func (t *Fast[V]) Overlaps(o *Fast[V]) bool {
	if o == nil {
		return false
	}
	return t.Overlaps4(o) || t.Overlaps6(o)
}

// Overlaps4 is like [Fast.Overlaps] but for the v4 routing table only.
func (t *Fast[V]) Overlaps4(o *Fast[V]) bool {
	if o == nil || t.size4 == 0 || o.size4 == 0 {
		return false
	}
	return t.root4.Overlaps(&o.root4, 0)
}

// Overlaps6 is like [Fast.Overlaps] but for the v6 routing table only.
func (t *Fast[V]) Overlaps6(o *Fast[V]) bool {
	if o == nil || t.size6 == 0 || o.size6 == 0 {
		return false
	}
	return t.root6.Overlaps(&o.root6, 0)
}

// Union merges another routing table into the receiver table, modifying it in-place.
//
// All prefixes and values from the other table (o) are inserted into the receiver.
// If a duplicate prefix exists in both tables, the value from o replaces the existing entry.
// This duplicate is shallow-copied by default, but if the value type V implements the
// Cloner interface, the value is deeply cloned before insertion. See also Fast.Clone.
func (t *Fast[V]) Union(o *Fast[V]) {
	if o == nil || o == t || (o.size4 == 0 && o.size6 == 0) {
		return
	}

	// Create a cloning function for deep copying values;
	// returns nil if V does not implement the Cloner interface.
	cloneFn := cloneFnFactory[V]()

	dup4 := t.root4.UnionRec(cloneFn, &o.root4, 0)
	dup6 := t.root6.UnionRec(cloneFn, &o.root6, 0)

	t.size4 += o.size4 - dup4
	t.size6 += o.size6 - dup6
}

// UnionPersist is similar to [Union] but the receiver isn't modified.
//
// All nodes touched during union are cloned and a new *Fast is returned.
// If o is nil or empty, no nodes are touched and the receiver may be
// returned unchanged.
func (t *Fast[V]) UnionPersist(o *Fast[V]) *Fast[V] {
	if o == nil || o == t || (o.size4 == 0 && o.size6 == 0) {
		return t
	}

	// Create a cloning function for deep copying values;
	// returns nil if V does not implement the Cloner interface.
	cloneFn := cloneFnFactory[V]()

	// new Fast with root nodes just copied.
	pt := &Fast[V]{
		root4: t.root4,
		root6: t.root6,
		//
		size4: t.size4,
		size6: t.size6,
	}

	// only clone the root node if there is something to union
	if o.size4 != 0 {
		pt.root4 = *t.root4.CloneFlat(cloneFn)
	}
	if o.size6 != 0 {
		pt.root6 = *t.root6.CloneFlat(cloneFn)
	}

	dup4 := pt.root4.UnionRecPersist(cloneFn, &o.root4, 0)
	dup6 := pt.root6.UnionRecPersist(cloneFn, &o.root6, 0)

	pt.size4 += o.size4 - dup4
	pt.size6 += o.size6 - dup6

	return pt
}

// Equal checks whether two tables are structurally and semantically equal.
// It ensures both trees (IPv4-based and IPv6-based) have the same sizes and
// recursively compares their root nodes.
func (t *Fast[V]) Equal(o *Fast[V]) bool {
	if o == nil || t.size4 != o.size4 || t.size6 != o.size6 {
		return false
	}
	if o == t {
		return true
	}

	return t.root4.EqualRec(&o.root4) && t.root6.EqualRec(&o.root6)
}

// Clone returns a copy of the routing table.
// The payload of type V is shallow copied, but if type V implements the [Cloner] interface,
// the values are cloned.
func (t *Fast[V]) Clone() *Fast[V] {
	if t == nil {
		return nil
	}

	c := new(Fast[V])

	cloneFn := cloneFnFactory[V]()

	c.root4 = *t.root4.CloneRec(cloneFn)
	c.root6 = *t.root6.CloneRec(cloneFn)

	c.size4 = t.size4
	c.size6 = t.size6

	return c
}

// Size returns the prefix count.
func (t *Fast[V]) Size() int {
	return t.size4 + t.size6
}

// Size4 returns the IPv4 prefix count.
func (t *Fast[V]) Size4() int {
	return t.size4
}

// Size6 returns the IPv6 prefix count.
func (t *Fast[V]) Size6() int {
	return t.size6
}

// All returns an iterator over all prefix–value pairs in the table.
//
// The entries from both IPv4 and IPv6 subtries are yielded using an internal recursive traversal.
// The iteration order is unspecified and may vary between calls; for a stable order, use AllSorted.
//
// You can use All directly in a for-range loop without providing a yield function.
// The Go compiler automatically synthesizes the yield callback for you:
//
//	for prefix, value := range t.All() {
//	    fmt.Println(prefix, value)
//	}
//
// Under the hood, the loop body is passed as a yield function to the iterator.
// If you break or return from the loop, iteration stops early as expected.
//
// IMPORTANT: Modifying or deleting entries during iteration is not allowed,
// as this would interfere with the internal traversal and may corrupt or
// prematurely terminate the iteration. If mutation of the table during
// traversal is required use persistent table methods, e.g.
//
//	pt := t // shallow copy of t
//	for pfx, val := range t.All() {
//		if cond(pfx, val) {
//		  pt = pt.DeletePersist(pfx)
//	  }
//	}
func (t *Fast[V]) All() iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		_ = t.root4.AllRec(stridePath{}, 0, true, yield) && t.root6.AllRec(stridePath{}, 0, false, yield)
	}
}

// All4 is like [Fast.All] but only for the v4 routing table.
func (t *Fast[V]) All4() iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		_ = t.root4.AllRec(stridePath{}, 0, true, yield)
	}
}

// All6 is like [Fast.All] but only for the v6 routing table.
func (t *Fast[V]) All6() iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		_ = t.root6.AllRec(stridePath{}, 0, false, yield)
	}
}

// AllSorted returns an iterator over all prefix–value pairs in the table,
// ordered in canonical CIDR prefix sort order.
//
// This can be used directly with a for-range loop;
// the Go compiler provides the yield function implicitly:
//
//	for prefix, value := range t.AllSorted() {
//	    fmt.Println(prefix, value)
//	}
//
// The traversal is stable and predictable across calls.
// Iteration stops early if you break out of the loop.
//
// IMPORTANT: Deleting entries during iteration is not allowed,
// as this would interfere with the internal traversal and may corrupt or
// prematurely terminate the iteration. If mutation of the table during
// traversal is required use persistent table methods.
func (t *Fast[V]) AllSorted() iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		_ = t.root4.AllRecSorted(stridePath{}, 0, true, yield) &&
			t.root6.AllRecSorted(stridePath{}, 0, false, yield)
	}
}

// AllSorted4 is like [Fast.AllSorted] but only for the v4 routing table.
func (t *Fast[V]) AllSorted4() iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		_ = t.root4.AllRecSorted(stridePath{}, 0, true, yield)
	}
}

// AllSorted6 is like [Fast.AllSorted] but only for the v6 routing table.
func (t *Fast[V]) AllSorted6() iter.Seq2[netip.Prefix, V] {
	return func(yield func(netip.Prefix, V) bool) {
		if t == nil {
			return
		}
		_ = t.root6.AllRecSorted(stridePath{}, 0, false, yield)
	}
}

// Fprint writes a hierarchical tree diagram of the ordered CIDRs
// with default formatted payload V to w.
//
// The order from top to bottom is in ascending order of the prefix address
// and the subtree structure is determined by the CIDRs coverage.
//
//	▼
//	├─ 10.0.0.0/8 (V)
//	│  ├─ 10.0.0.0/24 (V)
//	│  └─ 10.0.1.0/24 (V)
//	├─ 127.0.0.0/8 (V)
//	│  └─ 127.0.0.1/32 (V)
//	├─ 169.254.0.0/16 (V)
//	├─ 172.16.0.0/12 (V)
//	└─ 192.168.0.0/16 (V)
//	   └─ 192.168.1.0/24 (V)
//	▼
//	└─ ::/0 (V)
//	   ├─ ::1/128 (V)
//	   ├─ 2000::/3 (V)
//	   │  └─ 2001:db8::/32 (V)
//	   └─ fe80::/10 (V)
func (t *Fast[V]) Fprint(w io.Writer) error {
	if w == nil {
		return fmt.Errorf("nil writer")
	}
	if t == nil {
		return nil
	}

	// v4
	if err := t.fprint(w, true); err != nil {
		return err
	}

	// v6
	if err := t.fprint(w, false); err != nil {
		return err
	}

	return nil
}

// fprint is the version dependent adapter to fprintRec.
func (t *Fast[V]) fprint(w io.Writer, is4 bool) error {
	n := t.rootNodeByVersion(is4)
	if n.IsEmpty() {
		return nil
	}

	if _, err := fmt.Fprint(w, "▼\n"); err != nil {
		return err
	}

	startParent := nodes.TrieItem[V]{
		Node: nil,
		Idx:  0,
		Path: stridePath{},
		Is4:  is4,
	}

	return n.FprintRec(w, startParent, "", shouldPrintValues[V]())
}

// MarshalText implements the [encoding.TextMarshaler] interface,
// just a wrapper for [Fast.Fprint].
func (t *Fast[V]) MarshalText() ([]byte, error) {
	w := new(bytes.Buffer)
	if err := t.Fprint(w); err != nil {
		return nil, err
	}

	return w.Bytes(), nil
}

// MarshalJSON dumps the table into two sorted lists: for ipv4 and ipv6.
// Every root and subnet is an array, not a map, because the order matters.
func (t *Fast[V]) MarshalJSON() ([]byte, error) {
	if t == nil {
		return []byte("null"), nil
	}

	result := struct {
		Ipv4 []DumpListNode[V] `json:"ipv4,omitempty"`
		Ipv6 []DumpListNode[V] `json:"ipv6,omitempty"`
	}{
		Ipv4: t.DumpList4(),
		Ipv6: t.DumpList6(),
	}

	buf, err := json.Marshal(result)
	if err != nil {
		return nil, err
	}

	return buf, nil
}

// DumpList4 dumps the ipv4 tree into a list of roots and their subnets.
// It can be used to analyze the tree or build the text or JSON serialization.
func (t *Fast[V]) DumpList4() []DumpListNode[V] {
	if t == nil {
		return nil
	}
	return t.dumpListRec(&t.root4, 0, stridePath{}, 0, true)
}

// DumpList6 dumps the ipv6 tree into a list of roots and their subnets.
// It can be used to analyze the tree or build custom JSON representation.
func (t *Fast[V]) DumpList6() []DumpListNode[V] {
	if t == nil {
		return nil
	}
	return t.dumpListRec(&t.root6, 0, stridePath{}, 0, false)
}

// dumpListRec, build the data structure rec-descent with the help of directItemsRec.
// anyNode is nodes.BartNode, nodes.FastNode or nodes.LiteNode
func (t *Fast[V]) dumpListRec(anyNode any, parentIdx uint8, path stridePath, depth int, is4 bool) []DumpListNode[V] {
	// recursion stop condition
	if anyNode == nil {
		return nil
	}

	// the same method is generated for all table types, therefore
	// type assert to the smallest needed interface.
	// The panic on wrong type assertion is by intention, MUST NOT happen
	n := anyNode.(interface {
		DirectItemsRec(uint8, stridePath, int, bool) []nodes.TrieItem[V]
	})

	directItems := n.DirectItemsRec(parentIdx, path, depth, is4)

	// sort the items by prefix
	slices.SortFunc(directItems, func(a, b nodes.TrieItem[V]) int {
		return cmpPrefix(a.Cidr, b.Cidr)
	})

	dumpNodes := make([]DumpListNode[V], 0, len(directItems))

	for _, item := range directItems {
		dumpNodes = append(dumpNodes, DumpListNode[V]{
			CIDR:  item.Cidr,
			Value: item.Val,
			// build it rec-descent, item.Node is also from type any
			Subnets: t.dumpListRec(item.Node, item.Idx, item.Path, item.Depth, is4),
		})
	}

	return dumpNodes
}

// dumpString is just a wrapper for dump.
func (t *Fast[V]) dumpString() string {
	w := new(strings.Builder)
	t.dump(w)

	return w.String()
}

// dump the table structure and all the nodes to w.
func (t *Fast[V]) dump(w io.Writer) {
	if t == nil {
		return
	}

	if t.size4 > 0 {
		stats := t.root4.StatsRec()
		fmt.Fprintln(w)
		fmt.Fprintf(w, "### IPv4: size(%d), subnodes(%d), prefixes(%d), leaves(%d), fringes(%d)",
			t.size4, stats.SubNodes, stats.Prefixes, stats.Leaves, stats.Fringes)

		t.root4.DumpRec(w, stridePath{}, 0, true, shouldPrintValues[V]())
	}

	if t.size6 > 0 {
		stats := t.root6.StatsRec()
		fmt.Fprintln(w)
		fmt.Fprintf(w, "### IPv6: size(%d), subnodes(%d), prefixes(%d), leaves(%d), fringes(%d)",
			t.size6, stats.SubNodes, stats.Prefixes, stats.Leaves, stats.Fringes)

		t.root6.DumpRec(w, stridePath{}, 0, false, shouldPrintValues[V]())
	}
}
