// Package analyze turns a selection into words with furigana and romaji
// and finds the dictionary entries for each word.
package analyze

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

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

type Tokenizer interface {
	Tokenize(s string) []morph.Token
}

type Dictionary interface {
	Lookup(word string) []dict.Entry
	LookupReading(word, reading string) []dict.Entry
	LongestPrefix(s string, maxRunes int) (int, []dict.Entry)
	Kanji(r rune) (dict.Kanji, bool)
	Furigana(word, reading string) ([]dict.Ruby, bool)
}

// Seg is a run of surface text with the reading shown above it; Ruby is ""
// over kana or anything else that needs no reading.
type Seg struct{ Text, Ruby string }

// Word is a content word with the particles, endings and auxiliaries that
// attach to it, or a lone particle or symbol.
type Word struct {
	Start, End int // byte offsets into the analysed text
	Surface    string
	Segs       []Seg
	Reading    string // hiragana
	Romaji     string
	Key        string // dictionary form of the head morpheme
	KeyReading string // its hiragana reading, "" when unknown
	Content    bool   // a word worth looking up first (not a particle or symbol)
	Symbol     bool
	Parts      []Word // constituent particles when merged from a compound particle

	pos string // head morpheme's major POS (t.POS[0])

	// katakana reading and pronunciation not yet turned into Romaji; romaji
	// is made per run rather than per morpheme so a trailing っ doubles the
	// next morpheme's consonant (なかっ+た -> nakatta)
	kana, pron string
}

type Analyzer struct {
	Morph Tokenizer  // nil: every selection is one unanalysed word
	Dict  Dictionary // nil: no compound merging
	Style jp.Style
}

// Words splits text into display words.
func (a *Analyzer) Words(text string) []Word {
	toks := a.tokens(text)
	var words []Word
	var cur *Word
	var prev *morph.Token
	for i := range toks {
		t := &toks[i]
		if strings.TrimSpace(t.Surface) == "" {
			cur, prev = nil, nil
			continue
		}
		switch {
		case cur == nil || !attaches(t, prev, cur):
			words = append(words, Word{Start: t.Start})
			cur = &words[len(words)-1]
			a.head(cur, t)
		case prev.POS[0] == "接頭詞":
			a.head(cur, t) // お薬 is looked up as 薬 when お薬 is not in the dictionary
		}
		a.add(cur, t)
		prev = t
	}
	for i := range words {
		a.flush(&words[i])
	}
	return a.mergeCompounds(text, words)
}

func (a *Analyzer) tokens(text string) []morph.Token {
	if a.Morph != nil {
		return a.Morph.Tokenize(text)
	}
	tok := morph.Token{Surface: text, End: len(text), Base: text}
	if jp.IsKanaOnly(text) {
		tok.Reading = jp.ToKatakana(text)
		tok.Known = true
	}
	return []morph.Token{tok}
}

// attaches reports whether t continues the current word, given the token
// before it, rather than starting a new one.
func attaches(t, prev *morph.Token, cur *Word) bool {
	if prev.POS[0] == "接頭詞" {
		return true
	}
	if cur.Symbol {
		return false
	}
	inflecting := prev.POS[0] == "動詞" || prev.POS[0] == "形容詞" || prev.POS[0] == "助動詞"
	switch t.POS[0] {
	case "助動詞":
		// endings after verbs and adjectives; the copula after nouns stands alone
		return inflecting
	case "動詞", "形容詞":
		// 食べ+させる, 読んで+いる; not いけない after the は of 読んではいけない
		return t.POS[1] == "接尾" || t.POS[1] == "非自立" && (inflecting || prev.POS[1] == "接続助詞")
	case "名詞":
		return t.POS[1] == "接尾" && cur.Content
	case "助詞":
		// て, ば, ながら, and the たり of 食べたり
		return inflecting && (t.POS[1] == "接続助詞" || t.POS[1] == "並立助詞" && (t.Base == "たり" || t.Base == "だり"))
	}
	return false
}

// head sets the word's dictionary key from its first morpheme.
func (a *Analyzer) head(w *Word, t *morph.Token) {
	w.pos = t.POS[0]
	w.Symbol = t.POS[0] == "記号" || (!t.Known && !jp.HasJapanese(t.Surface))
	w.Content = !w.Symbol && t.POS[0] != "助詞" && t.POS[0] != "助動詞"
	if w.Symbol {
		return
	}
	w.Key = t.Base
	if w.Key == "" {
		w.Key = t.Surface
	}
	w.KeyReading = baseReading(t.Surface, t.Base, jp.ToHiragana(t.Reading))
}

func (a *Analyzer) add(w *Word, t *morph.Token) {
	w.End = t.End
	w.Surface += t.Surface
	reading := jp.ToHiragana(t.Reading)
	if reading == "" && jp.IsKanaOnly(t.Surface) {
		reading = jp.ToHiragana(t.Surface)
	}
	w.Reading += reading
	w.Segs = appendSegs(w.Segs, a.furigana(t, reading))
	switch {
	case reading == "" && jp.HasJapanese(t.Surface):
		a.flush(w) // kanji nobody knows the reading of: no romaji rather than a guess
	case t.POS[0] == "記号" || !jp.HasJapanese(t.Surface) || reading == "":
		a.flush(w)
		w.Romaji += punct(t.Surface)
	default:
		pron := t.Pron
		if pron == "" {
			pron = t.Reading
		}
		if pron == "" {
			pron = jp.ToKatakana(t.Surface)
		}
		w.kana += jp.ToKatakana(reading)
		w.pron += pron
	}
}

func (a *Analyzer) flush(w *Word) {
	if w.kana != "" {
		w.Romaji += jp.RomajiPron(w.kana, w.pron, a.Style)
		w.kana, w.pron = "", ""
	}
}

var punctMap = map[rune]string{
	'。': ".", '、': ",", '！': "!", '？': "?", '「': "“", '」': "”", '『': "“", '』': "”",
	'（': "(", '）': ")", '・': "·", '…': "…", '〜': "~", '～': "~", '：': ":", '　': " ",
}

func punct(s string) string {
	var b strings.Builder
	for _, r := range s {
		if p, ok := punctMap[r]; ok {
			b.WriteString(p)
		} else if r < utf8.RuneSelf || unicode.IsLetter(r) || unicode.IsDigit(r) {
			b.WriteRune(r)
		}
	}
	return strings.TrimSpace(b.String())
}

// baseReading derives the reading of a morpheme's dictionary form from its
// surface reading: 書か/かか with base 書く gives かく.
func baseReading(surface, base, reading string) string {
	if base == "" || base == surface {
		return reading
	}
	if base == "来る" { // the stem's reading changes: 来(き)ます, 来(こ)ない
		return "くる"
	}
	sr, br := []rune(surface), []rune(base)
	n := 0
	for n < len(sr) && n < len(br) && sr[n] == br[n] {
		n++
	}
	sTail := jp.ToHiragana(string(sr[n:]))
	bTail := string(br[n:])
	if !strings.HasSuffix(reading, sTail) || !jp.IsKanaOnly(bTail) && bTail != "" {
		return ""
	}
	return strings.TrimSuffix(reading, sTail) + jp.ToHiragana(bTail)
}

// mergeCompounds joins runs of nouns or particles that the dictionary knows
// as one word (東京+大学 -> 東京大学, と+は -> とは), so the card shows the
// compound's entry.
func (a *Analyzer) mergeCompounds(text string, words []Word) []Word {
	if a.Dict == nil || len(words) < 2 {
		return words
	}
	out := words[:0:0]
	for i := 0; i < len(words); i++ {
		w := words[i]
		switch {
		case isNounish(w):
			if n, es := a.Dict.LongestPrefix(text[w.Start:], 16); len(es) > 0 {
				end := w.Start + n
				j := i + 1
				for j < len(words) && words[j].Start == words[j-1].End && words[j].End <= end && isNounish(words[j]) {
					j++
				}
				if j-i > 1 && words[j-1].End == end {
					w = a.join(words[i:j])
					i = j - 1
				}
			}
		case w.pos == "助詞":
			j := i + 1
			for j < len(words) && j-i < 3 && words[j].Start == words[j-1].End && words[j].pos == "助詞" {
				j++
			}
			for k := j; k > i+1; k-- {
				if a.isCompoundParticle(text[w.Start:words[k-1].End]) {
					w = a.join(words[i:k])
					w.Parts = slices.Clone(words[i:k])
					i = k - 1
					break
				}
			}
		}
		out = append(out, w)
	}
	return out
}

func (a *Analyzer) isCompoundParticle(s string) bool {
	for _, e := range a.Dict.Lookup(s) {
		if !dict.WrittenInKana(e) || len(e.Senses) == 0 {
			continue
		}
		pos := e.Senses[0].POS
		if slices.Contains(pos, "prt") || slices.Contains(pos, "exp") || slices.Contains(pos, "conj") {
			return true
		}
	}
	return false
}

// isNounish reports whether every rune is kanji or katakana, the shape of
// noun compounds; good enough without carrying POS along.
func isNounish(w Word) bool {
	if w.Symbol || !w.Content {
		return false
	}
	for _, r := range w.Surface {
		if !jp.IsKanji(r) && !jp.IsKatakana(r) {
			return false
		}
	}
	return true
}

func appendSegs(segs, more []Seg) []Seg {
	for _, s := range more {
		if n := len(segs); n > 0 && s.Ruby == "" && segs[n-1].Ruby == "" {
			segs[n-1].Text += s.Text
			continue
		}
		segs = append(segs, s)
	}
	return segs
}

// join merges consecutive words into one looked up as a whole.
func (a *Analyzer) join(words []Word) Word {
	m := words[0]
	m.Segs = slices.Clone(m.Segs)
	sep := " "
	if m.pos == "助詞" {
		sep = ""
	}
	for _, x := range words[1:] {
		m.End = x.End
		m.Surface += x.Surface
		m.Reading += x.Reading
		m.Romaji += sep + x.Romaji
		m.Segs = appendSegs(m.Segs, x.Segs)
	}
	m.Key, m.KeyReading = m.Surface, m.Reading
	if m.pos != "助詞" {
		m.Content = true
	}
	if rs, ok := a.Dict.Furigana(m.Surface, m.Reading); ok {
		m.Segs = segs(rs)
	}
	return m
}
