// Copyright ©2022 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 r2

import (
	"math"
	"math/rand/v2"
	"testing"
)

func TestTriangleDegenerate(t *testing.T) {
	const (
		// tol is how much closer the problematic
		// vertex is placed to avoid floating point error
		// for degeneracy calculation.
		tol = 1e-12
		// This is the argument to Degenerate and represents
		// the minimum permissible distance between the triangle
		// longest edge and the opposite vertex.
		spatialTol = 1e-2
	)
	rnd := rand.New(rand.NewPCG(1, 1))
	randVec := func() Vec {
		return Vec{X: 20 * (rnd.Float64() - 0.5), Y: 20 * (rnd.Float64() - 0.5)}
	}
	for i := 0; i < 200; i++ {
		// Generate a random line for the longest triangle side.
		ln := line{randVec(), randVec()}
		lineDir := Sub(ln[1], ln[0])

		perpendicular := Unit(Vec{X: lineDir.X, Y: -lineDir.Y})
		// generate 3 permutations of needle triangles for
		// each vertex. A needle triangle has two vertices
		// very close to eachother an its third vertex far away.
		var needle Triangle
		for j := 0; j < 3; j++ {
			needle[j] = ln[0]
			needle[(j+1)%3] = ln[1]
			needle[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, perpendicular))
			if !needle.IsDegenerate(spatialTol) {
				t.Error("needle triangle not degenerate")
			}
		}

		midpoint := ln.vecOnLine(0.5)
		// cap triangles are characterized by having two sides
		// of similar lengths and whose sum is approximately equal
		// to the remaining longest side.
		var cap Triangle
		for j := 0; j < 3; j++ {
			cap[j] = ln[0]
			cap[(j+1)%3] = ln[1]
			cap[(j+2)%3] = Add(midpoint, Scale((1-tol)*spatialTol, perpendicular))
			if !cap.IsDegenerate(spatialTol) {
				t.Error("cap triangle not degenerate")
			}
		}

		var degenerate Triangle
		for j := 0; j < 3; j++ {
			degenerate[j] = ln[0]
			degenerate[(j+1)%3] = ln[1]
			// vertex perpendicular to some random point on longest side.
			degenerate[(j+2)%3] = Add(ln.vecOnLine(rnd.Float64()), Scale((1-tol)*spatialTol, perpendicular))
			if !degenerate.IsDegenerate(spatialTol) {
				t.Error("random degenerate triangle not degenerate")
			}
			// vertex about longest side 0 vertex
			degenerate[(j+2)%3] = Add(ln[0], Scale((1-tol)*spatialTol, Unit(randVec())))
			if !degenerate.IsDegenerate(spatialTol) {
				t.Error("needle-like degenerate triangle not degenerate")
			}
			// vertex about longest side 1 vertex
			degenerate[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, Unit(randVec())))
			if !degenerate.IsDegenerate(spatialTol) {
				t.Error("needle-like degenerate triangle not degenerate")
			}
		}
	}
}

func TestTriangleArea(t *testing.T) {
	const tol = 1e-16
	for _, test := range []struct {
		T      Triangle
		Expect float64
	}{
		{
			T: Triangle{
				{0, 0},
				{1, 0},
				{0, 1},
			},
			Expect: 0.5,
		},
		{
			T: Triangle{
				{1, 0},
				{0, 1},
				{0, 0},
			},
			Expect: 0.5,
		},
		{
			T: Triangle{
				{0, 0},
				{0, 20},
				{20, 0},
			},
			Expect: 20 * 20 / 2,
		},
	} {
		got := test.T.Area()
		if math.Abs(got-test.Expect) > tol {
			t.Errorf("got area %g, expected %g", got, test.Expect)
		}
		const tol2 = 1e-11
		rnd := rand.New(rand.NewPCG(1, 1))
		for i := 0; i < 100; i++ {
			tri := Triangle{
				{rnd.Float64() * 20, rnd.Float64() * 20},
				{rnd.Float64() * 20, rnd.Float64() * 20},
				{rnd.Float64() * 20, rnd.Float64() * 20},
			}

			got := tri.Area()
			want := math.Abs(Cross(Sub(tri[1], tri[0]), Sub(tri[1], tri[2]))) / 2
			if math.Abs(got-want) > tol2 {
				t.Errorf("got area %g not match half norm of cross product %g", got, want)
			}
		}
	}
}

func TestTriangleCentroid(t *testing.T) {
	const tol = 1e-12
	rnd := rand.New(rand.NewPCG(1, 1))
	for i := 0; i < 100; i++ {
		tri := Triangle{
			{rnd.Float64() * 20, rnd.Float64() * 20},
			{rand.Float64() * 20, rnd.Float64() * 20},
			{rnd.Float64() * 20, rnd.Float64() * 20},
		}
		got := tri.Centroid()
		want := Vec{
			X: (tri[0].X + tri[1].X + tri[2].X) / 3,
			Y: (tri[0].Y + tri[1].Y + tri[2].Y) / 3,
		}
		if math.Abs(got.X-want.X) > tol || math.Abs(got.Y-want.Y) > tol {
			t.Fatalf("got %.6g, want %.6g", got, want)
		}
	}
}
