package d2fonts

import (
	
	
	
	
	
	
	
	
	
	
	

	gotextfont 
	
	

	
)

// FallbackFont is one fully resolved host font face. Data is owned by the
// result and may be retained in a network-free renderer scene. FaceIndex
// selects a face when Data contains an OpenType collection.
type FallbackFont struct {
	Name      string
	MIMEType  string
	Data      []byte
	FaceIndex uint16
}

// FallbackRequest carries both missing code points and the source text style.
// Resolvers may ignore style when a face is intrinsically universal, but must
// preserve it when delegating so bold, italic, semibold, and monospace text do
// not silently fall back to an unrelated regular proportional face.
type FallbackRequest struct {
	Runes  []rune
	Family string
	Style  string
	Weight int
}

// FallbackResolver resolves an ordered set of font faces covering runes not
// present in a D2 label's configured primary font. An empty or partial result
// means the remaining runes have no available face; it is not an error.
// Implementations must return owned immutable bytes and must honor context
// cancellation between bounded I/O operations. Malformed resources and work
// or byte limit violations remain errors.
type FallbackResolver interface {
	ResolveFallbacks(context.Context, FallbackRequest) ([]FallbackFont, error)
}

// SystemFallbackLimits bounds deterministic host-font discovery for the
// lifetime of one resolver. FileBytes bounds one candidate; every other
// work/byte limit is cumulative across indexing attempts and ResolveFallbacks
// calls. A CLI export owns one resolver, so these are operation-wide ceilings
// rather than per-board or per-style allowances.
type SystemFallbackLimits struct {
	MaxDirectoryEntries int
	MaxFiles            int
	MaxFaces            int
	MaxRequestedRunes   int
	MaxCoverageChecks   int64
	MaxFileBytes        int64
	MaxScannedBytes     int64
	MaxResolvedBytes    int64
}

func ( SystemFallbackLimits) () error {
	if .MaxDirectoryEntries <= 0 || .MaxFiles <= 0 || .MaxFaces <= 0 || .MaxRequestedRunes <= 0 || .MaxCoverageChecks <= 0 || .MaxFileBytes <= 0 || .MaxScannedBytes <= 0 || .MaxResolvedBytes <= 0 {
		return fmt.Errorf("d2fonts: every system fallback limit must be positive")
	}
	return nil
}

// SystemFallbackResolver discovers fonts only under the operating system's
// conventional system font roots. It does not follow symlinks or consult a
// platform API, keeping discovery CGO-free and auditable. User-installed D2
// primary fonts continue to flow through AddFontFamily and do not need this
// resolver.
type SystemFallbackResolver struct {
	limits SystemFallbackLimits
	roots  []string

	resolveOnce sync.Once
	resolveGate chan struct{}

	pathsMu      sync.Mutex
	pathsIndexed bool
	indexedPaths []string
	pathFlight   chan struct{}

	work systemFallbackWork
}

type systemFallbackWork struct {
	directoryEntries int
	files            int
	requestedRunes   int64
	scannedBytes     int64
	faces            int
	coverageChecks   int64
	resolvedBytes    int64
}

type fallbackFaceProfile struct {
	family string
	italic bool
	weight int
	mono   bool
}

func ( SystemFallbackLimits) (*SystemFallbackResolver, error) {
	if  := .validate();  != nil {
		return nil, 
	}
	return &SystemFallbackResolver{limits: }, nil
}

func ( *SystemFallbackResolver) ( context.Context,  FallbackRequest) ([]FallbackFont, error) {
	if  == nil {
		return nil, fmt.Errorf("d2fonts: nil system fallback resolver")
	}
	if  == nil {
		 = context.Background()
	}
	if  := .Err();  != nil {
		return nil, 
	}
	,  := .acquireResolve()
	if  != nil {
		return nil, 
	}
	defer ()
	if  := .Err();  != nil {
		return nil, 
	}
	if len(.Family)+len(.Style) > 1_024 {
		return nil, fmt.Errorf("d2fonts: fallback request font metadata exceeds 1024 bytes")
	}
	if len(.Runes) > .limits.MaxRequestedRunes {
		return nil, fmt.Errorf("d2fonts: requested fallback rune count %d exceeds limit %d", len(.Runes), .limits.MaxRequestedRunes)
	}
	if int64(len(.Runes)) > int64(.limits.MaxRequestedRunes)-.work.requestedRunes {
		return nil, fmt.Errorf("d2fonts: requested fallback rune count exceeds cumulative resolver limit %d", .limits.MaxRequestedRunes)
	}
	.work.requestedRunes += int64(len(.Runes))
	,  := uniqueFallbackRunes(, .Runes)
	if  != nil {
		return nil, 
	}
	if len() == 0 {
		return nil, nil
	}
	 := newFallbackRequestProfile(, )
	 := .roots
	if  == nil {
		 = systemFontRoots()
	}
	if len() == 0 {
		return nil, nil
	}
	,  := .cachedFontPaths(, , )
	if  != nil {
		return nil, 
	}

	var  []FallbackFont
	for ,  := range  {
		if  := .Err();  != nil {
			return nil, 
		}
		,  := probeFontFileCoverage(
			, , .limits.MaxFileBytes, .limits.MaxScannedBytes-.work.scannedBytes,
			.limits.MaxFaces-.work.faces, ,
		)
		if  != nil {
			.work.scannedBytes += .bytes
			return nil, fmt.Errorf("d2fonts: system font %q: %w", filepath.Base(), )
		}
		if .faces == 0 {
			.work.scannedBytes += .bytes
			continue
		}
		if .faces > .limits.MaxFaces-.work.faces {
			.work.scannedBytes += .bytes
			return nil, fmt.Errorf("d2fonts: system font face count exceeds cumulative resolver limit %d while resolving %U", .limits.MaxFaces, firstRune())
		}
		if !.covers {
			.work.faces += .faces
			.work.scannedBytes += .bytes
			continue
		}
		,  := readBoundedFontFile(, .limits.MaxFileBytes, .limits.MaxScannedBytes-.work.scannedBytes)
		if  != nil {
			.work.scannedBytes += .bytes
			return nil, fmt.Errorf("d2fonts: system font %q: %w", filepath.Base(), )
		}
		if len() == 0 {
			.work.scannedBytes += .bytes
			continue
		}
		.work.scannedBytes += int64(len())
		if  := .Err();  != nil {
			return nil, 
		}
		 := .limits.MaxFaces - .work.faces
		if  <= 0 {
			return nil, fmt.Errorf("d2fonts: system font face count exceeds cumulative resolver limit %d while resolving %U", .limits.MaxFaces, firstRune())
		}
		,  := fontface.ParseFaceCollectionWithLimit(, )
		if  != nil {
			var  *fontface.FaceCountLimitError
			if errors.As(, &) {
				return nil, fmt.Errorf("d2fonts: system font %q: %w", filepath.Base(), )
			}
			continue
		}
		if .NumFaces() > .limits.MaxFaces-.work.faces {
			return nil, fmt.Errorf("d2fonts: system font face count exceeds cumulative resolver limit %d while resolving %U", .limits.MaxFaces, firstRune())
		}
		.work.faces += .NumFaces()
		 := make(map[int]bool)
		 := make([]fallbackFaceProfile, .NumFaces())
		 := make([]bool, .NumFaces())
		for len() != 0 {
			, ,  := -1, 0, []rune(nil)
			for  := 0;  < .NumFaces(); ++ {
				if  > int(^uint16(0)) {
					break
				}
				if [] {
					continue
				}
				,  := .Face()
				if  != nil {
					continue
				}
				,  := .coveredRunes(, , )
				if  != nil {
					return nil, 
				}
				if len() == 0 {
					continue
				}
				if ![] {
					[] = newFallbackFaceProfile()
					[] = true
				}
				 := fallbackFacePriority([], )
				if  < 0 ||  <  ||  ==  && len() > len() {
					, ,  = , , 
				}
			}
			if  < 0 {
				break
			}
			if int64(len()) > .limits.MaxResolvedBytes-.work.resolvedBytes {
				return nil, fmt.Errorf("d2fonts: resolved fallback font bytes exceed limit %d while retaining face %d from %q", .limits.MaxResolvedBytes, , filepath.Base())
			}
			[] = true
			 = append(, FallbackFont{
				Name: filepath.Base(), MIMEType: fontMIMEType(),
				Data: append([]byte(nil), ...), FaceIndex: uint16(),
			})
			.work.resolvedBytes += int64(len())
			for ,  := range  {
				delete(, )
			}
			if len() == 0 {
				return , nil
			}
		}
	}
	// Absence of coverage is not a malformed-font or resource-budget failure.
	// Callers can deterministically render their missing-glyph placeholder while
	// retaining any faces that did cover part of the request.
	return , nil
}

// acquireResolve serializes font parsing and cumulative work accounting.
// Waiting callers remain cancellable, and a canceled waiter never consumes a
// resolver-lifetime budget.
func ( *SystemFallbackResolver) ( context.Context) (func(), error) {
	.resolveOnce.Do(func() { .resolveGate = make(chan struct{}, 1) })
	select {
	case .resolveGate <- struct{}{}:
		return func() { <-.resolveGate }, nil
	case <-.Done():
		return nil, .Err()
	}
}

// cachedFontPaths snapshots the fixed system roots once for the lifetime of a
// resolver. One export operation shares one resolver across boards, avoiding a
// full directory walk for every scene while preserving request-specific sort
// priority. Cancellation never poisons the cache: a canceled owner publishes
// no partial index and later callers may retry.
func ( *SystemFallbackResolver) ( context.Context,  []string,  fallbackRequestProfile) ([]string, error) {
	for {
		if  := .Err();  != nil {
			return nil, 
		}
		.pathsMu.Lock()
		if .pathsIndexed {
			 := append([]string(nil), .indexedPaths...)
			.pathsMu.Unlock()
			sortFontPaths(, )
			return , nil
		}
		if  := .pathFlight;  != nil {
			.pathsMu.Unlock()
			select {
			case <-.Done():
				return nil, .Err()
			case <-:
				continue
			}
		}
		 := make(chan struct{})
		.pathFlight = 
		.pathsMu.Unlock()

		,  := .fontPaths(, , )
		.pathsMu.Lock()
		if  == nil {
			.indexedPaths = append([]string(nil), ...)
			.pathsIndexed = true
		}
		.pathFlight = nil
		close()
		.pathsMu.Unlock()
		return , 
	}
}

func ( *SystemFallbackResolver) ( context.Context,  []string,  fallbackRequestProfile) ([]string, error) {
	 := make([]string, 0, min(.limits.MaxFiles, 256))
	for ,  := range  {
		if  := .Err();  != nil {
			return nil, 
		}
		,  := os.Lstat()
		if  != nil || !.IsDir() || .Mode()&os.ModeSymlink != 0 {
			continue
		}
		if .work.directoryEntries >= .limits.MaxDirectoryEntries {
			return nil, fmt.Errorf("d2fonts: system font directory entries exceed limit %d", .limits.MaxDirectoryEntries)
		}
		.work.directoryEntries++
		 := []string{}
		for len() != 0 {
			 := [len()-1]
			 = [:len()-1]
			,  := openVerifiedDirectory()
			if  != nil {
				continue
			}
			for {
				if  := .Err();  != nil {
					_ = .Close()
					return nil, 
				}
				,  := .ReadDir(directoryReadBatch)
				for ,  := range  {
					if .work.directoryEntries >= .limits.MaxDirectoryEntries {
						_ = .Close()
						return nil, fmt.Errorf("d2fonts: system font directory entries exceed limit %d", .limits.MaxDirectoryEntries)
					}
					.work.directoryEntries++
					if .work.directoryEntries&255 == 0 {
						if  := .Err();  != nil {
							_ = .Close()
							return nil, 
						}
					}
					if .Type()&os.ModeSymlink != 0 {
						continue
					}
					 := filepath.Join(, .Name())
					if .IsDir() {
						 = append(, )
						continue
					}
					if !isFontPath() {
						continue
					}
					if .work.files >= .limits.MaxFiles {
						_ = .Close()
						return nil, fmt.Errorf("d2fonts: system font candidate count exceeds limit %d; raise the bounded fallback search limit or configure a custom font", .limits.MaxFiles)
					}
					.work.files++
					 = append(, )
				}
				if  == io.EOF {
					break
				}
				if  != nil {
					break
				}
			}
			_ = .Close()
		}
	}
	sortFontPaths(, )
	return , nil
}

func sortFontPaths( []string,  fallbackRequestProfile) {
	sort.Slice(, func(,  int) bool {
		,  := fallbackPathPriority([], ), fallbackPathPriority([], )
		if  !=  {
			return  < 
		}
		return [] < []
	})
}

const directoryReadBatch = 256

// openVerifiedDirectory supports batched ReadDir calls without
// filepath.WalkDir's whole-directory sorted allocation. The final font path
// list is sorted separately, so successful discovery remains deterministic.
func openVerifiedDirectory( string) (*os.File, error) {
	,  := os.Lstat()
	if  != nil {
		return nil, 
	}
	if !.IsDir() || .Mode()&os.ModeSymlink != 0 {
		return nil, fmt.Errorf("path is not a non-symlink directory")
	}
	,  := os.Open()
	if  != nil {
		return nil, 
	}
	,  := .Stat()
	if  != nil {
		_ = .Close()
		return nil, 
	}
	if !.IsDir() || !os.SameFile(, ) {
		_ = .Close()
		return nil, fmt.Errorf("directory identity changed while opening")
	}
	return , nil
}

type fontCoverageProbe struct {
	faces  int
	bytes  int64
	covers bool
}

var errFontProbeBytes = errors.New("font cmap probe bytes exceed remaining scan limit")

type boundedFontReaderAt struct {
	ctx       context.Context
	reader    io.ReaderAt
	remaining int64
	read      int64
	limitHit  bool
}

func ( *boundedFontReaderAt) ( []byte,  int64) (int, error) {
	if  := .ctx.Err();  != nil {
		return 0, 
	}
	if int64(len()) > .remaining-.read {
		.limitHit = true
		return 0, errFontProbeBytes
	}
	,  := .reader.ReadAt(, )
	.read += int64()
	return , 
}

// probeFontFileCoverage reads only SFNT directories and cmap data through
// ReaderAt. A full go-text parse is substantially more expensive and is
// reserved for files whose outline cmap actually maps one requested rune.
// Matching files are subsequently read in full, so their probe bytes are not
// charged twice against the cumulative scan budget.
func probeFontFileCoverage( context.Context,  string, ,  int64,  int,  map[rune]struct{}) (fontCoverageProbe, error) {
	if  <= 0 {
		return fontCoverageProbe{}, fmt.Errorf("system font scan bytes exceed limit")
	}
	,  := os.Lstat()
	if  != nil || !.Mode().IsRegular() || .Mode()&os.ModeSymlink != 0 || .Size() <= 0 || .Size() >  {
		return fontCoverageProbe{}, nil
	}
	,  := os.Open()
	if  != nil {
		return fontCoverageProbe{}, nil
	}
	defer .Close()
	,  := .Stat()
	if  != nil || !.Mode().IsRegular() || !os.SameFile(, ) || .Size() != .Size() {
		return fontCoverageProbe{}, nil
	}
	 := &boundedFontReaderAt{ctx: , reader: , remaining: }
	,  := opentype.ParseCollectionReaderAt()
	if  != nil {
		if  := .Err();  != nil {
			return fontCoverageProbe{bytes: .read}, 
		}
		if .limitHit || errors.Is(, errFontProbeBytes) {
			return fontCoverageProbe{bytes: .read}, errFontProbeBytes
		}
		return fontCoverageProbe{bytes: .read}, nil
	}
	 := fontCoverageProbe{faces: .NumFonts(), bytes: .read}
	if .faces >  {
		return , nil
	}
	var  sfnt.Buffer
	for  := 0;  < .NumFonts(); ++ {
		if  := .Err();  != nil {
			return , 
		}
		,  := .Font()
		if  != nil {
			if  := .Err();  != nil {
				return fontCoverageProbe{faces: .faces, bytes: .read}, 
			}
			if .limitHit || errors.Is(, errFontProbeBytes) {
				return fontCoverageProbe{faces: .faces, bytes: .read}, errFontProbeBytes
			}
			continue
		}
		for  := range  {
			,  := .GlyphIndex(&, )
			if  != nil {
				if .limitHit || errors.Is(, errFontProbeBytes) {
					return fontCoverageProbe{faces: .faces, bytes: .read}, errFontProbeBytes
				}
				continue
			}
			if  != 0 {
				.bytes = .read
				.covers = true
				return , nil
			}
		}
	}
	.bytes = .read
	return , nil
}

func readBoundedFontFile( string, ,  int64) ([]byte, error) {
	if  <= 0 {
		return nil, fmt.Errorf("system font scan bytes exceed limit")
	}
	,  := os.Lstat()
	if  != nil || !.Mode().IsRegular() || .Mode()&os.ModeSymlink != 0 || .Size() <= 0 {
		return nil, nil
	}
	if .Size() >  {
		return nil, nil
	}
	if .Size() >  {
		return nil, fmt.Errorf("system font scan bytes exceed remaining limit %d", )
	}
	,  := os.Open()
	if  != nil {
		return nil, nil
	}
	defer .Close()
	,  := .Stat()
	if  != nil || !.Mode().IsRegular() || !os.SameFile(, ) || .Size() <= 0 {
		return nil, nil
	}
	 := min(, )
	if .Size() >  {
		return nil, fmt.Errorf("system font file exceeds remaining byte limit %d", )
	}
	,  := io.ReadAll(io.LimitReader(, +1))
	if int64(len()) >  {
		return nil, fmt.Errorf("system font file grew beyond byte limit %d while reading", )
	}
	if  != nil {
		// Preserve partial bytes so the caller charges the I/O that actually
		// happened. Parsing will reject an incomplete font without retaining it.
		return , nil
	}
	return , nil
}

func uniqueFallbackRunes( context.Context,  []rune) (map[rune]struct{}, error) {
	 := make(map[rune]struct{}, len())
	for ,  := range  {
		if &1023 == 0 {
			if  := .Err();  != nil {
				return nil, 
			}
		}
		if fallbackRuneMayHaveGlyph() {
			[] = struct{}{}
		}
	}
	if  := .Err();  != nil {
		return nil, 
	}
	return , nil
}

// fallbackRuneMayHaveGlyph rejects only values which Unicode permanently
// reserves as noncharacters (plus invalid scalar values). They can never gain
// text semantics, so walking and parsing every system font for them is pure
// resource amplification. Unassigned and private-use scalars remain eligible:
// custom and future fonts may legitimately map them.
func fallbackRuneMayHaveGlyph( rune) bool {
	if !utf8.ValidRune() {
		return false
	}
	if  >= 0xfdd0 &&  <= 0xfdef {
		return false
	}
	 :=  & 0xffff
	return  != 0xfffe &&  != 0xffff
}

func ( *SystemFallbackResolver) ( context.Context,  *fontface.ParsedFace,  map[rune]struct{}) ([]rune, error) {
	 := make([]rune, 0)
	 := 0
	for  := range  {
		if .work.coverageChecks >= .limits.MaxCoverageChecks {
			return nil, fmt.Errorf("d2fonts: system font coverage checks exceed limit %d across this resolver while resolving U+%04X", .limits.MaxCoverageChecks, firstRune())
		}
		.work.coverageChecks++
		if &255 == 0 {
			if  := .Err();  != nil {
				return nil, 
			}
		}
		,  := .SupportsRenderableRune()
		if  != nil {
			return nil, 
		}
		if  {
			 = append(, )
		}
		++
	}
	sort.Slice(, func(,  int) bool { return [] < [] })
	if  := .Err();  != nil {
		return nil, 
	}
	return , nil
}

func firstRune( map[rune]struct{}) rune {
	var  rune
	 := false
	for  := range  {
		if ! ||  <  {
			 = 
			 = true
		}
	}
	return 
}

func isFontPath( string) bool {
	switch strings.ToLower(filepath.Ext()) {
	case ".ttf", ".otf", ".ttc", ".otc":
		return true
	default:
		return false
	}
}

func fontMIMEType( string) string {
	switch strings.ToLower(filepath.Ext()) {
	case ".ttc", ".otc":
		return "font/collection"
	case ".otf":
		return "font/otf"
	default:
		return "font/ttf"
	}
}

const genericFallbackPriority = 20

type fallbackRequestProfile struct {
	cjk, arabic, hebrew, symbol  bool
	italic, bold, semibold, mono bool
	family                       string
	weight                       int
}

func newFallbackRequestProfile( map[rune]struct{},  FallbackRequest) fallbackRequestProfile {
	 := normalizeFallbackFamily(.Family)
	 := strings.ToLower(.Style)
	 := strings.Contains(, "semibold") || strings.Contains(, "semi-bold")
	 := .Weight
	if  <= 0 {
		switch {
		case :
			 = 600
		case strings.Contains(, "bold"):
			 = 700
		default:
			 = 400
		}
	}
	 = min(900, max(100, ))
	 := fallbackRequestProfile{
		italic:   strings.Contains(, "italic") || strings.Contains(, "oblique"),
		bold:      >= 700,
		semibold:  >= 600 &&  < 700,
		mono:     strings.Contains(, "mono") || strings.Contains(, "code") || strings.Contains(, "console"),
		family:   ,
		weight:   ,
	}
	for  := range  {
		.cjk = .cjk || isCJK()
		.arabic = .arabic || isArabic()
		.hebrew = .hebrew || isHebrew()
		.symbol = .symbol || isEmojiOrSymbol()
	}
	return 
}

func fallbackPathPriority( string,  fallbackRequestProfile) int {
	 := strings.ToLower(filepath.Base())
	 := 80
	 := func( ...string) bool {
		for ,  := range  {
			if strings.Contains(, ) {
				return true
			}
		}
		return false
	}
	// Prefer genuinely broad Unicode faces before accumulating one
	// system face per script. This keeps mixed-script scenes within the retained
	// font-byte budget when a single host font can cover the whole label set.
	if ("arial unicode", "unifont") {
		 = -10
	}
	switch {
	case .cjk && ("cjk", "hiragino", "pingfang", "heiti", "gothic", "songti", "mingliu", "simsun", "malgun"):
		 = min(, 0)
	case .arabic && ("arab", "amiri", "naskh", "kufi"):
		 = min(, 0)
	case .hebrew && ("hebrew", "david", "frank"):
		 = min(, 0)
	case .symbol && ("symbol", "emoji", "dingbat", "dejavu", "unifont", "unicode"):
		 = min(, 0)
	}
	if ("noto", "dejavu", "arial unicode", "unifont") {
		 = min(, genericFallbackPriority)
	}
	if ("emoji") {
		// Deprioritize emoji-specific faces because their glyphs may use
		// unsupported bitmap, SVG, or COLRv1 data. Coverage validation still
		// admits ordinary outlines and COLRv0 layers.
		 += 10
	}
	if .mono && ("mono", "code", "console") {
		 -= 6
	}
	return fallbackPathStylePenalty(, ) + 
}

func fallbackPathStylePenalty( string,  fallbackRequestProfile) int {
	 := fallbackFaceProfile{
		family: normalizeFallbackFamily(),
		italic: strings.Contains(, "italic") || strings.Contains(, "oblique"),
		weight: fallbackFilenameWeight(),
		mono:   strings.Contains(, "mono") || strings.Contains(, "code") || strings.Contains(, "console"),
	}
	return fallbackStylePenalty(, )
}

func fallbackFilenameWeight( string) int {
	switch {
	case strings.Contains(, "extrablack"), strings.Contains(, "ultrablack"), strings.Contains(, "superblack"):
		return 900
	case strings.Contains(, "black"), strings.Contains(, "heavy"):
		return 900
	case strings.Contains(, "extrabold"), strings.Contains(, "extra-bold"), strings.Contains(, "ultrabold"), strings.Contains(, "ultra-bold"):
		return 800
	case strings.Contains(, "semibold"), strings.Contains(, "semi-bold"), strings.Contains(, "demibold"), strings.Contains(, "demi-bold"):
		return 600
	case strings.Contains(, "bold"):
		return 700
	case strings.Contains(, "medium"):
		return 500
	case strings.Contains(, "extralight"), strings.Contains(, "extra-light"), strings.Contains(, "ultralight"), strings.Contains(, "ultra-light"):
		return 200
	case strings.Contains(, "semilight"), strings.Contains(, "semi-light"), strings.Contains(, "demilight"), strings.Contains(, "demi-light"):
		return 350
	case strings.Contains(, "thin"):
		return 100
	case strings.Contains(, "light"):
		return 300
	default:
		return 400
	}
}

func newFallbackFaceProfile( *fontface.ParsedFace) fallbackFaceProfile {
	 := .Shaping.Describe()
	return fallbackFaceProfile{
		family: normalizeFallbackFamily(.Family),
		italic: .Aspect.Style == gotextfont.StyleItalic,
		weight: min(900, max(100, int(.Aspect.Weight+0.5))),
		mono:   .Shaping.IsMonospace(),
	}
}

func fallbackFacePriority( fallbackFaceProfile,  fallbackRequestProfile) int {
	return fallbackStylePenalty(, )
}

func fallbackStylePenalty( fallbackFaceProfile,  fallbackRequestProfile) int {
	 := absInt(.weight - .weight)
	if .italic != .italic {
		 += 1_000
	}
	if .mono != .mono {
		if .mono {
			 += 1_000
		} else {
			 += 100
		}
	}
	if .family != "" && .family != "" {
		switch {
		case .family == .family:
			 -= 40
		case sameFallbackFamilyClass(.family, .family):
			 -= 10
		}
	}
	return 
}

func sameFallbackFamilyClass(,  string) bool {
	for ,  := range []string{"sans", "serif", "mono", "code", "console"} {
		if strings.Contains(, ) && strings.Contains(, ) {
			return true
		}
	}
	return false
}

func normalizeFallbackFamily( string) string {
	 = strings.ToLower()
	return strings.Map(func( rune) rune {
		if  >= 'a' &&  <= 'z' ||  >= '0' &&  <= '9' {
			return 
		}
		return -1
	}, )
}

func absInt( int) int {
	if  < 0 {
		return -
	}
	return 
}

func isCJK( rune) bool {
	return  >= 0x2e80 &&  <= 0x9fff ||  >= 0xf900 &&  <= 0xfaff ||  >= 0x20000 &&  <= 0x323af
}

func isArabic( rune) bool {
	return  >= 0x0600 &&  <= 0x08ff ||  >= 0xfb50 &&  <= 0xfdff ||  >= 0xfe70 &&  <= 0xfeff
}

func isHebrew( rune) bool {
	return  >= 0x0590 &&  <= 0x05ff ||  >= 0xfb1d &&  <= 0xfb4f
}

func isEmojiOrSymbol( rune) bool {
	return  >= 0x2190 &&  <= 0x2bff ||  >= 0x1f000 &&  <= 0x1faff
}

func systemFontRoots() []string {
	switch runtime.GOOS {
	case "darwin":
		return []string{"/System/Library/Fonts", "/Library/Fonts"}
	case "linux", "freebsd", "openbsd", "netbsd", "dragonfly":
		return []string{"/usr/share/fonts", "/usr/local/share/fonts"}
	case "windows":
		 := os.Getenv("WINDIR")
		if  == "" {
			 = `C:\Windows`
		}
		return []string{filepath.Join(, "Fonts")}
	default:
		return nil
	}
}