package analyze

import (
	"slices"
	"strings"
	"unicode/utf8"

	"chirami/internal/dict"
	"chirami/internal/jp"
)

// Entries finds dictionary entries for w, best first: the exact surface (or
// dictionary form first for inflected verbs/adjectives), then the dictionary
// form of its head, then the longest known prefix.
func (a *Analyzer) Entries(w Word) []dict.Entry {
	if a.Dict == nil || w.Symbol {
		return nil
	}
	if w.Key != "" && w.Key != w.Surface && (w.pos == "動詞" || w.pos == "形容詞") {
		if es := a.Dict.LookupReading(w.Key, w.KeyReading); len(es) > 0 {
			return rankByPOS(es, w)
		}
	}
	if es := a.Dict.LookupReading(w.Surface, w.Reading); len(es) > 0 {
		return rankByPOS(es, w)
	}
	if w.Key != "" && w.Key != w.Surface {
		if es := a.Dict.LookupReading(w.Key, w.KeyReading); len(es) > 0 {
			return rankByPOS(es, w)
		}
	}
	if _, es := a.Dict.LongestPrefix(w.Surface, 12); len(es) > 0 {
		return rankByPOS(es, w)
	}
	return nil
}

// rankByPOS prefers entries whose JMdict part of speech matches the
// tokenizer's POS for kana words (e.g. かも as particle vs 鴨 as noun).
func rankByPOS(es []dict.Entry, w Word) []dict.Entry {
	if w.pos == "" || len(es) <= 1 || !jp.IsKanaOnly(w.Surface) {
		return es
	}
	slices.SortStableFunc(es, func(a, b dict.Entry) int {
		return posRank(b, w) - posRank(a, w)
	})
	return es
}

func posRank(e dict.Entry, w Word) int {
	if !dict.WrittenInKana(e) {
		return 0
	}
	common := hasCommonKana(e, w.Reading)
	if !common && w.Content {
		return 0
	}
	s := 0
	if common {
		s += 2
	}
	if matchesPOS(e, w.pos) {
		s += 4
	}
	return s
}

func hasCommonKana(e dict.Entry, reading string) bool {
	for _, r := range e.Kana {
		if r.Common && jp.ToHiragana(r.Text) == reading {
			return true
		}
	}
	return false
}

func matchesPOS(e dict.Entry, pos string) bool {
	for _, s := range e.Senses {
		for _, p := range s.POS {
			switch pos {
			case "助詞":
				if p == "prt" || p == "exp" || p == "conj" {
					return true
				}
			case "助動詞":
				if p == "cop" || p == "aux" || p == "aux-v" || p == "aux-adj" {
					return true
				}
			case "接続詞":
				if p == "conj" || p == "prt" || p == "exp" {
					return true
				}
			case "副詞":
				if p == "adv" || p == "adv-to" {
					return true
				}
			case "動詞":
				if strings.HasPrefix(p, "v") {
					return true
				}
			case "形容詞":
				if p == "adj-i" || p == "adj-ix" {
					return true
				}
			case "名詞":
				if p == "n" || p == "pn" || p == "adj-na" || p == "adj-no" || p == "num" || p == "ctr" {
					return true
				}
			}
		}
	}
	return false
}

// Kanji returns KANJIDIC entries for the distinct kanji in s, at most max.
func (a *Analyzer) Kanji(s string, max int) []dict.Kanji {
	if a.Dict == nil {
		return nil
	}
	var out []dict.Kanji
	seen := map[rune]bool{}
	for _, r := range s {
		if !jp.IsKanji(r) || seen[r] || len(out) == max {
			continue
		}
		seen[r] = true
		if k, ok := a.Dict.Kanji(r); ok {
			out = append(out, k)
		}
	}
	return out
}

// Romaji romanizes reading, the reading of word. Long vowels need morpheme
// boundaries (東京 tōkyō, but 思う omou), so it tokenizes word and uses the
// tokens' pronunciations when they spell out reading; otherwise it falls back
// to the context-free conversion.
func (a *Analyzer) Romaji(word, reading string) string {
	if a.Morph != nil && word != "" {
		var kana, pron string
		for _, t := range a.Morph.Tokenize(word) {
			if t.Reading == "" {
				kana = ""
				break
			}
			kana += t.Reading
			pron += cmp(t.Pron, t.Reading)
		}
		if kana != "" && jp.ToHiragana(kana) == jp.ToHiragana(reading) {
			return jp.RomajiPron(kana, pron, a.Style)
		}
	}
	return jp.Romaji(reading, a.Style)
}

func cmp(s, fallback string) string {
	if s != "" {
		return s
	}
	return fallback
}

// Phrase returns the whole selection as one word when the dictionary knows
// it as a whole (an idiom such as 猫に小判, a proverb), so the card can offer
// its entry while keeping each word clickable.
func (a *Analyzer) Phrase(text string, words []Word) (Word, bool) {
	if a.Dict == nil || len(words) < 2 || utf8.RuneCountInString(text) > 24 {
		return Word{}, false
	}
	for _, w := range words {
		if w.Symbol {
			return Word{}, false
		}
	}
	if len(a.Dict.Lookup(text)) == 0 {
		return Word{}, false
	}
	return a.join(words), true
}
