// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package portable

import (
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"math"
	"os"
	"runtime"
	"testing"

	"golang.org/x/mobile/event/size"
	"golang.org/x/mobile/exp/f32"
	"golang.org/x/mobile/geom"
)

func TestAffine(t *testing.T) {
	if runtime.GOOS == "android" || runtime.GOOS == "ios" {
		t.Skipf("testdata not available on %s", runtime.GOOS)
	}
	f, err := os.Open("../../../testdata/testpattern.png")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	srcOrig, _, err := image.Decode(f)
	if err != nil {
		t.Fatal(err)
	}
	src := image.NewRGBA(srcOrig.Bounds())
	draw.Draw(src, src.Rect, srcOrig, srcOrig.Bounds().Min, draw.Src)

	const (
		pixW = 100
		pixH = 100
		ptW  = geom.Pt(50)
		ptH  = geom.Pt(50)
	)
	sz := size.Event{
		WidthPx:     pixW,
		HeightPx:    pixH,
		WidthPt:     ptW,
		HeightPt:    ptH,
		PixelsPerPt: float32(pixW) / float32(ptW),
	}

	got := image.NewRGBA(image.Rect(0, 0, pixW, pixH))
	blue := image.NewUniform(color.RGBA{B: 0xff, A: 0xff})
	draw.Draw(got, got.Bounds(), blue, image.Point{}, draw.Src)

	b := src.Bounds()
	b.Min.X += 10
	b.Max.Y /= 2

	var a f32.Affine
	a.Identity()
	a.Scale(&a, sz.PixelsPerPt, sz.PixelsPerPt)
	a.Translate(&a, 0, 24)
	a.Rotate(&a, float32(math.Asin(12./20)))
	// See commentary in the render method defined in portable.go.
	a.Scale(&a, 40/float32(b.Dx()), 20/float32(b.Dy()))
	a.Inverse(&a)

	affine(got, src, b, nil, &a, draw.Over)

	ptTopLeft := geom.Point{0, 24}
	ptBottomRight := geom.Point{12 + 32, 16}

	drawCross(got, 0, 0)
	drawCross(got, int(ptTopLeft.X.Px(sz.PixelsPerPt)), int(ptTopLeft.Y.Px(sz.PixelsPerPt)))
	drawCross(got, int(ptBottomRight.X.Px(sz.PixelsPerPt)), int(ptBottomRight.Y.Px(sz.PixelsPerPt)))
	drawCross(got, pixW-1, pixH-1)

	const wantPath = "../../../testdata/testpattern-window.png"
	f, err = os.Open(wantPath)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	wantSrc, _, err := image.Decode(f)
	if err != nil {
		t.Fatal(err)
	}
	want, ok := wantSrc.(*image.RGBA)
	if !ok {
		b := wantSrc.Bounds()
		want = image.NewRGBA(b)
		draw.Draw(want, b, wantSrc, b.Min, draw.Src)
	}

	if !imageEq(got, want) {
		gotPath, err := writeTempPNG("testpattern-window-got", got)
		if err != nil {
			t.Fatal(err)
		}
		t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath)
	}
}

func TestAffineMask(t *testing.T) {
	if runtime.GOOS == "android" || runtime.GOOS == "ios" {
		t.Skipf("testdata not available on %s", runtime.GOOS)
	}
	f, err := os.Open("../../../testdata/testpattern.png")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	srcOrig, _, err := image.Decode(f)
	if err != nil {
		t.Fatal(err)
	}
	b := srcOrig.Bounds()
	src := image.NewRGBA(b)
	draw.Draw(src, src.Rect, srcOrig, b.Min, draw.Src)
	mask := image.NewAlpha(b)
	for y := b.Min.Y; y < b.Max.Y; y++ {
		for x := b.Min.X; x < b.Max.X; x++ {
			mask.Set(x, y, color.Alpha{A: uint8(x - b.Min.X)})
		}
	}
	want := image.NewRGBA(b)
	draw.DrawMask(want, want.Rect, src, b.Min, mask, b.Min, draw.Src)

	a := new(f32.Affine)
	a.Identity()
	got := image.NewRGBA(b)
	affine(got, src, b, mask, a, draw.Src)

	if !imageEq(got, want) {
		gotPath, err := writeTempPNG("testpattern-mask-got", got)
		if err != nil {
			t.Fatal(err)
		}
		wantPath, err := writeTempPNG("testpattern-mask-want", want)
		if err != nil {
			t.Fatal(err)
		}
		t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath)
	}
}

func writeTempPNG(prefix string, m image.Image) (string, error) {
	f, err := os.CreateTemp("", prefix+"-")
	if err != nil {
		return "", err
	}
	f.Close()
	path := f.Name() + ".png"
	f, err = os.Create(path)
	if err != nil {
		return "", err
	}
	if err := png.Encode(f, m); err != nil {
		f.Close()
		return "", err
	}
	return path, f.Close()
}

func drawCross(m *image.RGBA, x, y int) {
	c := color.RGBA{0xff, 0, 0, 0xff} // red
	m.SetRGBA(x+0, y-2, c)
	m.SetRGBA(x+0, y-1, c)
	m.SetRGBA(x-2, y+0, c)
	m.SetRGBA(x-1, y+0, c)
	m.SetRGBA(x+0, y+0, c)
	m.SetRGBA(x+1, y+0, c)
	m.SetRGBA(x+2, y+0, c)
	m.SetRGBA(x+0, y+1, c)
	m.SetRGBA(x+0, y+2, c)
}

func eqEpsilon(x, y uint8) bool {
	const epsilon = 9
	return x-y < epsilon || y-x < epsilon
}

func colorEq(c0, c1 color.RGBA) bool {
	return eqEpsilon(c0.R, c1.R) && eqEpsilon(c0.G, c1.G) && eqEpsilon(c0.B, c1.B) && eqEpsilon(c0.A, c1.A)
}

func imageEq(m0, m1 *image.RGBA) bool {
	b0 := m0.Bounds()
	b1 := m1.Bounds()
	if b0 != b1 {
		return false
	}
	badPx := 0
	for y := b0.Min.Y; y < b0.Max.Y; y++ {
		for x := b0.Min.X; x < b0.Max.X; x++ {
			c0, c1 := m0.At(x, y).(color.RGBA), m1.At(x, y).(color.RGBA)
			if !colorEq(c0, c1) {
				badPx++
			}
		}
	}
	badFrac := float64(badPx) / float64(b0.Dx()*b0.Dy())
	return badFrac < 0.01
}
