// Copyright 2018 Google Inc. All rights reserved.
//
// 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 s2

import (
	"math"
	"slices"
	"testing"

	"github.com/golang/geo/s1"
)

func TestConvexHullQueryNoPoints(t *testing.T) {
	query := NewConvexHullQuery()
	result := query.ConvexHull()
	if !result.IsEmpty() {
		t.Errorf("ConvexHullQuery with no geometry should return an empty hull")
	}
}

func TestConvexHullQueryOnePoint(t *testing.T) {
	query := NewConvexHullQuery()
	p := PointFromCoords(0, 0, 1)
	query.AddPoint(p)
	result := query.ConvexHull()
	if got, want := len(result.vertices), 3; got != want {
		t.Errorf("len(query.ConvexHull()) = %d, want %d", got, want)
	}
	if !result.IsNormalized() {
		t.Errorf("ConvexHull should be normalized but wasn't")
	}

	if !loopHasVertex(result, p) {
		t.Errorf("ConvexHull doesn't have vertex %v, but should", p)
	}

	// Add some duplicate points and check that the result is the same.
	query.AddPoint(p)
	query.AddPoint(p)
	result2 := query.ConvexHull()
	if !result2.Equal(result) {
		t.Errorf("adding duplicate points to the ConvexHull should not change the result.")
	}
}

func TestConvexHullQueryTwoPoints(t *testing.T) {
	query := NewConvexHullQuery()
	p := PointFromCoords(0, 0, 1)
	q := PointFromCoords(0, 1, 0)
	query.AddPoint(p)
	query.AddPoint(q)
	result := query.ConvexHull()
	if got, want := len(result.vertices), 3; got != want {
		t.Errorf("len(query.ConvexHull()) = %d, want %d", got, want)
	}
	if !result.IsNormalized() {
		t.Errorf("ConvexHull should be normalized but wasn't")
	}

	if !loopHasVertex(result, p) {
		t.Errorf("ConvexHull doesn't have vertex %v, but should", p)
	}
	if !loopHasVertex(result, q) {
		t.Errorf("ConvexHull doesn't have vertex %v, but should", q)
	}
	// Add some duplicate points and check that the result is the same.
	query.AddPoint(q)
	query.AddPoint(p)
	query.AddPoint(p)
	result2 := query.ConvexHull()
	if !result2.Equal(result) {
		t.Errorf("adding duplicate points to the ConvexHull should not change the result.")
	}
}

func TestConvexHullAntipodalPoints(t *testing.T) {
	query := NewConvexHullQuery()
	query.AddPoint(PointFromCoords(0, 0, 1))
	query.AddPoint(PointFromCoords(0, 0, -1))
	result := query.ConvexHull()
	if !result.IsFull() {
		t.Errorf("antipodal points should return a Full Polygon, got: %v", result)
	}
}

func loopHasVertex(l *Loop, p Point) bool {
	return slices.Contains(l.vertices, p)
}

func TestConvexHullQueryEmptyLoop(t *testing.T) {
	query := NewConvexHullQuery()
	query.AddLoop(EmptyLoop())
	result := query.ConvexHull()
	if !result.IsEmpty() {
		t.Errorf("ConvexHull of Empty Loop should be the Empty Loop")
	}
}

func TestConvexHullQueryFullLoop(t *testing.T) {
	query := NewConvexHullQuery()
	query.AddLoop(FullLoop())
	result := query.ConvexHull()
	if !result.IsFull() {
		t.Errorf("ConvexHull of Full Loop should be the Full Loop")
	}
}

func TestConvexHullQueryEmptyPolygon(t *testing.T) {
	query := NewConvexHullQuery()
	query.AddPolygon(PolygonFromLoops([]*Loop{}))
	result := query.ConvexHull()
	if !result.IsEmpty() {
		t.Errorf("ConvexHull of an empty Polygon should be the Empty Loop")
	}
}

func TestConvexHullQueryNonConvexPoints(t *testing.T) {
	// Generate a point set such that the only convex region containing them is
	// the entire sphere. In other words, you can generate any point on the
	// sphere by repeatedly linearly interpolating between the points. (The
	// four points of a tetrahedron would also work, but this is easier.)
	query := NewConvexHullQuery()
	for face := 0; face < 6; face++ {
		query.AddPoint(CellIDFromFace(face).Point())
	}
	result := query.ConvexHull()
	if !result.IsFull() {
		t.Errorf("ConvexHull of all faces should be the Full Loop, got %v", result)
	}
}

func TestConvexHullQuerySimplePolyline(t *testing.T) {
	// A polyline is handled identically to a point set, so there is no need
	// for special testing other than code coverage.
	polyline := makePolyline("0:1, 0:9, 1:6, 2:6, 3:10, 4:10, 5:5, 4:0, 3:0, 2:5, 1:5")
	query := NewConvexHullQuery()
	query.AddPolyline(polyline)
	result := query.ConvexHull()
	want := makeLoop("0:1, 0:9, 3:10, 4:10, 5:5, 4:0, 3:0")
	if !result.BoundaryEqual(want) {
		t.Errorf("ConvexHull from %v = %v, want %v", polyline, result, want)
	}
}

func TestConvexHullQueryLoopsAroundNorthPole(t *testing.T) {
	tests := []struct {
		radius   s1.Angle
		numVerts int
	}{
		// Test loops of various sizes around the north pole.
		{radius: 1 * s1.Degree, numVerts: 3},
		{radius: 89 * s1.Degree, numVerts: 3},
		// The following two loops should yield the full loop.
		{radius: 91 * s1.Degree, numVerts: 3},
		{radius: 179 * s1.Degree, numVerts: 3},

		{radius: 10 * s1.Degree, numVerts: 100},
		{radius: 89 * s1.Degree, numVerts: 1000},
	}

	for _, test := range tests {
		query := NewConvexHullQuery()
		loop := RegularLoop(PointFromCoords(0, 0, 1), test.radius, test.numVerts)
		query.AddLoop(loop)
		result := query.ConvexHull()

		if test.radius > s1.Angle(math.Pi/2) {
			if !result.IsFull() {
				t.Errorf("ConvexHull of a Loop with radius > 90 should be the Full Loop")
			}
		} else {
			if !result.BoundaryEqual(loop) {
				t.Errorf("ConvexHull of a north pole loop = %v, want %v", result, loop)
			}
		}
	}
}

func TestConvexHullQueryPointsInsideHull(t *testing.T) {
	// Repeatedly build the convex hull of a set of points, then add more points
	// inside that loop and build the convex hull again. The result should
	// always be the same.
	const iters = 1000
	for iter := 0; iter < iters; iter++ {
		// Choose points from within a cap of random size, up to but not including
		// an entire hemisphere.
		c := randomCap(1e-15, 1.999*math.Pi)
		numPoints1 := randomUniformInt(100) + 3

		query := NewConvexHullQuery()
		for i := 0; i < numPoints1; i++ {
			query.AddPoint(samplePointFromCap(c))
		}
		hull := query.ConvexHull()

		// When the convex hull is nearly a hemisphere, the algorithm sometimes
		// returns a full cap instead. This is because it first computes a
		// bounding rectangle for all the input points/edges and then converts it
		// to a bounding cap, which sometimes yields a non-convex cap (radius
		// larger than 90 degrees). This should not be a problem in practice
		// (since most convex hulls are not hemispheres), but in order make this
		// test pass reliably it means that we need to reject convex hulls whose
		// bounding cap (when computed from a bounding rectangle) is not convex.
		//
		// TODO(rsned): This test can still fail (about 1 iteration in 500,000)
		// because the Rect.CapBound implementation does not guarantee
		// that A.Contains(B) implies A.CapBound().Contains(B.CapBound()).
		if hull.CapBound().Height() >= 1 {
			continue
		}

		// Otherwise, add more points inside the convex hull.
		const numPoints2 = 1000
		for i := 0; i < numPoints2; i++ {
			p := samplePointFromCap(c)
			if hull.ContainsPoint(p) {
				query.AddPoint(p)
			}
		}
		// Finally, build a new convex hull and check that it hasn't changed.
		hull2 := query.ConvexHull()
		if !hull2.BoundaryEqual(hull) {
			t.Errorf("%v.BoundaryEqual(%v) = false, but should be true", hull2, hull)
		}
	}
}
