package card

import (
	"image/png"
	"os"
	"path/filepath"
	"testing"
)

func taberu(expanded bool) *Model {
	return &Model{Hover: -1, Word: &WordView{
		Words: []Word{{
			Segs:   []Seg{{"食", "た"}, {"べさせられなかった", ""}},
			Romaji: "tabesaserarenakatta",
			Lookup: true,
			Parts: []Part{
				{Segs: []Seg{{"食", "た"}, {"べ", ""}}, Romaji: "tabe", Gloss: "eat"},
				{Segs: []Seg{{"させ", ""}}, Romaji: "sase", Gloss: "causative", Tip: "causative: make or let someone do"},
				{Segs: []Seg{{"られ", ""}}, Romaji: "rare", Gloss: "passive", Tip: "passive: be done to"},
				{Segs: []Seg{{"なかっ", ""}}, Romaji: "nakat", Gloss: "negative"},
				{Segs: []Seg{{"た", ""}}, Romaji: "ta", Gloss: "past"},
			},
		}},
		Focus:    0,
		Expanded: expanded,
		Entries: []Entry{
			{Head: "食べる", Reading: "たべる", Romaji: "taberu", Downstep: 2, Common: true, JLPT: "N5", DictForm: true,
				Meanings: []string{"to eat", "to live on (e.g. a salary); to live off", "to subsist on"}, Tip: "ichidan verb · transitive"},
		},
		Kanji: []Chip{{Text: "食", Tip: "eat, food"}},
	}}
}

func hashi() *Model {
	return &Model{Hover: -1, Word: &WordView{
		Words:    []Word{{Segs: []Seg{{"はし", ""}}, Romaji: "hashi", Lookup: true}},
		Expanded: true,
		Entries: []Entry{
			{Head: "橋", Reading: "はし", Romaji: "hashi", Downstep: 2, Common: true, JLPT: "N5", Meanings: []string{"bridge"}},
			{Head: "箸", Reading: "はし", Downstep: 1, Meanings: []string{"chopsticks"}},
			{Head: "端", Reading: "はし", Downstep: 0, Meanings: []string{"end (e.g. of street); tip; point; edge; margin"}},
		},
		Kanji: []Chip{{Text: "橋"}},
	}}
}

func sentence() *Model {
	return &Model{Hover: -1, Word: &WordView{
		Words: []Word{
			{Segs: []Seg{{"今日", "きょう"}}, Romaji: "kyō", Lookup: true},
			{Segs: []Seg{{"は", ""}}, Romaji: "wa", Lookup: true},
			{Segs: []Seg{{"良", "よ"}, {"い", ""}}, Romaji: "yoi", Lookup: true},
			{Segs: []Seg{{"天", "てん"}, {"気", "き"}}, Romaji: "tenki", Lookup: true},
			{Segs: []Seg{{"ですね。", ""}}, Romaji: "desu ne."},
		},
		Focus: -1,
	}}
}

func kanjiPage() *Model {
	return &Model{Hover: -1, Kanji: &KanjiView{
		Back: "食べさせられなかった", Char: "食", Meta: "9 strokes · grade 2 · N4",
		Meanings: "eat, food", On: "ショク ジキ", Kun: "た.べる く.う く.らう",
		Parts:     []Chip{{Text: "人", Label: "person"}, {Text: "良", Label: "good"}},
		Words:     []Chip{{Text: "食事", Label: "meal"}, {Text: "食べ物", Label: "food"}, {Text: "和食", Label: "Japanese food"}, {Text: "食堂", Label: "cafeteria"}},
		Similar:   []Chip{{Text: "飲", Label: "drink"}, {Text: "館", Label: "building"}, {Text: "飯", Label: "meal"}},
		SimilarOf: "良",
	}}
}

func TestRender(t *testing.T) {
	f, err := LoadFonts()
	if err != nil {
		t.Skip(err)
	}
	cases := map[string]*Model{
		"collapsed": taberu(false), "expanded": taberu(true), "hashi": hashi(),
		"sentence": sentence(), "kanji": kanjiPage(),
	}
	for name, m := range cases {
		for _, th := range []struct {
			n string
			t Theme
		}{{"night", Night}, {"day", Day}} {
			r := &Renderer{Fonts: f, Theme: th.t}
			res := r.Render(m, 2)
			if res.Image.Bounds().Dx() < 100 || len(res.Regions) == 0 {
				t.Fatalf("%s: bad render %v regions=%d", name, res.Image.Bounds(), len(res.Regions))
			}
			if dir := os.Getenv("CHIRAMI_CARD_OUT"); dir != "" {
				o, _ := os.Create(filepath.Join(dir, name+"-"+th.n+".png"))
				png.Encode(o, res.Image)
				o.Close()
			}
		}
	}
}

// Hovering a region paints a chip behind its text; the text must stay on top.
func TestHoverKeepsText(t *testing.T) {
	f, err := LoadFonts()
	if err != nil {
		t.Skip(err)
	}
	r := &Renderer{Fonts: f, Theme: Night}
	const scale = 2
	for name, m := range map[string]*Model{"expanded": taberu(true), "hashi": hashi(), "kanji": kanjiPage()} {
		for i, reg := range r.Render(m, scale).Regions {
			if !reg.chip {
				continue
			}
			hm := *m
			hm.Hover = i
			img := r.Render(&hm, scale).Image
			chip := pm(Night.Hover)
			ink := 0
			// inside the chip's rounded corners
			const in = 4
			for y := int((reg.Rect.Y + in) * scale); y < int((reg.Rect.Y+reg.Rect.H-in)*scale); y++ {
				for x := int((reg.Rect.X + in) * scale); x < int((reg.Rect.X+reg.Rect.W-in)*scale); x++ {
					c := img.RGBAAt(x, y)
					if absDiff(c.R, uint8(chip.r*255)) > 40 || absDiff(c.G, uint8(chip.g*255)) > 40 || absDiff(c.B, uint8(chip.b*255)) > 40 {
						ink++
					}
				}
			}
			if ink < 20 {
				t.Errorf("%s: region %d (%v) is a blank chip when hovered", name, i, reg.Action)
			}
		}
	}
}

// In a sentence, the focused word drawn as parts still gets one chip
// behind all of them, gaps included, so the parts read as one word.
func TestFocusChipSpansParts(t *testing.T) {
	f, err := LoadFonts()
	if err != nil {
		t.Skip(err)
	}
	m := taberu(true)
	m.Word.Words = append([]Word{{Segs: []Seg{{"昨日", "きのう"}}, Romaji: "kinō", Lookup: true}}, m.Word.Words...)
	m.Word.Focus = 1
	const scale = 2
	res := (&Renderer{Fonts: f, Theme: Night}).Render(m, scale)
	var parts []Region
	for _, r := range res.Regions {
		if r.Action == None && r.sound >= 0 {
			parts = append(parts, r)
		}
	}
	if len(parts) < 2 {
		t.Fatalf("found %d part regions", len(parts))
	}
	a, b := parts[0].Rect, parts[1].Rect
	x := (a.X + a.W + b.X) / 2 // the gap between the first two parts
	y := a.Y + 8               // in the furigana row, which the chip now covers too
	got := res.Image.RGBAAt(int(x*scale), int(y*scale))
	want := pm(Night.Chip)
	if absDiff(got.R, uint8(want.r*255)) > 6 || absDiff(got.G, uint8(want.g*255)) > 6 || absDiff(got.B, uint8(want.b*255)) > 6 {
		t.Fatalf("gap between parts at (%v,%v) is %v, want the focus chip colour", x, y, got)
	}
	// ...and stops above the glosses
	gy := a.Y + a.H - 4
	got = res.Image.RGBAAt(int(x*scale), int(gy*scale))
	if absDiff(got.R, uint8(want.r*255)) <= 2 && absDiff(got.G, uint8(want.g*255)) <= 2 && absDiff(got.B, uint8(want.b*255)) <= 2 {
		t.Fatalf("the focus chip reaches the glosses at (%v,%v)", x, gy)
	}
}

// Pointing at a word or part lights up its romaji: some accent-coloured ink
// appears in the romaji line that is not there otherwise.
func TestHoverLightsRomaji(t *testing.T) {
	f, err := LoadFonts()
	if err != nil {
		t.Skip(err)
	}
	r := &Renderer{Fonts: f, Theme: Night}
	for name, m := range map[string]*Model{"parts": taberu(true), "sentence": sentence()} {
		base := r.Render(m, 1)
		lit := false
		for i, reg := range base.Regions {
			if reg.sound < 0 {
				continue
			}
			hm := *m
			hm.Hover = i
			if accentInk(r.Render(&hm, 1)) > accentInk(base) {
				lit = true
			}
		}
		if !lit {
			t.Errorf("%s: no region lights up any romaji", name)
		}
	}
}

// accentInk counts pixels close to the accent colour.
func accentInk(res *Result) int {
	a := Night.Accent
	n := 0
	b := res.Image.Bounds()
	for y := b.Min.Y; y < b.Max.Y; y++ {
		for x := b.Min.X; x < b.Max.X; x++ {
			c := res.Image.RGBAAt(x, y)
			if absDiff(c.R, a.R) < 30 && absDiff(c.G, a.G) < 30 && absDiff(c.B, a.B) < 30 {
				n++
			}
		}
	}
	return n
}

// Long headwords and readings stay inside the card: nothing bright is drawn
// right of its edge.
func TestLongEntryStaysInside(t *testing.T) {
	f, err := LoadFonts()
	if err != nil {
		t.Skip(err)
	}
	m := sentence()
	m.Word.Expanded, m.Word.Focus = true, 0
	m.Word.Entries = []Entry{{
		Head: "鳴く猫は鼠を捕らぬ", Reading: "なくねこはねずみをとらぬ", Romaji: "nakunekowanezumiotoranu",
		Downstep: 3, Common: true, JLPT: "N1", DictForm: true,
		Meanings: []string{"empty vessels make the most sound; the meowing cat does not catch the mouse"},
	}}
	const scale = 2
	res := (&Renderer{Fonts: f, Theme: Night}).Render(m, scale)
	edge := int((res.Hit.X + res.Hit.W) * scale)
	b := res.Image.Bounds()
	for y := b.Min.Y; y < b.Max.Y; y++ {
		for x := edge + 2; x < b.Max.X; x++ {
			if c := res.Image.RGBAAt(x, y); c.R > 150 || c.G > 150 || c.B > 150 {
				t.Fatalf("ink at (%d,%d) right of the card edge %d", x, y, edge)
			}
		}
	}
}

// A tip draws over the card; it must not change the card's size or move
// any region relative to the card. Above the card it may grow the image
// upwards, and then only by as much as it needs.
func TestTipMovesNothing(t *testing.T) {
	f, err := LoadFonts()
	if err != nil {
		t.Skip(err)
	}
	r := &Renderer{Fonts: f, Theme: Night}
	for _, room := range []float64{0, 200} {
		m := taberu(true)
		m.Room = room
		base := r.Render(m, 2)
		for i, reg := range base.Regions {
			if reg.Tip == "" {
				continue
			}
			tm := *m
			tm.Hover, tm.Tip = i, true
			got := r.Render(&tm, 2)
			if got.Hit.W != base.Hit.W || got.Hit.H != base.Hit.H || len(got.Regions) != len(base.Regions) {
				t.Fatalf("room %v tip %d changed the card: %v -> %v", room, i, base.Hit, got.Hit)
			}
			if room == 0 && got.Hit != base.Hit {
				t.Fatalf("tip %d grew the image without room above: %v -> %v", i, base.Hit, got.Hit)
			}
			for j := range got.Regions {
				if got.Regions[j].Rect.Y-got.Hit.Y != base.Regions[j].Rect.Y-base.Hit.Y {
					t.Fatalf("room %v tip %d moved region %d", room, i, j)
				}
			}
		}
	}
}

func absDiff(a, b uint8) int {
	if a > b {
		return int(a - b)
	}
	return int(b - a)
}

func BenchmarkRender(b *testing.B) {
	f, err := LoadFonts()
	if err != nil {
		b.Skip(err)
	}
	r := &Renderer{Fonts: f, Theme: Night}
	m := taberu(true)
	for b.Loop() {
		r.Render(m, 2)
	}
}
