// Copyright ©2020 The Gonum 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 vggio

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

	"gioui.org/layout"
	"gioui.org/op"
	"gonum.org/v1/plot"
	"gonum.org/v1/plot/cmpimg"
	"gonum.org/v1/plot/plotter"
	"gonum.org/v1/plot/vg"
	"gonum.org/v1/plot/vg/draw"
)

const deltaGio = 0.05 // empirical value from experimentation.

// init makes sure the headless display is ready for tests with Gio.
// On GitHub Actions and on linux, that headless display may take some time to
// be properly available and appears to be setup "on demand".
// So we request it by trying to take a screenshot twice:
//   - the first time around might fail
//   - the second time shouldn't.
func init() {
	if runtime.GOOS != "linux" {
		return
	}

	const (
		w   = 20 * vg.Centimeter
		h   = 15 * vg.Centimeter
		dpi = 96
	)
	gtx := layout.Context{
		Ops: new(op.Ops),
		Constraints: layout.Exact(image.Pt(
			int(w.Dots(dpi)),
			int(h.Dots(dpi)),
		)),
	}

	var err error
	for try := 0; try < 2; try++ {
		_, err = New(gtx, w, h, UseDPI(dpi)).Screenshot()
		if err == nil {
			return
		}
	}

	panic(fmt.Errorf("vg/vggio_test: could not setup headless display: %+v", err))
}

func TestCanvas(t *testing.T) {
	if runtime.GOOS == "darwin" {
		t.Skip("TODO: github actions for darwin with headless setup.")
	}

	const fname = "testdata/func.png"

	const (
		w   = 20 * vg.Centimeter
		h   = 15 * vg.Centimeter
		dpi = 96
	)

	cmpimg.CheckPlotApprox(func() {
		p := plot.New()
		p.Title.Text = "My title"
		p.X.Label.Text = "X"
		p.Y.Label.Text = "Y"

		quad := plotter.NewFunction(func(x float64) float64 { return x * x })
		quad.Color = color.RGBA{B: 255, A: 255}

		exp := plotter.NewFunction(func(x float64) float64 { return math.Pow(2, x) })
		exp.Dashes = []vg.Length{vg.Points(2), vg.Points(2)}
		exp.Width = vg.Points(2)
		exp.Color = color.RGBA{G: 255, A: 255}

		sin := plotter.NewFunction(func(x float64) float64 { return 10*math.Sin(x) + 50 })
		sin.Dashes = []vg.Length{vg.Points(4), vg.Points(5)}
		sin.Width = vg.Points(4)
		sin.Color = color.RGBA{R: 255, A: 255}

		p.Add(quad, exp, sin)
		p.Legend.Add("x^2", quad)
		p.Legend.Add("2^x", exp)
		p.Legend.Add("10*sin(x)+50", sin)
		p.Legend.ThumbnailWidth = 0.5 * vg.Inch

		p.X.Min = 0
		p.X.Max = 10
		p.Y.Min = 0
		p.Y.Max = 100

		p.Add(plotter.NewGrid())

		gtx := layout.Context{
			Ops: new(op.Ops),
			Constraints: layout.Exact(image.Pt(
				int(w.Dots(dpi)),
				int(h.Dots(dpi)),
			)),
		}
		cnv := New(gtx, w, h, UseDPI(dpi))
		p.Draw(draw.New(cnv))

		img, err := cnv.Screenshot()
		if err != nil {
			t.Fatalf("could not create screenshot: %+v", err)
		}
		f, err := os.Create(fname)
		if err != nil {
			t.Fatalf("could not create output file: %+v", err)
		}
		defer f.Close()

		err = png.Encode(f, img)
		if err != nil {
			t.Fatalf("could not encode screenshot: %+v", err)
		}

		err = f.Close()
		if err != nil {
			t.Fatalf("could not save screenshot: %+v", err)
		}
	}, t, deltaGio, "func.png",
	)
}

func TestCollectionName(t *testing.T) {
	for _, tc := range []struct {
		name string
		want string
	}{
		{"Liberation", "Liberation"},
		{"LiberationSerif-Bold", "LiberationSerif"},
		{"LiberationSerif-BoldItalic", "LiberationSerif"},
		{"LiberationSerif-BoldItalic-Extra", "LiberationSerif"},

		{"LiberationMono", "LiberationMono"},
		{"LiberationMono-Regular", "LiberationMono"},

		{"Times-Roman", "Times"},
		{"Times-Bold", "Times"},
	} {
		got := collectionName(tc.name)
		if got != tc.want {
			t.Errorf(
				"%s: invalid collection name: got=%q, want=%q",
				tc.name, got, tc.want,
			)
		}
	}
}

func TestLabels(t *testing.T) {
	if runtime.GOOS == "darwin" {
		t.Skip("TODO: github actions for darwin with headless setup.")
	}

	const fname = "testdata/labels.png"

	const (
		w   = 20 * vg.Centimeter
		h   = 15 * vg.Centimeter
		dpi = 96
	)

	cmpimg.CheckPlotApprox(func() {
		p := plot.New()
		p.Title.Text = "Labels"
		p.X.Min = -1
		p.X.Max = +1
		p.Y.Min = -1
		p.Y.Max = +1

		const (
			left   = 0.00
			middle = 0.02
			right  = 0.04
		)

		labels, err := plotter.NewLabels(plotter.XYLabels{
			XYs: []plotter.XY{
				{X: -0.8 + left, Y: -0.5},   // Aq + y-align bottom
				{X: -0.6 + middle, Y: -0.5}, // Aq + y-align center
				{X: -0.4 + right, Y: -0.5},  // Aq + y-align top

				{X: -0.8 + left, Y: +0.5}, // ditto for Aq\nAq
				{X: -0.6 + middle, Y: +0.5},
				{X: -0.4 + right, Y: +0.5},

				{X: +0.0 + left, Y: +0}, // ditto for Bg\nBg\nBg
				{X: +0.2 + middle, Y: +0},
				{X: +0.4 + right, Y: +0},
			},
			Labels: []string{
				"Aq", "Aq", "Aq",
				"Aq\nAq", "Aq\nAq", "Aq\nAq",

				"Bg\nBg\nBg",
				"Bg\nBg\nBg",
				"Bg\nBg\nBg",
			},
		})
		if err != nil {
			t.Fatalf("could not creates labels plotter: %+v", err)
		}
		for i := range labels.TextStyle {
			sty := &labels.TextStyle[i]
			sty.Font.Size = vg.Length(34)
		}
		labels.TextStyle[0].YAlign = draw.YBottom
		labels.TextStyle[1].YAlign = draw.YCenter
		labels.TextStyle[2].YAlign = draw.YTop

		labels.TextStyle[3].YAlign = draw.YBottom
		labels.TextStyle[4].YAlign = draw.YCenter
		labels.TextStyle[5].YAlign = draw.YTop

		labels.TextStyle[6].YAlign = draw.YBottom
		labels.TextStyle[7].YAlign = draw.YCenter
		labels.TextStyle[8].YAlign = draw.YTop

		lred, err := plotter.NewLabels(plotter.XYLabels{
			XYs: []plotter.XY{
				{X: -0.8 + left, Y: +0.5},
				{X: +0.0 + left, Y: +0},
			},
			Labels: []string{
				"Aq", "Bg",
			},
		})
		if err != nil {
			t.Fatalf("could not creates labels plotter: %+v", err)
		}
		for i := range lred.TextStyle {
			sty := &lred.TextStyle[i]
			sty.Font.Size = vg.Length(34)
			sty.Color = color.RGBA{R: 255, A: 255}
			sty.YAlign = draw.YBottom
		}

		m5 := plotter.NewFunction(func(float64) float64 { return -0.5 })
		m5.LineStyle.Color = color.RGBA{R: 255, A: 255}

		l0 := plotter.NewFunction(func(float64) float64 { return 0 })
		l0.LineStyle.Color = color.RGBA{G: 255, A: 255}

		p5 := plotter.NewFunction(func(float64) float64 { return +0.5 })
		p5.LineStyle.Color = color.RGBA{B: 255, A: 255}

		p.Add(labels, lred, m5, l0, p5)
		p.Add(plotter.NewGrid())
		p.Add(plotter.NewGlyphBoxes())

		gtx := layout.Context{
			Ops: new(op.Ops),
			Constraints: layout.Exact(image.Pt(
				int(w.Dots(dpi)),
				int(h.Dots(dpi)),
			)),
		}
		cnv := New(gtx, w, h, UseDPI(dpi))
		p.Draw(draw.New(cnv))

		img, err := cnv.Screenshot()
		if err != nil {
			t.Fatalf("could not create screenshot: %+v", err)
		}
		f, err := os.Create(fname)
		if err != nil {
			t.Fatalf("could not create output file: %+v", err)
		}
		defer f.Close()

		err = png.Encode(f, img)
		if err != nil {
			t.Fatalf("could not encode screenshot: %+v", err)
		}

		err = f.Close()
		if err != nil {
			t.Fatalf("could not save screenshot: %+v", err)
		}
	}, t, deltaGio, "labels.png",
	)
}

func TestPaths(t *testing.T) {
	if runtime.GOOS == "darwin" {
		t.Skip("TODO: github actions for darwin with headless setup.")
	}

	const fname = "testdata/paths.png"

	const (
		w   = 20 * vg.Centimeter
		h   = 15 * vg.Centimeter
		dpi = 96
	)

	cmpimg.CheckPlotApprox(func() {
		p := plot.New()
		p.Title.Text = "Paths"
		p.X.Min = -1
		p.X.Max = +1
		p.Y.Min = -1
		p.Y.Max = +1

		newScatter := func(c color.Color, sty draw.GlyphDrawer, x, y float64) *plotter.Scatter {
			t.Helper()

			pts := make(plotter.XYs, 1)
			pts[0].X = x
			pts[0].Y = y

			plt, err := plotter.NewScatter(pts)
			if err != nil {
				t.Fatal(err)
			}
			plt.GlyphStyle.Color = c
			plt.GlyphStyle.Radius = vg.Points(10)
			plt.GlyphStyle.Shape = sty
			return plt
		}

		p.Add(
			newScatter(
				color.RGBA{R: 255, A: 255},
				draw.CircleGlyph{},
				-0.8, -0.8,
			),
			newScatter(
				color.RGBA{B: 255, A: 255},
				draw.RingGlyph{},
				-0.6, -0.6,
			),
			newScatter(
				color.RGBA{R: 255, A: 255},
				draw.SquareGlyph{},
				-0.4, -0.4,
			),
			newScatter(
				color.RGBA{B: 255, A: 255},
				draw.BoxGlyph{},
				-0.2, -0.2,
			),
			newScatter(
				color.RGBA{R: 255, A: 255},
				draw.TriangleGlyph{},
				0, 0,
			),
			newScatter(
				color.RGBA{B: 255, A: 255},
				draw.PyramidGlyph{},
				0.2, 0.2,
			),
			newScatter(
				color.RGBA{R: 255, A: 255},
				draw.PlusGlyph{},
				0.4, 0.4,
			),
			newScatter(
				color.RGBA{B: 255, A: 255},
				draw.CrossGlyph{},
				0.6, 0.6,
			),
		)

		p.Add(plotter.NewGrid())
		p.Add(plotter.NewGlyphBoxes())

		gtx := layout.Context{
			Ops: new(op.Ops),
			Constraints: layout.Exact(image.Pt(
				int(w.Dots(dpi)),
				int(h.Dots(dpi)),
			)),
		}
		cnv := New(gtx, w, h, UseDPI(dpi), UseBackgroundColor(color.Transparent))
		p.Draw(draw.New(cnv))

		img, err := cnv.Screenshot()
		if err != nil {
			t.Fatalf("could not create screenshot: %+v", err)
		}
		f, err := os.Create(fname)
		if err != nil {
			t.Fatalf("could not create output file: %+v", err)
		}
		defer f.Close()

		err = png.Encode(f, img)
		if err != nil {
			t.Fatalf("could not encode screenshot: %+v", err)
		}

		err = f.Close()
		if err != nil {
			t.Fatalf("could not save screenshot: %+v", err)
		}
	}, t, deltaGio, "paths.png",
	)
}

// An example of embedding an image in a plot.
func TestImage(t *testing.T) {
	if runtime.GOOS == "darwin" {
		t.Skip("TODO: github actions for darwin with headless setup.")
	}

	const fname = "testdata/image.png"

	const (
		w   = 20 * vg.Centimeter
		h   = 15 * vg.Centimeter
		dpi = 96
	)

	cmpimg.CheckPlotApprox(func() {
		p := plot.New()
		p.Title.Text = "A Logo"

		// load an image
		src, err := os.Open("../../plotter/testdata/gopher.png")
		if err != nil {
			t.Fatalf("error opening image file: %v\n", err)
		}
		defer src.Close()

		img, err := png.Decode(src)
		if err != nil {
			t.Fatalf("error decoding image file: %v\n", err)
		}

		p.Add(plotter.NewImage(img, 100, 100, 200, 200))

		gtx := layout.Context{
			Ops: new(op.Ops),
			Constraints: layout.Exact(image.Pt(
				int(w.Dots(dpi)),
				int(h.Dots(dpi)),
			)),
		}
		cnv := New(gtx, w, h, UseDPI(dpi), UseBackgroundColor(color.Transparent))
		p.Draw(draw.New(cnv))

		scr, err := cnv.Screenshot()
		if err != nil {
			t.Fatalf("could not create screenshot: %+v", err)
		}

		out, err := os.Create(fname)
		if err != nil {
			t.Fatalf("could not create output file: %+v", err)
		}
		defer out.Close()

		err = png.Encode(out, scr)
		if err != nil {
			t.Fatalf("could not encode screenshot: %+v", err)
		}

		err = out.Close()
		if err != nil {
			t.Fatalf("could not save screenshot: %+v", err)
		}
	}, t, deltaGio, "image.png",
	)
}
