// 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"
	"testing"

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

func TestCentroidsPlanarCentroid(t *testing.T) {
	tests := []struct {
		name             string
		p0, p1, p2, want Point
	}{
		{
			name: "xyz axis",
			p0:   Point{r3.Vector{X: 0, Y: 0, Z: 1}},
			p1:   Point{r3.Vector{X: 0, Y: 1, Z: 0}},
			p2:   Point{r3.Vector{X: 1, Y: 0, Z: 0}},
			want: Point{r3.Vector{X: 1. / 3, Y: 1. / 3, Z: 1. / 3}},
		},
		{
			name: "Same point",
			p0:   Point{r3.Vector{X: 1, Y: 0, Z: 0}},
			p1:   Point{r3.Vector{X: 1, Y: 0, Z: 0}},
			p2:   Point{r3.Vector{X: 1, Y: 0, Z: 0}},
			want: Point{r3.Vector{X: 1, Y: 0, Z: 0}},
		},
	}

	for _, test := range tests {
		got := PlanarCentroid(test.p0, test.p1, test.p2)
		if !got.ApproxEqual(test.want) {
			t.Errorf("%s: PlanarCentroid(%v, %v, %v) = %v, want %v", test.name, test.p0, test.p1, test.p2, got, test.want)
		}
	}
}

func TestCentroidsTrueCentroid(t *testing.T) {
	// Test TrueCentroid with very small triangles. This test assumes that
	// the triangle is small enough so that it is nearly planar.
	for i := 0; i < 100; i++ {
		f := randomFrame()
		p := f.col(0)
		x := f.col(1)
		y := f.col(2)
		d := 1e-4 * math.Pow(1e-4, randomFloat64())

		// Make a triangle with two equal sides.
		p0 := Point{p.Sub(x.Mul(d)).Normalize()}
		p1 := Point{p.Add(x.Mul(d)).Normalize()}
		p2 := Point{p.Add(y.Mul(d * 3)).Normalize()}
		want := Point{p.Add(y.Mul(d)).Normalize()}

		// The centroid of a planar triangle is at the intersection of its
		// medians, which is two-thirds of the way along each median.
		got := TrueCentroid(p0, p1, p2).Normalize()
		if got.Distance(want.Vector) >= 2e-8 {
			t.Errorf("TrueCentroid(%v, %v, %v).Normalize() = %v, want %v", p0, p1, p2, got, want)
		}

		// Make a triangle with a right angle.
		p0 = p
		p1 = Point{p.Add(x.Mul(d * 3)).Normalize()}
		p2 = Point{p.Add(y.Mul(d * 6)).Normalize()}
		want = Point{p.Add(x.Add(y.Mul(2)).Mul(d)).Normalize()}

		got = TrueCentroid(p0, p1, p2).Normalize()
		if got.Distance(want.Vector) >= 2e-8 {
			t.Errorf("TrueCentroid(%v, %v, %v).Normalize() = %v, want %v", p0, p1, p2, got, want)
		}
	}
}

func TestCentroidsEdgeTrueCentroidSemiCircles(t *testing.T) {
	// Test the centroid of polyline ABC that follows the equator and consists
	// of two 90 degree edges (i.e., C = -A).  The centroid (multiplied by
	// length) should point toward B and have a norm of 2.0.  (The centroid
	// itself has a norm of 2/Pi, and the total edge length is Pi.)
	a := PointFromCoords(0, -1, 0)
	b := PointFromCoords(1, 0, 0)
	c := PointFromCoords(0, 1, 0)
	centroid := Point{EdgeTrueCentroid(a, b).Add(EdgeTrueCentroid(b, c).Vector)}

	if !b.ApproxEqual(Point{centroid.Normalize()}) {
		t.Errorf("EdgeTrueCentroid(%v, %v) + EdgeTrueCentroid(%v, %v) = %v, want %v", a, b, b, c, centroid, b)
	}
	if got, want := centroid.Norm(), 2.0; !float64Eq(got, want) {
		t.Errorf("%v.Norm() = %v, want %v", centroid, got, want)
	}
}

func TestCentroidsEdgeTrueCentroidGreatCircles(t *testing.T) {
	// Construct random great circles and divide them randomly into segments.
	// Then make sure that the centroid is approximately at the center of the
	// sphere.  Note that because of the way the centroid is computed, it does
	// not matter how we split the great circle into segments.
	//
	// Note that this is a direct test of the properties that the centroid
	// should have, rather than a test that it matches a particular formula.
	for iter := 0; iter < 100; iter++ {
		f := randomFrameAtPoint(randomPoint())
		x := f.col(0)
		y := f.col(1)

		var centroid Point

		v0 := x
		for theta := 0.0; theta < 2*math.Pi; theta += math.Pow(randomFloat64(), 10) {
			v1 := Point{x.Mul(math.Cos(theta)).Add(y.Mul(math.Sin(theta)))}
			centroid = Point{centroid.Add(EdgeTrueCentroid(v0, v1).Vector)}
			v0 = v1
		}
		// Close the circle.
		centroid = Point{centroid.Add(EdgeTrueCentroid(v0, x).Vector)}
		if got, want := centroid.Norm(), 2e-14; got > want {
			t.Errorf("%v.Norm() = %v, want <= %v", centroid, got, want)
		}
	}
}
