package dict

import (
	"bytes"
	"cmp"
	"encoding/binary"
	"errors"
	"fmt"
	"os"
	"slices"
	"unicode/utf8"

	"chirami/internal/jp"
	"golang.org/x/sys/unix"
)

// Ruby is one segmented ruby element: Text is the surface text (kanji or kana),
// and Ruby is the reading annotation (empty string for kana parts).
type Ruby struct {
	Text string
	Ruby string
}

// Dict is an open, memory-mapped Japanese dictionary.
type Dict struct {
	file                 *os.File
	mmap                 []byte
	header               *Header
	interned             []string
	tags                 map[string]string
	keyIndexBytes        []byte
	keyDataBytes         []byte
	postingBytes         []byte
	entryIndexBytes      []byte
	entryDataBytes       []byte
	kanjiIndexBytes      []byte
	kanjiDataBytes       []byte
	furiganaIndexBytes   []byte
	furiganaKeyBytes     []byte
	furiganaDataBytes    []byte
	kradIndexBytes       []byte
	kradDataBytes        []byte
	radkIndexBytes       []byte
	radkDataBytes        []byte
	kanjiWordsIndexBytes []byte
	kanjiWordsDataBytes  []byte
}

// Open memory-maps a dictionary file produced by build.Build.
func Open(path string) (*Dict, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}

	st, err := f.Stat()
	if err != nil {
		f.Close()
		return nil, err
	}
	if st.Size() < 16 {
		f.Close()
		return nil, fmt.Errorf("%w: file size %d smaller than header", ErrBufferTooSmall, st.Size())
	}

	data, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED)
	if err != nil {
		f.Close()
		return nil, fmt.Errorf("mmap %s: %w", path, err)
	}

	hdr, err := DecodeHeader(data)
	if err != nil {
		unix.Munmap(data)
		f.Close()
		return nil, err
	}

	// Validate section bounds
	checkSection := func(name string, s *SectionHeader) error {
		if uint64(s.Offset)+uint64(s.Size) > uint64(len(data)) {
			return fmt.Errorf("section %s out of bounds: offset %d + size %d > file %d", name, s.Offset, s.Size, len(data))
		}
		return nil
	}
	sections := []struct {
		name string
		s    *SectionHeader
	}{
		{"InternedStrings", &hdr.InternedStrings},
		{"TagMap", &hdr.TagMap},
		{"KeyIndex", &hdr.KeyIndex},
		{"KeyData", &hdr.KeyData},
		{"PostingData", &hdr.PostingData},
		{"EntryIndex", &hdr.EntryIndex},
		{"EntryData", &hdr.EntryData},
		{"KanjiIndex", &hdr.KanjiIndex},
		{"KanjiData", &hdr.KanjiData},
		{"FuriganaIndex", &hdr.FuriganaIndex},
		{"FuriganaKeyData", &hdr.FuriganaKeyData},
		{"FuriganaData", &hdr.FuriganaData},
		{"KradIndex", &hdr.KradIndex},
		{"KradData", &hdr.KradData},
		{"RadkIndex", &hdr.RadkIndex},
		{"RadkData", &hdr.RadkData},
		{"KanjiWordsIndex", &hdr.KanjiWordsIndex},
		{"KanjiWordsData", &hdr.KanjiWordsData},
	}
	for _, sec := range sections {
		if err := checkSection(sec.name, sec.s); err != nil {
			unix.Munmap(data)
			f.Close()
			return nil, err
		}
	}

	// Read interned strings
	internedBytes := data[hdr.InternedStrings.Offset : hdr.InternedStrings.Offset+hdr.InternedStrings.Size]
	interned := make([]string, hdr.InternedStrings.Count)
	ir := &ByteReader{B: internedBytes}
	for i := uint32(0); i < hdr.InternedStrings.Count; i++ {
		s, err := ir.Str()
		if err != nil {
			unix.Munmap(data)
			f.Close()
			return nil, fmt.Errorf("read interned string %d: %w", i, err)
		}
		interned[i] = s
	}

	// Read tags map
	tagBytes := data[hdr.TagMap.Offset : hdr.TagMap.Offset+hdr.TagMap.Size]
	tags := make(map[string]string, hdr.TagMap.Count)
	tr := &ByteReader{B: tagBytes}
	for i := uint32(0); i < hdr.TagMap.Count; i++ {
		code, err := tr.Str()
		if err != nil {
			unix.Munmap(data)
			f.Close()
			return nil, fmt.Errorf("read tag code %d: %w", i, err)
		}
		label, err := tr.Str()
		if err != nil {
			unix.Munmap(data)
			f.Close()
			return nil, fmt.Errorf("read tag label %d: %w", i, err)
		}
		tags[code] = label
	}

	d := &Dict{
		file:                 f,
		mmap:                 data,
		header:               hdr,
		interned:             interned,
		tags:                 tags,
		keyIndexBytes:        data[hdr.KeyIndex.Offset : hdr.KeyIndex.Offset+hdr.KeyIndex.Size],
		keyDataBytes:         data[hdr.KeyData.Offset : hdr.KeyData.Offset+hdr.KeyData.Size],
		postingBytes:         data[hdr.PostingData.Offset : hdr.PostingData.Offset+hdr.PostingData.Size],
		entryIndexBytes:      data[hdr.EntryIndex.Offset : hdr.EntryIndex.Offset+hdr.EntryIndex.Size],
		entryDataBytes:       data[hdr.EntryData.Offset : hdr.EntryData.Offset+hdr.EntryData.Size],
		kanjiIndexBytes:      data[hdr.KanjiIndex.Offset : hdr.KanjiIndex.Offset+hdr.KanjiIndex.Size],
		kanjiDataBytes:       data[hdr.KanjiData.Offset : hdr.KanjiData.Offset+hdr.KanjiData.Size],
		furiganaIndexBytes:   data[hdr.FuriganaIndex.Offset : hdr.FuriganaIndex.Offset+hdr.FuriganaIndex.Size],
		furiganaKeyBytes:     data[hdr.FuriganaKeyData.Offset : hdr.FuriganaKeyData.Offset+hdr.FuriganaKeyData.Size],
		furiganaDataBytes:    data[hdr.FuriganaData.Offset : hdr.FuriganaData.Offset+hdr.FuriganaData.Size],
		kradIndexBytes:       data[hdr.KradIndex.Offset : hdr.KradIndex.Offset+hdr.KradIndex.Size],
		kradDataBytes:        data[hdr.KradData.Offset : hdr.KradData.Offset+hdr.KradData.Size],
		radkIndexBytes:       data[hdr.RadkIndex.Offset : hdr.RadkIndex.Offset+hdr.RadkIndex.Size],
		radkDataBytes:        data[hdr.RadkData.Offset : hdr.RadkData.Offset+hdr.RadkData.Size],
		kanjiWordsIndexBytes: data[hdr.KanjiWordsIndex.Offset : hdr.KanjiWordsIndex.Offset+hdr.KanjiWordsIndex.Size],
		kanjiWordsDataBytes:  data[hdr.KanjiWordsData.Offset : hdr.KanjiWordsData.Offset+hdr.KanjiWordsData.Size],
	}

	return d, nil
}

// Close unmaps memory and closes the dictionary file.
func (d *Dict) Close() error {
	var errs []error
	if d.mmap != nil {
		if err := unix.Munmap(d.mmap); err != nil {
			errs = append(errs, err)
		}
		d.mmap = nil
	}
	if d.file != nil {
		if err := d.file.Close(); err != nil {
			errs = append(errs, err)
		}
		d.file = nil
	}
	return errors.Join(errs...)
}

// TagLabel returns the English description of a JMdict tag code (e.g. "v1" -> "Ichidan verb")
// using this dictionary's tags map, falling back to built-in common tags, or returning code unchanged.
func (d *Dict) TagLabel(code string) string {
	if d != nil && d.tags != nil {
		if label, ok := d.tags[code]; ok {
			return label
		}
	}
	if label, ok := fallbackTags[code]; ok {
		return label
	}
	return code
}

// searchKey performs a binary search in the sorted key index for exact match of key.
func (d *Dict) searchKey(target []byte) (postingOffset uint32, postingCount uint16, found bool) {
	numKeys := len(d.keyIndexBytes) / KeyIndexRecordSize
	low := 0
	high := numKeys - 1

	for low <= high {
		mid := int(uint(low+high) >> 1)
		recOff := mid * KeyIndexRecordSize
		keyOff := binary.LittleEndian.Uint32(d.keyIndexBytes[recOff : recOff+4])
		keyLen := binary.LittleEndian.Uint16(d.keyIndexBytes[recOff+4 : recOff+6])
		keyBytes := d.keyDataBytes[keyOff : keyOff+uint32(keyLen)]

		cmp := bytes.Compare(keyBytes, target)
		if cmp < 0 {
			low = mid + 1
		} else if cmp > 0 {
			high = mid - 1
		} else {
			pCount := binary.LittleEndian.Uint16(d.keyIndexBytes[recOff+6 : recOff+8])
			pOff := binary.LittleEndian.Uint32(d.keyIndexBytes[recOff+8 : recOff+12])
			return pOff, pCount, true
		}
	}
	return 0, 0, false
}

// getPostings reads the list of entry indices for a given posting offset and count.
func (d *Dict) getPostings(postOff uint32, postCount uint16) []uint32 {
	if postCount == 0 {
		return nil
	}
	out := make([]uint32, postCount)
	byteStart := postOff * 4
	for i := uint16(0); i < postCount; i++ {
		out[i] = binary.LittleEndian.Uint32(d.postingBytes[byteStart+uint32(i)*4 : byteStart+uint32(i)*4+4])
	}
	return out
}

// getEntry decodes entry record by index.
func (d *Dict) getEntry(entryIdx uint32) (Entry, error) {
	numEntries := uint32(len(d.entryIndexBytes) / 4)
	if entryIdx >= numEntries {
		return Entry{}, fmt.Errorf("entry index %d out of bounds (%d)", entryIdx, numEntries)
	}
	off := binary.LittleEndian.Uint32(d.entryIndexBytes[entryIdx*4 : entryIdx*4+4])
	if off >= uint32(len(d.entryDataBytes)) {
		return Entry{}, fmt.Errorf("entry data offset %d out of bounds", off)
	}
	e, _, err := DecodeEntry(d.entryDataBytes[off:], d.interned)
	return e, err
}

// Lookup performs an exact match on any kanji or kana form.
// Kana keys match across hiragana and katakana.
func (d *Dict) Lookup(word string) []Entry {
	if len(word) == 0 {
		return nil
	}

	norm := jp.ToHiragana(word)

	// Collect candidate posting lists.
	var candidateIndices []uint32
	seen := make(map[uint32]bool)

	if pOff, pCount, ok := d.searchKey([]byte(word)); ok {
		posts := d.getPostings(pOff, pCount)
		for _, idx := range posts {
			if !seen[idx] {
				seen[idx] = true
				candidateIndices = append(candidateIndices, idx)
			}
		}
	}

	// If word has kana and norm != word, also look up normalized form
	if norm != word {
		if pOff, pCount, ok := d.searchKey([]byte(norm)); ok {
			posts := d.getPostings(pOff, pCount)
			for _, idx := range posts {
				if !seen[idx] {
					seen[idx] = true
					candidateIndices = append(candidateIndices, idx)
				}
			}
		}
	}

	if len(candidateIndices) == 0 {
		return nil
	}

	entries := make([]Entry, 0, len(candidateIndices))
	for _, idx := range candidateIndices {
		if e, err := d.getEntry(idx); err == nil {
			entries = append(entries, e)
		}
	}
	return entries
}

// LookupReading returns entries matching word, with entries where kana==reading
// placed first, followed by the rest of Lookup(word). When word is kana-only,
// entries ordinarily written in kana (particles, kana words, and "uk" entries)
// come before kanji words that merely share the reading.
func (d *Dict) LookupReading(word, reading string) []Entry {
	entries := d.Lookup(word)
	if len(entries) <= 1 {
		return entries
	}

	wordNorm := jp.ToHiragana(word)
	if jp.IsKanaOnly(word) {
		slices.SortStableFunc(entries, func(a, b Entry) int {
			return cmp.Compare(kanaRank(b, word, wordNorm), kanaRank(a, word, wordNorm))
		})
		return entries
	}

	readingNorm := jp.ToHiragana(reading)
	var matched []Entry
	var rest []Entry

	for _, e := range entries {
		hasWord := false
		for _, k := range e.Kanji {
			if k.Text == word {
				hasWord = true
				break
			}
		}
		if !hasWord {
			for _, r := range e.Kana {
				if r.Text == word || jp.ToHiragana(r.Text) == wordNorm {
					hasWord = true
					break
				}
			}
		}

		hasReading := false
		for _, r := range e.Kana {
			if r.Text == reading || jp.ToHiragana(r.Text) == readingNorm {
				hasReading = true
				break
			}
		}

		if hasWord && hasReading {
			matched = append(matched, e)
		} else {
			rest = append(rest, e)
		}
	}

	return append(matched, rest...)
}

var (
	nonDisplayKanji = []string{"sK", "iK", "rK", "oK"}
	nonDisplayKana  = []string{"sk", "ik", "rk", "ok"}
)

func hasAnyTag(tags, set []string) bool {
	for _, t := range tags {
		if slices.Contains(set, t) {
			return true
		}
	}
	return false
}

// WrittenInKana reports whether e is ordinarily written in kana: it has no
// displayable kanji form, or its primary sense is marked "uk" (usually kana).
func WrittenInKana(e Entry) bool {
	if len(e.Senses) > 0 && slices.Contains(e.Senses[0].Misc, "uk") {
		return true
	}
	for _, k := range e.Kanji {
		if !hasAnyTag(k.Tags, nonDisplayKanji) {
			return false
		}
	}
	return true
}

func kanaRank(e Entry, word, wordNorm string) int {
	best := 0
	inKana := WrittenInKana(e)
	for _, r := range e.Kana {
		if r.Text != word && jp.ToHiragana(r.Text) != wordNorm {
			continue
		}
		s := 1
		if !hasAnyTag(r.Tags, nonDisplayKana) {
			s += 16
		}
		if r.Common || (word != wordNorm && r.Text == word && inKana) {
			s += 8
			if inKana {
				s += 4
			}
		}
		if r.Text == word {
			s += 2
		}
		if s > best {
			best = s
		}
	}
	return best
}

// LongestPrefix finds the longest prefix of s (up to maxRunes) that has
// dictionary entries. It returns the byte length n of the prefix and the entries.
func (d *Dict) LongestPrefix(s string, maxRunes int) (int, []Entry) {
	if len(s) == 0 || maxRunes <= 0 {
		return 0, nil
	}

	boundaries := make([]int, 0, maxRunes+1)
	boundaries = append(boundaries, 0)
	cur := 0
	for cur < len(s) && len(boundaries) <= maxRunes {
		_, size := utf8.DecodeRuneInString(s[cur:])
		cur += size
		boundaries = append(boundaries, cur)
	}

	for i := len(boundaries) - 1; i >= 1; i-- {
		prefix := s[:boundaries[i]]
		entries := d.Lookup(prefix)
		if len(entries) > 0 {
			return len(prefix), entries
		}
	}
	return 0, nil
}

// Kanji looks up a single kanji character by rune.
func (d *Dict) Kanji(r rune) (Kanji, bool) {
	target := uint32(r)
	numKanji := len(d.kanjiIndexBytes) / KanjiIndexRecordSize
	low := 0
	high := numKanji - 1

	for low <= high {
		mid := int(uint(low+high) >> 1)
		recOff := mid * KanjiIndexRecordSize
		kRune := binary.LittleEndian.Uint32(d.kanjiIndexBytes[recOff : recOff+4])

		if kRune < target {
			low = mid + 1
		} else if kRune > target {
			high = mid - 1
		} else {
			dataOff := binary.LittleEndian.Uint32(d.kanjiIndexBytes[recOff+4 : recOff+8])
			if dataOff >= uint32(len(d.kanjiDataBytes)) {
				return Kanji{}, false
			}
			k, _, err := DecodeKanji(d.kanjiDataBytes[dataOff:])
			if err != nil {
				return Kanji{}, false
			}
			return k, true
		}
	}
	return Kanji{}, false
}

// Furigana looks up per-kanji furigana segmentation for word with reading.
// word may be any form (typically kanji); reading is matched normalized to hiragana.
func (d *Dict) Furigana(word, reading string) ([]Ruby, bool) {
	if len(d.furiganaIndexBytes) == 0 {
		return nil, false
	}
	readingNorm := jp.ToHiragana(reading)
	targetKey := []byte(word + "\x00" + readingNorm)

	numRecords := len(d.furiganaIndexBytes) / FuriganaIndexRecordSize
	low := 0
	high := numRecords - 1

	for low <= high {
		mid := int(uint(low+high) >> 1)
		recOff := mid * FuriganaIndexRecordSize
		kOff := binary.LittleEndian.Uint32(d.furiganaIndexBytes[recOff : recOff+4])
		kLen := binary.LittleEndian.Uint16(d.furiganaIndexBytes[recOff+4 : recOff+6])
		kBytes := d.furiganaKeyBytes[kOff : kOff+uint32(kLen)]

		cmp := bytes.Compare(kBytes, targetKey)
		if cmp < 0 {
			low = mid + 1
		} else if cmp > 0 {
			high = mid - 1
		} else {
			dataOff := binary.LittleEndian.Uint32(d.furiganaIndexBytes[recOff+8 : recOff+12])
			if dataOff >= uint32(len(d.furiganaDataBytes)) {
				return nil, false
			}
			rubies, _, err := DecodeRubies(d.furiganaDataBytes[dataOff:])
			if err != nil {
				return nil, false
			}
			return rubies, true
		}
	}
	return nil, false
}

var fallbackTags = map[string]string{
	"v1":     "Ichidan verb",
	"v5u":    "Godan verb with 'u' ending",
	"v5k":    "Godan verb with 'ku' ending",
	"v5s":    "Godan verb with 'su' ending",
	"v5t":    "Godan verb with 'tsu' ending",
	"v5n":    "Godan verb with 'nu' ending",
	"v5m":    "Godan verb with 'mu' ending",
	"v5r":    "Godan verb with 'ru' ending",
	"v5g":    "Godan verb with 'gu' ending",
	"v5b":    "Godan verb with 'bu' ending",
	"vz":     "Ichidan verb - zuru verb",
	"vk":     "Kuru verb - special class",
	"vn":     "irregular nu verb",
	"vr":     "irregular ru verb",
	"vs":     "noun or participle which takes the aux. verb suru",
	"adj-i":  "adjective (keiyoushi)",
	"adj-na": "adjectival nouns or quasi-adjectives (keiyodoushi)",
	"adj-no": "nouns which may take the genitive case particle 'no'",
	"adj-pn": "pre-noun adjectival (rentaishi)",
	"adj-t":  "'taru' adjective",
	"adj-f":  "noun or verb acting prenominally",
	"n":      "noun (common) (futsuumeishi)",
	"n-adv":  "adverbial noun (fukushitekimeishi)",
	"n-pref": "noun, used as a prefix",
	"n-suf":  "noun, used as a suffix",
	"n-t":    "noun (temporal) (jisoumeishi)",
	"adv":    "adverb (fukushi)",
	"adv-to": "adverb taking the 'to' particle",
	"prt":    "particle",
	"conj":   "conjunction",
	"exp":    "expressions (phrases, clauses, etc.)",
	"int":    "interjection (kandoushi)",
	"pref":   "prefix",
	"suf":    "suffix",
	"ctr":    "counter",
	"pn":     "pronoun",
	"uk":     "word usually written using kana alone",
	"sK":     "search-only kanji form",
	"sk":     "search-only kana form",
	"iK":     "word containing irregular kanji usage",
	"ik":     "word containing irregular kana usage",
	"rK":     "rarely used kanji form",
	"rk":     "rarely used kana form",
	"io":     "irregular okurigana usage",
	"pol":    "polite (teineigo) language",
	"hon":    "honorific or respectful (sonkeigo) language",
	"hum":    "humble (kenjougo) language",
	"col":    "colloquialism",
	"sl":     "slang",
	"id":     "idiomatic expression",
	"arch":   "archaic",
	"obs":    "obsolete term",
	"rare":   "rare term",
	"ateji":  "ateji (phonetic) reading",
	"abbr":   "abbreviation",
	"form":   "formal or literary term",
	"vt":     "transitive verb",
	"vi":     "intransitive verb",
}

// Components returns the KRADFILE components of k in KRADFILE order,
// mapped to their real display component forms (e.g. 化->⺅, 艾->艹, 汁->氵, 込->辶, ｜->丨, ノ->丿).
// Returns nil if k is not found in the index.
func (d *Dict) Components(k rune) []rune {
	if len(d.kradIndexBytes) == 0 {
		return nil
	}
	target := uint32(k)
	numRecords := len(d.kradIndexBytes) / KradIndexRecordSize
	low := 0
	high := numRecords - 1

	for low <= high {
		mid := int(uint(low+high) >> 1)
		recOff := mid * KradIndexRecordSize
		r := binary.LittleEndian.Uint32(d.kradIndexBytes[recOff : recOff+4])

		if r < target {
			low = mid + 1
		} else if r > target {
			high = mid - 1
		} else {
			dataOff := binary.LittleEndian.Uint32(d.kradIndexBytes[recOff+4 : recOff+8])
			count := binary.LittleEndian.Uint16(d.kradIndexBytes[recOff+8 : recOff+10])
			if count == 0 {
				return nil
			}
			byteLen := uint32(count) * 4
			if dataOff+byteLen > uint32(len(d.kradDataBytes)) {
				return nil
			}
			out := make([]rune, count)
			for i := uint16(0); i < count; i++ {
				rVal := rune(binary.LittleEndian.Uint32(d.kradDataBytes[dataOff+uint32(i)*4 : dataOff+uint32(i)*4+4]))
				out[i] = DisplayComponent(rVal)
			}
			return out
		}
	}
	return nil
}

// KanjiWith returns kanji containing the given component from RADKFILE,
// ordered most frequent first (by KANJIDIC2 freq rank; unranked last by stroke count).
// Accepts both real component forms (e.g. ⺅, 艹, 氵, 辶, 丨, 丿) and legacy RADKFILE
// placeholder glyphs (e.g. 化, 艾, 汁, 込, ｜, ノ).
// If max <= 0, it returns nil.
func (d *Dict) KanjiWith(component rune, max int) []rune {
	if max <= 0 || len(d.radkIndexBytes) == 0 {
		return nil
	}

	searchRad := func(rad rune) []rune {
		target := uint32(rad)
		numRecords := len(d.radkIndexBytes) / RadkIndexRecordSize
		low := 0
		high := numRecords - 1

		for low <= high {
			mid := int(uint(low+high) >> 1)
			recOff := mid * RadkIndexRecordSize
			r := binary.LittleEndian.Uint32(d.radkIndexBytes[recOff : recOff+4])

			if r < target {
				low = mid + 1
			} else if r > target {
				high = mid - 1
			} else {
				dataOff := binary.LittleEndian.Uint32(d.radkIndexBytes[recOff+4 : recOff+8])
				count := binary.LittleEndian.Uint32(d.radkIndexBytes[recOff+8 : recOff+12])
				if count == 0 {
					return nil
				}
				n := int(count)
				if max > 0 && max < n {
					n = max
				}
				byteLen := uint32(n) * 4
				if dataOff+byteLen > uint32(len(d.radkDataBytes)) {
					return nil
				}
				out := make([]rune, n)
				for i := 0; i < n; i++ {
					out[i] = rune(binary.LittleEndian.Uint32(d.radkDataBytes[dataOff+uint32(i)*4 : dataOff+uint32(i)*4+4]))
				}
				return out
			}
		}
		return nil
	}

	// 1. Direct search
	if res := searchRad(component); len(res) > 0 {
		return res
	}

	// 2. Try display form
	if disp, ok := ToDisplayForm[component]; ok && disp != component {
		if res := searchRad(disp); len(res) > 0 {
			return res
		}
	}

	// 3. Try placeholder form
	if orig, ok := ToPlaceholderForm[component]; ok && orig != component {
		if res := searchRad(orig); len(res) > 0 {
			return res
		}
	}

	// 4. Single-component kanji (e.g. 良 -> 艮, 八 -> ハ)
	if comps := d.Components(component); len(comps) == 1 && comps[0] != component {
		if res := searchRad(comps[0]); len(res) > 0 {
			return res
		}
		if orig, ok := ToPlaceholderForm[comps[0]]; ok && orig != comps[0] {
			if res := searchRad(orig); len(res) > 0 {
				return res
			}
		}
		if disp, ok := ToDisplayForm[comps[0]]; ok && disp != comps[0] {
			if res := searchRad(disp); len(res) > 0 {
				return res
			}
		}
	}

	return nil
}

// ComponentName returns the English name for display of component r:
// for Kangxi radicals (or variants per CJKRadicals.txt), it returns the lowercased
// Unicode radical name. For non-radical components, it uses KANJIDIC2's first meaning
// with any "... radical (no. N)" suffixes stripped. Names are never empty.
func (d *Dict) ComponentName(r rune) string {
	// 1. Check Kangxi radical names table
	if name, ok := kangxiRadicalNames[r]; ok {
		return name
	}
	if disp, ok := ToDisplayForm[r]; ok {
		if name, ok := kangxiRadicalNames[disp]; ok {
			return name
		}
	}
	if orig, ok := ToPlaceholderForm[r]; ok {
		if name, ok := kangxiRadicalNames[orig]; ok {
			return name
		}
	}

	// 2. Non-radical components: KANJIDIC2 first meaning, stripping "... radical (no. N)"
	if k, ok := d.Kanji(r); ok && len(k.Meanings) > 0 {
		for _, m := range k.Meanings {
			cleaned := CleanKanjidicMeaning(m)
			if cleaned != "" {
				return cleaned
			}
		}
	}

	// 3. Fallback to general radical names table
	if name, ok := radicalNames[r]; ok {
		return name
	}

	// 4. Fallback must never be empty
	return "component"
}

// WordsWith returns JMdict entries with a displayable kanji form containing k,
// in the same priority order Lookup uses (EDRDG pri, common first), excluding
// entries whose only k-forms are search-only.
// If max <= 0, it returns nil.
func (d *Dict) WordsWith(k rune, max int) []Entry {
	if max <= 0 || len(d.kanjiWordsIndexBytes) == 0 {
		return nil
	}

	target := uint32(k)
	numRecords := len(d.kanjiWordsIndexBytes) / KanjiWordsIndexRecordSize
	low := 0
	high := numRecords - 1

	for low <= high {
		mid := int(uint(low+high) >> 1)
		recOff := mid * KanjiWordsIndexRecordSize
		r := binary.LittleEndian.Uint32(d.kanjiWordsIndexBytes[recOff : recOff+4])

		if r < target {
			low = mid + 1
		} else if r > target {
			high = mid - 1
		} else {
			dataOff := binary.LittleEndian.Uint32(d.kanjiWordsIndexBytes[recOff+4 : recOff+8])
			count := binary.LittleEndian.Uint16(d.kanjiWordsIndexBytes[recOff+8 : recOff+10])
			if count == 0 {
				return nil
			}
			n := int(count)
			if max > 0 && max < n {
				n = max
			}
			byteLen := uint32(n) * 4
			if dataOff+byteLen > uint32(len(d.kanjiWordsDataBytes)) {
				return nil
			}
			entries := make([]Entry, 0, n)
			for i := 0; i < n; i++ {
				entryIdx := binary.LittleEndian.Uint32(d.kanjiWordsDataBytes[dataOff+uint32(i)*4 : dataOff+uint32(i)*4+4])
				if e, err := d.getEntry(entryIdx); err == nil {
					entries = append(entries, e)
				}
			}
			return entries
		}
	}
	return nil
}
