package jp

// Port of WanaKana (github.com/WaniKani/WanaKana, MIT License) kana->romaji conversion
// as the core engine for Wapuro style romanization.
// See WANAKANA_LICENSE for original license notice.

import (
	_ "embed"
	"encoding/json"
	"strings"
	"unicode"
)

type Style int

const (
	Macron Style = iota
	Wapuro
	Plain
)

// IsHiragana reports whether r is a hiragana character.
func IsHiragana(r rune) bool {
	return (r >= 0x3041 && r <= 0x3096) || (r >= 0x309D && r <= 0x309F)
}

// IsKatakana reports whether r is a katakana character, including ー and half-width katakana.
func IsKatakana(r rune) bool {
	if r == 'ー' || r == 'ｰ' {
		return true
	}
	if r >= 0x30A1 && r <= 0x30FA {
		return true
	}
	if r >= 0x30FD && r <= 0x30FF {
		return true
	}
	if r >= 0xFF66 && r <= 0xFF9F {
		return true
	}
	return false
}

// IsKana reports whether r is hiragana or katakana.
func IsKana(r rune) bool {
	return IsHiragana(r) || IsKatakana(r)
}

// IsKanji reports whether r is a kanji character (CJK unified ideographs + ext A + 々〆ヵヶ).
func IsKanji(r rune) bool {
	if r == '々' || r == '〆' || r == 'ヵ' || r == 'ヶ' {
		return true
	}
	if r >= 0x4E00 && r <= 0x9FFF {
		return true
	}
	if r >= 0x3400 && r <= 0x4DBF {
		return true
	}
	if r >= 0xF900 && r <= 0xFAFF {
		return true
	}
	return false
}

// HasJapanese reports whether s contains any kana or kanji.
func HasJapanese(s string) bool {
	for _, r := range s {
		if IsKana(r) || IsKanji(r) {
			return true
		}
	}
	return false
}

func isKanaLetter(r rune) bool {
	return IsKana(r) && r != 'ー' && r != 'ｰ'
}

// IsKanaOnly reports whether s contains >=1 kana and only kana/ー/punct/space.
func IsKanaOnly(s string) bool {
	hasKana := false
	for _, r := range s {
		if isKanaLetter(r) {
			hasKana = true
			continue
		}
		if r == 'ー' || r == 'ｰ' || unicode.IsPunct(r) || unicode.IsSpace(r) {
			continue
		}
		return false
	}
	return hasKana
}

var halfwidthKatakana = map[rune]rune{
	'｡': '。', '｢': '「', '｣': '」', '､': '、', '･': '・',
	'ｦ': 'ヲ', 'ｧ': 'ァ', 'ｨ': 'ィ', 'ｩ': 'ゥ', 'ｪ': 'ェ', 'ｫ': 'ォ',
	'ｬ': 'ャ', 'ｭ': 'ュ', 'ｮ': 'ョ', 'ｯ': 'ッ', 'ｰ': 'ー',
	'ｱ': 'ア', 'ｲ': 'イ', 'ｳ': 'ウ', 'ｴ': 'エ', 'ｵ': 'オ',
	'ｶ': 'カ', 'ｷ': 'キ', 'ｸ': 'ク', 'ｹ': 'ケ', 'ｺ': 'コ',
	'ｻ': 'サ', 'ｼ': 'シ', 'ｽ': 'ス', 'ｾ': 'セ', 'ｿ': 'ソ',
	'ﾀ': 'タ', 'ﾁ': 'チ', 'ﾂ': 'ツ', 'ﾃ': 'テ', 'ﾄ': 'ト',
	'ﾅ': 'ナ', 'ﾆ': 'ニ', 'ﾇ': 'ヌ', 'ﾈ': 'ネ', 'ﾉ': 'ノ',
	'ﾊ': 'ハ', 'ﾋ': 'ヒ', 'ﾌ': 'フ', 'ﾍ': 'ヘ', 'ﾎ': 'ホ',
	'ﾏ': 'マ', 'ﾐ': 'ミ', 'ﾑ': 'ム', 'ﾒ': 'メ', 'ﾓ': 'モ',
	'ﾔ': 'ヤ', 'ﾕ': 'ユ', 'ﾖ': 'ヨ',
	'ﾗ': 'ラ', 'ﾘ': 'リ', 'ﾙ': 'ル', 'ﾚ': 'レ', 'ﾛ': 'ロ',
	'ﾜ': 'ワ', 'ﾝ': 'ン',
}

var voicedKatakana = map[rune]rune{
	'カ': 'ガ', 'キ': 'ギ', 'ク': 'グ', 'ケ': 'ゲ', 'コ': 'ゴ',
	'サ': 'ザ', 'シ': 'ジ', 'ス': 'ズ', 'セ': 'ゼ', 'ソ': 'ゾ',
	'タ': 'ダ', 'チ': 'ヂ', 'ツ': 'ヅ', 'テ': 'デ', 'ト': 'ド',
	'ハ': 'バ', 'ヒ': 'ビ', 'フ': 'ブ', 'ヘ': 'ベ', 'ホ': 'ボ',
	'ウ': 'ヴ', 'ワ': 'ヷ', 'ヰ': 'ヸ', 'ヱ': 'ヹ', 'ヲ': 'ヺ',
}

var semiVoicedKatakana = map[rune]rune{
	'ハ': 'パ', 'ヒ': 'ピ', 'フ': 'プ', 'ヘ': 'ペ', 'ホ': 'ポ',
}

func normalizeHalfwidth(s string) string {
	runes := []rune(s)
	var out []rune
	for i := 0; i < len(runes); i++ {
		r := runes[i]
		if r == 'ﾞ' || r == 'ﾟ' {
			continue
		}
		if full, ok := halfwidthKatakana[r]; ok {
			if i+1 < len(runes) {
				if runes[i+1] == 'ﾞ' {
					if v, vok := voicedKatakana[full]; vok {
						out = append(out, v)
						i++
						continue
					}
				} else if runes[i+1] == 'ﾟ' {
					if sv, sok := semiVoicedKatakana[full]; sok {
						out = append(out, sv)
						i++
						continue
					}
				}
			}
			out = append(out, full)
		} else {
			out = append(out, r)
		}
	}
	return string(out)
}

// ToHiragana converts katakana to hiragana, leaving other characters unchanged.
func ToHiragana(s string) string {
	s = normalizeHalfwidth(s)
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		if r >= 0x30A1 && r <= 0x30F6 {
			b.WriteRune(r - 0x60)
		} else if r == 0x30FD {
			b.WriteRune(0x309D) // ヽ -> ゝ
		} else if r == 0x30FE {
			b.WriteRune(0x309E) // ヾ -> ゞ
		} else {
			b.WriteRune(r)
		}
	}
	return b.String()
}

// ToKatakana converts hiragana to katakana, leaving other characters unchanged.
func ToKatakana(s string) string {
	s = normalizeHalfwidth(s)
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		if r >= 0x3041 && r <= 0x3096 {
			b.WriteRune(r + 0x60)
		} else if r == 0x309D {
			b.WriteRune(0x30FD) // ゝ -> ヽ
		} else if r == 0x309E {
			b.WriteRune(0x30FE) // ゞ -> ヾ
		} else {
			b.WriteRune(r)
		}
	}
	return b.String()
}

func isSmallAttaching(r rune) bool {
	switch r {
	case 'ゃ', 'ゅ', 'ょ', 'ぁ', 'ぃ', 'ぅ', 'ぇ', 'ぉ', 'ゎ',
		'ャ', 'ュ', 'ョ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ヮ',
		'ｬ', 'ｭ', 'ｮ', 'ｧ', 'ｨ', 'ｩ', 'ｪ', 'ｫ':
		return true
	default:
		return false
	}
}

// Moras segments kana into moras. Small ゃゅょぁぃぅぇぉ and ゎ attach to the
// previous kana; っ, ん, and ー are each their own mora.
func Moras(kana string) []string {
	var moras []string
	for _, r := range kana {
		if isSmallAttaching(r) && len(moras) > 0 {
			moras[len(moras)-1] += string(r)
		} else {
			moras = append(moras, string(r))
		}
	}
	return moras
}

//go:embed wanakana_tree.json
var wanakanaTreeJSON []byte

type trieNode struct {
	val      string
	children map[rune]*trieNode
}

var wanakanaRoot *trieNode

func parseTrie(raw map[string]interface{}) *trieNode {
	n := &trieNode{children: make(map[rune]*trieNode)}
	for k, v := range raw {
		if k == "" {
			if s, ok := v.(string); ok {
				n.val = s
			}
		} else {
			r := []rune(k)[0]
			if childRaw, ok := v.(map[string]interface{}); ok {
				n.children[r] = parseTrie(childRaw)
			}
		}
	}
	return n
}

func init() {
	var raw map[string]interface{}
	if err := json.Unmarshal(wanakanaTreeJSON, &raw); err != nil {
		panic(err)
	}
	wanakanaRoot = parseTrie(raw)
}

func isCharKatakana(r rune) bool {
	return r >= 0x30A1 && r <= 0x30FC
}

var wanakanaLongVowels = map[byte]rune{
	'a': 'あ',
	'i': 'い',
	'u': 'う',
	'e': 'え',
	'o': 'う',
}

// wanakanaKatakanaToHiragana implements WanaKana's katakanaToHiragana transform
// with convertLongVowelMark=true and isDestinationRomaji=true.
func wanakanaKatakanaToHiragana(s string) string {
	runes := []rune(s)
	var b strings.Builder
	var prevKana string

	for idx, r := range runes {
		if r == '・' || (r == 'ー' && idx < 1) || r == 'ヶ' || r == 'ヵ' {
			b.WriteRune(r)
			prevKana = ""
			continue
		}

		if prevKana != "" && r == 'ー' && idx > 0 {
			romaji := wanakanaApplyMapping(prevKana)
			var lastV byte
			if len(romaji) > 0 {
				lastV = romaji[len(romaji)-1]
			}
			if isCharKatakana(runes[idx-1]) && lastV == 'o' {
				b.WriteRune('お')
			} else if mapped, ok := wanakanaLongVowels[lastV]; ok {
				b.WriteRune(mapped)
			}
			continue
		}

		if r != 'ー' && isCharKatakana(r) {
			h := r - 0x60
			prevKana = string(h)
			b.WriteRune(h)
			continue
		}

		prevKana = ""
		b.WriteRune(r)
	}
	return b.String()
}

// wanakanaApplyMapping implements WanaKana's applyMapping over the Hepburn trie.
func wanakanaApplyMapping(s string) string {
	runes := []rune(s)
	var b strings.Builder
	i := 0
	n := len(runes)

	for i < n {
		curr := wanakanaRoot
		lastMatch := ""
		lastMatchIdx := -1
		j := i

		for j < n {
			next, ok := curr.children[runes[j]]
			if !ok {
				break
			}
			curr = next
			if curr.val != "" || (j == i && curr.val == "" && runes[j] == 'っ') {
				lastMatch = curr.val
				lastMatchIdx = j + 1
			}
			j++
		}

		if lastMatchIdx != -1 {
			b.WriteString(lastMatch)
			i = lastMatchIdx
		} else {
			b.WriteRune(runes[i])
			i++
		}
	}
	return b.String()
}

// ToRomajiWapuro converts kana to Wapuro romaji with 100% WanaKana parity.
func ToRomajiWapuro(s string) string {
	hira := wanakanaKatakanaToHiragana(s)
	return wanakanaApplyMapping(hira)
}

var dakutenMap = map[rune]rune{
	'か': 'が', 'き': 'ぎ', 'く': 'ぐ', 'け': 'げ', 'こ': 'ご',
	'さ': 'ざ', 'し': 'じ', 'す': 'ず', 'せ': 'ぜ', 'そ': 'ぞ',
	'た': 'だ', 'ち': 'ぢ', 'つ': 'づ', 'て': 'で', 'と': 'ど',
	'は': 'ば', 'ひ': 'び', 'ふ': 'ぶ', 'へ': 'べ', 'ほ': 'ぼ',
	'カ': 'ガ', 'キ': 'ギ', 'ク': 'グ', 'ケ': 'ゲ', 'コ': 'ゴ',
	'サ': 'ザ', 'シ': 'ジ', 'ス': 'ズ', 'セ': 'ゼ', 'ソ': 'ゾ',
	'タ': 'ダ', 'チ': 'ヂ', 'ツ': 'ヅ', 'テ': 'デ', 'ト': 'ド',
	'ハ': 'バ', 'ヒ': 'ビ', 'フ': 'ブ', 'ヘ': 'ベ', 'ホ': 'ボ',
	'ウ': 'ヴ',
}

func expandIterationMarks(kana string) string {
	runes := []rune(kana)
	var out []rune
	for _, r := range runes {
		switch r {
		case 'ゝ', 'ヽ':
			if len(out) > 0 {
				out = append(out, out[len(out)-1])
			} else {
				out = append(out, r)
			}
		case 'ゞ', 'ヾ':
			if len(out) > 0 {
				prev := out[len(out)-1]
				if voiced, ok := dakutenMap[prev]; ok {
					out = append(out, voiced)
				} else {
					out = append(out, prev)
				}
			} else {
				out = append(out, r)
			}
		default:
			out = append(out, r)
		}
	}
	return string(out)
}

var macronMap = map[rune]rune{
	'a': 'ā', 'i': 'ī', 'u': 'ū', 'e': 'ē', 'o': 'ō',
	'A': 'Ā', 'I': 'Ī', 'U': 'Ū', 'E': 'Ē', 'O': 'Ō',
}

func lastVowel(s string) rune {
	runes := []rune(s)
	for i := len(runes) - 1; i >= 0; i-- {
		ch := runes[i]
		switch ch {
		case 'a', 'i', 'u', 'e', 'o':
			return ch
		case 'ā':
			return 'a'
		case 'ī':
			return 'i'
		case 'ū':
			return 'u'
		case 'ē':
			return 'e'
		case 'ō':
			return 'o'
		}
	}
	return 0
}

func lengthenLastVowel(s string) string {
	runes := []rune(s)
	for i := len(runes) - 1; i >= 0; i-- {
		if mac, ok := macronMap[runes[i]]; ok {
			runes[i] = mac
			return string(runes)
		}
	}
	return s
}

// Romaji converts kana to Hepburn romaji context-free according to style.
// Wapuro style uses the WanaKana engine.
// Macron and Plain styles apply Hepburn conventions (e.g. ヲ/を -> "o", ー -> macron/drop).
func Romaji(kana string, st Style) string {
	if st == Wapuro {
		return ToRomajiWapuro(kana)
	}

	kana = expandIterationMarks(kana)
	moras := Moras(kana)
	var out []string

	for i := 0; i < len(moras); i++ {
		m := moras[i]
		cur := strings.Join(out, "")
		prevV := lastVowel(cur)

		if m == "を" || m == "ヲ" {
			out = append(out, "o")
			continue
		}

		if m == "っ" || m == "ッ" {
			if i+1 < len(moras) {
				nxt := ToRomajiWapuro(moras[i+1])
				if strings.HasPrefix(nxt, "ch") {
					out = append(out, "t")
				} else if len(nxt) > 0 && !strings.ContainsRune("aiueo", rune(nxt[0])) {
					out = append(out, string(nxt[0]))
				}
			}
			continue
		}

		if m == "ん" || m == "ン" {
			if i+1 < len(moras) {
				nxt := ToRomajiWapuro(moras[i+1])
				if len(nxt) > 0 && (strings.ContainsRune("aiueo", rune(nxt[0])) || strings.HasPrefix(nxt, "y")) {
					out = append(out, "n'")
				} else {
					out = append(out, "n")
				}
			} else {
				out = append(out, "n")
			}
			continue
		}

		if m == "ー" || m == "ｰ" {
			if prevV != 0 {
				switch st {
				case Macron:
					out = []string{lengthenLastVowel(cur)}
				case Plain:
					// drop
				}
			} else {
				out = append(out, "-")
			}
			continue
		}

		out = append(out, ToRomajiWapuro(m))
	}

	return strings.Join(out, "")
}

// RomajiPron converts reading aligned with pronunciation into romaji.
// It merges long vowels when pron has ー and reading has ウ after o/u
// or a vowel repeat (おお, ああ...). It preserves "ei" when reading has イ after e.
// Particles ハ->wa, ヘ->e are romanized according to pron.
// ヲ/を is romanized as "o" in Macron and Plain, and "wo" in Wapuro (unless pron is "o").
// Falls back to Romaji(reading) when pron is empty or alignment fails.
func RomajiPron(reading, pron string, st Style) string {
	if pron == "" {
		return Romaji(reading, st)
	}

	rNorm := expandIterationMarks(reading)
	pNorm := expandIterationMarks(pron)

	rMoras := Moras(ToKatakana(rNorm))
	pMoras := Moras(ToKatakana(pNorm))

	if len(rMoras) != len(pMoras) {
		return Romaji(reading, st)
	}

	var out []string

	for i := 0; i < len(rMoras); i++ {
		r := rMoras[i]
		p := pMoras[i]

		// Particle check
		var particle string
		if r == "ハ" && p == "ワ" {
			particle = "wa"
		} else if r == "ヘ" && p == "エ" {
			particle = "e"
		} else if r == "ヲ" {
			if st == Macron || st == Plain {
				particle = "o"
			} else if p == "オ" {
				particle = "o"
			} else {
				particle = "wo"
			}
		}

		cur := strings.Join(out, "")
		prevV := lastVowel(cur)

		if p == "ー" || p == "ｰ" {
			if r == "イ" && prevV == 'e' {
				// Keep "ei"
				out = append(out, "i")
			} else if r == "ウ" && (prevV == 'o' || prevV == 'u') {
				switch st {
				case Macron:
					out = []string{lengthenLastVowel(cur)}
				case Wapuro:
					out = append(out, "u")
				case Plain:
					// drop
				}
			} else if prevV != 0 && ((prevV == 'a' && r == "ア") ||
				(prevV == 'i' && r == "イ") ||
				(prevV == 'u' && r == "ウ") ||
				(prevV == 'e' && r == "エ") ||
				(prevV == 'o' && r == "オ")) {
				switch st {
				case Macron:
					out = []string{lengthenLastVowel(cur)}
				case Wapuro:
					out = append(out, string(prevV))
				case Plain:
					// drop
				}
			} else if r == "ー" || r == "ｰ" {
				if prevV != 0 {
					switch st {
					case Macron:
						out = []string{lengthenLastVowel(cur)}
					case Wapuro:
						out = append(out, string(prevV))
					case Plain:
						// drop
					}
				} else {
					out = append(out, "-")
				}
			} else {
				if particle != "" {
					out = append(out, particle)
				} else {
					out = append(out, ToRomajiWapuro(r))
				}
			}
		} else if r == "ッ" {
			if i+1 < len(rMoras) {
				nxt := ToRomajiWapuro(rMoras[i+1])
				if strings.HasPrefix(nxt, "ch") {
					out = append(out, "t")
				} else if len(nxt) > 0 && !strings.ContainsRune("aiueo", rune(nxt[0])) {
					out = append(out, string(nxt[0]))
				}
			}
		} else if r == "ン" {
			if i+1 < len(rMoras) {
				nxt := ToRomajiWapuro(rMoras[i+1])
				if len(nxt) > 0 && (strings.ContainsRune("aiueo", rune(nxt[0])) || strings.HasPrefix(nxt, "y")) {
					out = append(out, "n'")
				} else {
					out = append(out, "n")
				}
			} else {
				out = append(out, "n")
			}
		} else if r == "ー" || r == "ｰ" {
			if prevV != 0 {
				switch st {
				case Macron:
					out = []string{lengthenLastVowel(cur)}
				case Wapuro:
					out = append(out, string(prevV))
				case Plain:
					// drop
				}
			} else {
				out = append(out, "-")
			}
		} else {
			if particle != "" {
				out = append(out, particle)
			} else {
				out = append(out, ToRomajiWapuro(r))
			}
		}
	}
	return strings.Join(out, "")
}
