// Copyright 2023 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package text_test

import (
	"bytes"
	"image"
	"image/color"
	"math"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"github.com/hajimehoshi/bitmapfont/v3"
	"golang.org/x/image/font"
	"golang.org/x/image/font/opentype"
	"golang.org/x/image/math/fixed"

	"github.com/hajimehoshi/ebiten/v2"
	t "github.com/hajimehoshi/ebiten/v2/internal/testing"
	"github.com/hajimehoshi/ebiten/v2/text/v2"
)

func TestMain(m *testing.M) {
	t.MainWithRunLoop(m)
}

func TestGlyphIndex(t *testing.T) {
	const sampleText = `The quick brown fox jumps
over the lazy dog.`

	f := text.NewGoXFace(bitmapfont.Face)
	got := sampleText
	for _, g := range text.AppendGlyphs(nil, sampleText, f, nil) {
		got = got[:g.StartIndexInBytes] + strings.Repeat(" ", g.EndIndexInBytes-g.StartIndexInBytes) + got[g.EndIndexInBytes:]
	}
	want := regexp.MustCompile(`\S`).ReplaceAllString(sampleText, " ")
	if got != want {
		t.Errorf("got: %q, want: %q", got, want)
	}
}

func TestTextColor(t *testing.T) {
	clr := color.RGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
	img := ebiten.NewImage(30, 30)
	op := &text.DrawOptions{}
	op.GeoM.Translate(0, 0)
	op.ColorScale.ScaleWithColor(clr)
	text.Draw(img, "Hello", text.NewGoXFace(bitmapfont.Face), op)

	w, h := img.Bounds().Dx(), img.Bounds().Dy()
	allTransparent := true
	for j := 0; j < h; j++ {
		for i := 0; i < w; i++ {
			got := img.At(i, j)
			want1 := color.RGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
			want2 := color.RGBA{}
			if got != want1 && got != want2 {
				t.Errorf("img At(%d, %d): got %v; want %v or %v", i, j, got, want1, want2)
			}
			if got == want1 {
				allTransparent = false
			}
		}
	}
	if allTransparent {
		t.Fail()
	}
}

const testGoXFaceSize = 6

type testGoXFace struct{}

func (f *testGoXFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
	dr = image.Rect(0, 0, testGoXFaceSize, testGoXFaceSize)
	a := image.NewAlpha(dr)
	switch r {
	case 'a':
		for j := dr.Min.Y; j < dr.Max.Y; j++ {
			for i := dr.Min.X; i < dr.Max.X; i++ {
				a.SetAlpha(i, j, color.Alpha{A: 0x80})
			}
		}
	case 'b':
		for j := dr.Min.Y; j < dr.Max.Y; j++ {
			for i := dr.Min.X; i < dr.Max.X; i++ {
				a.SetAlpha(i, j, color.Alpha{A: 0xff})
			}
		}
	}
	mask = a
	advance = fixed.I(testGoXFaceSize)
	ok = true
	return
}

func (f *testGoXFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
	bounds = fixed.R(0, 0, testGoXFaceSize, testGoXFaceSize)
	advance = fixed.I(testGoXFaceSize)
	ok = true
	return
}

func (f *testGoXFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
	return fixed.I(testGoXFaceSize), true
}

func (f *testGoXFace) Kern(r0, r1 rune) fixed.Int26_6 {
	if r1 == 'b' {
		return fixed.I(-testGoXFaceSize)
	}
	return 0
}

func (f *testGoXFace) Close() error {
	return nil
}

func (f *testGoXFace) Metrics() font.Metrics {
	return font.Metrics{
		Height:     fixed.I(testGoXFaceSize),
		Ascent:     0,
		Descent:    fixed.I(testGoXFaceSize),
		XHeight:    0,
		CapHeight:  fixed.I(testGoXFaceSize),
		CaretSlope: image.Pt(0, 1),
	}
}

// Issue #1378
func TestNegativeKern(t *testing.T) {
	f := text.NewGoXFace(&testGoXFace{})
	dst := ebiten.NewImage(testGoXFaceSize*2, testGoXFaceSize)

	// With testGoXFace, 'b' is rendered at the previous position as 0xff.
	// 'a' is rendered at the current position as 0x80.
	op := &text.DrawOptions{}
	op.GeoM.Translate(0, 0)
	text.Draw(dst, "ab", f, op)
	for j := 0; j < testGoXFaceSize; j++ {
		for i := 0; i < testGoXFaceSize; i++ {
			got := dst.At(i, j)
			want := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
			if got != want {
				t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
			}
		}
	}

	// The glyph 'a' should be treated correctly.
	op = &text.DrawOptions{}
	op.GeoM.Translate(testGoXFaceSize, 0)
	text.Draw(dst, "a", f, op)
	for j := 0; j < testGoXFaceSize; j++ {
		for i := testGoXFaceSize; i < testGoXFaceSize*2; i++ {
			got := dst.At(i, j)
			want := color.RGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
			if got != want {
				t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
			}
		}
	}
}

type unhashableGoXFace func()

const unhashableGoXFaceSize = 10

func (u *unhashableGoXFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
	dr = image.Rect(0, 0, unhashableGoXFaceSize, unhashableGoXFaceSize)
	a := image.NewAlpha(dr)
	for j := dr.Min.Y; j < dr.Max.Y; j++ {
		for i := dr.Min.X; i < dr.Max.X; i++ {
			a.SetAlpha(i, j, color.Alpha{A: 0xff})
		}
	}
	mask = a
	advance = fixed.I(unhashableGoXFaceSize)
	ok = true
	return
}

func (u *unhashableGoXFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
	bounds = fixed.R(0, 0, unhashableGoXFaceSize, unhashableGoXFaceSize)
	advance = fixed.I(unhashableGoXFaceSize)
	ok = true
	return
}

func (u *unhashableGoXFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
	return fixed.I(unhashableGoXFaceSize), true
}

func (u *unhashableGoXFace) Kern(r0, r1 rune) fixed.Int26_6 {
	return 0
}

func (u *unhashableGoXFace) Close() error {
	return nil
}

func (u *unhashableGoXFace) Metrics() font.Metrics {
	return font.Metrics{
		Height:     fixed.I(unhashableGoXFaceSize),
		Ascent:     0,
		Descent:    fixed.I(unhashableGoXFaceSize),
		XHeight:    0,
		CapHeight:  fixed.I(unhashableGoXFaceSize),
		CaretSlope: image.Pt(0, 1),
	}
}

// Issue #2669
func TestUnhashableFace(t *testing.T) {
	var face unhashableGoXFace
	f := text.NewGoXFace(&face)
	dst := ebiten.NewImage(unhashableGoXFaceSize*2, unhashableGoXFaceSize*2)
	text.Draw(dst, "a", f, nil)

	for j := 0; j < unhashableGoXFaceSize*2; j++ {
		for i := 0; i < unhashableGoXFaceSize*2; i++ {
			got := dst.At(i, j)
			var want color.RGBA
			if i < unhashableGoXFaceSize && j < unhashableGoXFaceSize {
				want = color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
			}
			if got != want {
				t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
			}
		}
	}
}

func TestConvertToFixed26_6(t *testing.T) {
	testCases := []struct {
		In  float64
		Out fixed.Int26_6
	}{
		{
			In:  0,
			Out: 0,
		},
		{
			In:  0.25,
			Out: fixed.I(1) / 4,
		},
		{
			In:  0.5,
			Out: fixed.I(1) / 2,
		},
		{
			In:  1.25,
			Out: fixed.I(1) * 5 / 4,
		},
		{
			In:  1,
			Out: fixed.I(1),
		},
		{
			In:  -0.25,
			Out: fixed.I(-1) / 4,
		},
		{
			In:  -0.5,
			Out: fixed.I(-1) / 2,
		},
		{
			In:  -1,
			Out: fixed.I(-1),
		},
		{
			In:  -1.25,
			Out: fixed.I(-1) * 5 / 4,
		},
	}

	for _, tc := range testCases {
		got := text.Float32ToFixed26_6(float32(tc.In))
		want := tc.Out
		if got != want {
			t.Errorf("Float32ToFixed26_6(%v): got: %v, want: %v", tc.In, got, want)
		}

		got = text.Float64ToFixed26_6(tc.In)
		want = tc.Out
		if got != want {
			t.Errorf("Float32ToFixed26_6(%v): got: %v, want: %v", tc.In, got, want)
		}
	}
}

func TestConvertToFloat(t *testing.T) {
	testCases := []struct {
		In  fixed.Int26_6
		Out float64
	}{
		{
			In:  0,
			Out: 0,
		},
		{
			In:  fixed.I(1) / 4,
			Out: 0.25,
		},
		{
			In:  fixed.I(1) / 2,
			Out: 0.5,
		},
		{
			In:  fixed.I(1) * 5 / 4,
			Out: 1.25,
		},
		{
			In:  fixed.I(1),
			Out: 1,
		},
		{
			In:  fixed.I(-1) / 4,
			Out: -0.25,
		},
		{
			In:  fixed.I(-1) / 2,
			Out: -0.5,
		},
		{
			In:  fixed.I(-1),
			Out: -1,
		},
		{
			In:  fixed.I(-1) * 5 / 4,
			Out: -1.25,
		},
	}

	for _, tc := range testCases {
		got := text.Fixed26_6ToFloat32(tc.In)
		want := float32(tc.Out)
		if got != want {
			t.Errorf("Fixed26_6ToFloat32(%v): got: %v, want: %v", tc.In, got, want)
		}

		got64 := text.Fixed26_6ToFloat64(tc.In)
		want64 := tc.Out
		if got64 != want64 {
			t.Errorf("Fixed26_6ToFloat64(%v): got: %v, want: %v", tc.In, got64, want64)
		}
	}
}

// Issue #2954
func TestDrawOptionsNotModified(t *testing.T) {
	img := ebiten.NewImage(30, 30)

	op := &text.DrawOptions{}
	text.Draw(img, "Hello", text.NewGoXFace(bitmapfont.Face), op)

	if got, want := op.GeoM, (ebiten.GeoM{}); got != want {
		t.Errorf("got: %v, want: %v", got, want)
	}
	if got, want := op.ColorScale, (ebiten.ColorScale{}); got != want {
		t.Errorf("got: %v, want: %v", got, want)
	}
}

func TestGoXFaceMetrics(t *testing.T) {
	const size = 100

	fontFiles := []string{
		// MPLUS1p-Regular.ttf is an old version of M+ 1p font, and this doesn't have metadata.
		"MPLUS1p-Regular.ttf",
		"Roboto-Regular.ttf",
	}

	for _, fontFile := range fontFiles {
		fontFile := fontFile
		t.Run(fontFile, func(t *testing.T) {
			fontdata, err := os.ReadFile(filepath.Join("testdata", fontFile))
			if err != nil {
				t.Fatal(err)
			}

			sfntFont, err := opentype.Parse(fontdata)
			if err != nil {
				t.Fatal(err)
			}
			opentypeFace, err := opentype.NewFace(sfntFont, &opentype.FaceOptions{
				Size: size,
				DPI:  72,
			})
			if err != nil {
				t.Fatal(err)
			}
			goXFace := text.NewGoXFace(opentypeFace)
			goXMetrics := goXFace.Metrics()
			if goXMetrics.XHeight <= 0 {
				t.Errorf("GoXFace's XHeight must be positive but not: %f", goXMetrics.XHeight)
			}
			if goXMetrics.CapHeight <= 0 {
				t.Errorf("GoXFace's CapHeight must be positive but not: %f", goXMetrics.CapHeight)
			}

			goTextFaceSource, err := text.NewGoTextFaceSource(bytes.NewBuffer(fontdata))
			if err != nil {
				t.Fatal(err)
			}
			goTextFace := &text.GoTextFace{
				Source: goTextFaceSource,
				Size:   size,
			}
			goTextMetrics := goTextFace.Metrics()
			if goTextMetrics.XHeight <= 0 {
				t.Errorf("GoTextFace's XHeight must be positive but not: %f", goTextMetrics.XHeight)
			}
			if goTextMetrics.CapHeight <= 0 {
				t.Errorf("GoTextFace's CapHeight must be positive but not: %f", goTextMetrics.CapHeight)
			}

			if math.Abs(goXMetrics.XHeight-goTextMetrics.XHeight) >= 0.1 {
				t.Errorf("XHeight values don't match: %f (GoXFace) vs %f (GoTextFace)", goXMetrics.XHeight, goTextMetrics.XHeight)
			}
			if math.Abs(goXMetrics.CapHeight-goTextMetrics.CapHeight) >= 0.1 {
				t.Errorf("CapHeight values don't match: %f (GoXFace) vs %f (GoTextFace)", goXMetrics.CapHeight, goTextMetrics.CapHeight)
			}

			// Check that a MultiFace should have the same metrics.
			multiFace, err := text.NewMultiFace(goTextFace)
			if err != nil {
				t.Fatal(err)
			}
			if got := multiFace.Metrics(); got != goTextMetrics {
				t.Errorf("got: %v, want: %v", got, goTextMetrics)
			}
		})
	}
}
