// Copyright ©2015 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 mat

import (
	"fmt"
	"os"
	"reflect"
	"testing"

	"golang.org/x/exp/rand"

	"gonum.org/v1/gonum/blas"
	"gonum.org/v1/gonum/blas/blas64"
	"gonum.org/v1/gonum/floats/scalar"
)

func TestNewSymmetric(t *testing.T) {
	t.Parallel()
	for i, test := range []struct {
		data []float64
		n    int
		mat  *SymDense
	}{
		{
			data: []float64{
				1, 2, 3,
				4, 5, 6,
				7, 8, 9,
			},
			n: 3,
			mat: &SymDense{
				mat: blas64.Symmetric{
					N:      3,
					Stride: 3,
					Uplo:   blas.Upper,
					Data:   []float64{1, 2, 3, 4, 5, 6, 7, 8, 9},
				},
				cap: 3,
			},
		},
	} {
		sym := NewSymDense(test.n, test.data)
		rows, cols := sym.Dims()

		if rows != test.n {
			t.Errorf("unexpected number of rows for test %d: got: %d want: %d", i, rows, test.n)
		}
		if cols != test.n {
			t.Errorf("unexpected number of cols for test %d: got: %d want: %d", i, cols, test.n)
		}
		if !reflect.DeepEqual(sym, test.mat) {
			t.Errorf("unexpected data slice for test %d: got: %v want: %v", i, sym, test.mat)
		}

		m := NewDense(test.n, test.n, test.data)
		if !reflect.DeepEqual(sym.mat.Data, m.mat.Data) {
			t.Errorf("unexpected data slice mismatch for test %d: got: %v want: %v", i, sym.mat.Data, m.mat.Data)
		}
	}

	panicked, message := panics(func() { NewSymDense(3, []float64{1, 2}) })
	if !panicked || message != ErrShape.Error() {
		t.Error("expected panic for invalid data slice length")
	}
}

func TestSymAtSet(t *testing.T) {
	t.Parallel()
	sym := &SymDense{
		mat: blas64.Symmetric{
			N:      3,
			Stride: 3,
			Uplo:   blas.Upper,
			Data:   []float64{1, 2, 3, 4, 5, 6, 7, 8, 9},
		},
		cap: 3,
	}
	rows, cols := sym.Dims()

	// Check At out of bounds
	for _, row := range []int{-1, rows, rows + 1} {
		panicked, message := panics(func() { sym.At(row, 0) })
		if !panicked || message != ErrRowAccess.Error() {
			t.Errorf("expected panic for invalid row access N=%d r=%d", rows, row)
		}
	}
	for _, col := range []int{-1, cols, cols + 1} {
		panicked, message := panics(func() { sym.At(0, col) })
		if !panicked || message != ErrColAccess.Error() {
			t.Errorf("expected panic for invalid column access N=%d c=%d", cols, col)
		}
	}

	// Check Set out of bounds
	for _, row := range []int{-1, rows, rows + 1} {
		panicked, message := panics(func() { sym.SetSym(row, 0, 1.2) })
		if !panicked || message != ErrRowAccess.Error() {
			t.Errorf("expected panic for invalid row access N=%d r=%d", rows, row)
		}
	}
	for _, col := range []int{-1, cols, cols + 1} {
		panicked, message := panics(func() { sym.SetSym(0, col, 1.2) })
		if !panicked || message != ErrColAccess.Error() {
			t.Errorf("expected panic for invalid column access N=%d c=%d", cols, col)
		}
	}

	for _, st := range []struct {
		row, col  int
		orig, new float64
	}{
		{row: 1, col: 2, orig: 6, new: 15},
		{row: 2, col: 1, orig: 15, new: 12},
	} {
		if e := sym.At(st.row, st.col); e != st.orig {
			t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", st.row, st.col, e, st.orig)
		}
		if e := sym.At(st.col, st.row); e != st.orig {
			t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", st.col, st.row, e, st.orig)
		}
		sym.SetSym(st.row, st.col, st.new)
		if e := sym.At(st.row, st.col); e != st.new {
			t.Errorf("unexpected value for At(%d, %d) after SetSym(%[1]d, %[2]d, %[4]v): got: %[3]v want: %v", st.row, st.col, e, st.new)
		}
		if e := sym.At(st.col, st.row); e != st.new {
			t.Errorf("unexpected value for At(%d, %d) after SetSym(%[2]d, %[1]d, %[4]v): got: %[3]v want: %v", st.col, st.row, e, st.new)
		}
	}
}

func TestSymDenseZero(t *testing.T) {
	t.Parallel()
	// Elements that equal 1 should be set to zero, elements that equal -1
	// should remain unchanged.
	for _, test := range []*SymDense{
		{
			mat: blas64.Symmetric{
				Uplo:   blas.Upper,
				N:      4,
				Stride: 5,
				Data: []float64{
					1, 1, 1, 1, -1,
					-1, 1, 1, 1, -1,
					-1, -1, 1, 1, -1,
					-1, -1, -1, 1, -1,
				},
			},
		},
	} {
		dataCopy := make([]float64, len(test.mat.Data))
		copy(dataCopy, test.mat.Data)
		test.Zero()
		for i, v := range test.mat.Data {
			if dataCopy[i] != -1 && v != 0 {
				t.Errorf("Matrix not zeroed in bounds")
			}
			if dataCopy[i] == -1 && v != -1 {
				t.Errorf("Matrix zeroed out of bounds")
			}
		}
	}
}

func TestSymDiagView(t *testing.T) {
	t.Parallel()
	for cas, test := range []*SymDense{
		NewSymDense(1, []float64{1}),
		NewSymDense(2, []float64{1, 2, 2, 3}),
		NewSymDense(3, []float64{1, 2, 3, 2, 4, 5, 3, 5, 6}),
	} {
		testDiagView(t, cas, test)
	}
}

func TestSymAdd(t *testing.T) {
	t.Parallel()
	rnd := rand.New(rand.NewSource(1))
	for _, test := range []struct {
		n int
	}{
		{n: 1},
		{n: 2},
		{n: 3},
		{n: 4},
		{n: 5},
		{n: 10},
	} {
		n := test.n
		a := NewSymDense(n, nil)
		for i := range a.mat.Data {
			a.mat.Data[i] = rnd.Float64()
		}
		b := NewSymDense(n, nil)
		for i := range a.mat.Data {
			b.mat.Data[i] = rnd.Float64()
		}
		var m Dense
		m.Add(a, b)

		// Check with new receiver
		var s SymDense
		s.AddSym(a, b)
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				want := m.At(i, j)
				if got := s.At(i, j); got != want {
					t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want)
				}
			}
		}

		// Check with equal receiver
		s.CopySym(a)
		s.AddSym(&s, b)
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				want := m.At(i, j)
				if got := s.At(i, j); got != want {
					t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want)
				}
			}
		}
	}

	method := func(receiver, a, b Matrix) {
		type addSymer interface {
			AddSym(a, b Symmetric)
		}
		rd := receiver.(addSymer)
		rd.AddSym(a.(Symmetric), b.(Symmetric))
	}
	denseComparison := func(receiver, a, b *Dense) {
		receiver.Add(a, b)
	}
	testTwoInput(t, "AddSym", &SymDense{}, method, denseComparison, legalTypesSym, legalSizeSameSquare, 1e-14)
}

func TestCopy(t *testing.T) {
	t.Parallel()
	rnd := rand.New(rand.NewSource(1))
	for _, test := range []struct {
		n int
	}{
		{n: 1},
		{n: 2},
		{n: 3},
		{n: 4},
		{n: 5},
		{n: 10},
	} {
		n := test.n
		a := NewSymDense(n, nil)
		for i := range a.mat.Data {
			a.mat.Data[i] = rnd.Float64()
		}
		s := NewSymDense(n, nil)
		s.CopySym(a)
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				want := a.At(i, j)
				if got := s.At(i, j); got != want {
					t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want)
				}
			}
		}
	}
}

// TODO(kortschak) Roll this into testOneInput when it exists.
// https://github.com/gonum/matrix/issues/171
func TestSymCopyPanic(t *testing.T) {
	t.Parallel()
	var (
		a SymDense
		n int
	)
	m := NewSymDense(1, nil)
	panicked, message := panics(func() { n = m.CopySym(&a) })
	if panicked {
		t.Errorf("unexpected panic: %v", message)
	}
	if n != 0 {
		t.Errorf("unexpected n: got: %d want: 0", n)
	}
}

func TestSymRankOne(t *testing.T) {
	t.Parallel()
	rnd := rand.New(rand.NewSource(1))
	const tol = 1e-15

	for _, test := range []struct {
		n int
	}{
		{n: 1},
		{n: 2},
		{n: 3},
		{n: 4},
		{n: 5},
		{n: 10},
	} {
		n := test.n
		alpha := 2.0
		a := NewSymDense(n, nil)
		for i := range a.mat.Data {
			a.mat.Data[i] = rnd.Float64()
		}
		x := make([]float64, n)
		for i := range x {
			x[i] = rnd.Float64()
		}

		xMat := NewDense(n, 1, x)
		var m Dense
		m.Mul(xMat, xMat.T())
		m.Scale(alpha, &m)
		m.Add(&m, a)

		// Check with new receiver
		s := NewSymDense(n, nil)
		s.SymRankOne(a, alpha, NewVecDense(len(x), x))
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				want := m.At(i, j)
				if got := s.At(i, j); !scalar.EqualWithinAbsOrRel(got, want, tol, tol) {
					t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want)
				}
			}
		}

		// Check with reused receiver
		copy(s.mat.Data, a.mat.Data)
		s.SymRankOne(s, alpha, NewVecDense(len(x), x))
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				want := m.At(i, j)
				if got := s.At(i, j); !scalar.EqualWithinAbsOrRel(got, want, tol, tol) {
					t.Errorf("unexpected value for At(%d, %d): got: %v want: %v", i, j, got, want)
				}
			}
		}
	}

	alpha := 3.0
	method := func(receiver, a, b Matrix) {
		type SymRankOner interface {
			SymRankOne(a Symmetric, alpha float64, x Vector)
		}
		rd := receiver.(SymRankOner)
		rd.SymRankOne(a.(Symmetric), alpha, b.(Vector))
	}
	denseComparison := func(receiver, a, b *Dense) {
		var tmp Dense
		tmp.Mul(b, b.T())
		tmp.Scale(alpha, &tmp)
		receiver.Add(a, &tmp)
	}
	legalTypes := func(a, b Matrix) bool {
		_, ok := a.(Symmetric)
		if !ok {
			return false
		}
		_, ok = b.(Vector)
		return ok
	}
	legalSize := func(ar, ac, br, bc int) bool {
		if ar != ac {
			return false
		}
		return br == ar
	}
	testTwoInput(t, "SymRankOne", &SymDense{}, method, denseComparison, legalTypes, legalSize, 1e-14)
}

func TestIssue250SymRankOne(t *testing.T) {
	t.Parallel()
	x := NewVecDense(5, []float64{1, 2, 3, 4, 5})
	var s1, s2 SymDense
	s1.SymRankOne(NewSymDense(5, nil), 1, x)
	s2.SymRankOne(NewSymDense(5, nil), 1, x)
	s2.SymRankOne(NewSymDense(5, nil), 1, x)
	if !Equal(&s1, &s2) {
		t.Error("unexpected result from repeat")
	}
}

func TestRankTwo(t *testing.T) {
	t.Parallel()
	rnd := rand.New(rand.NewSource(1))
	for _, test := range []struct {
		n int
	}{
		{n: 1},
		{n: 2},
		{n: 3},
		{n: 4},
		{n: 5},
		{n: 10},
	} {
		n := test.n
		alpha := 2.0
		a := NewSymDense(n, nil)
		for i := range a.mat.Data {
			a.mat.Data[i] = rnd.Float64()
		}
		x := make([]float64, n)
		y := make([]float64, n)
		for i := range x {
			x[i] = rnd.Float64()
			y[i] = rnd.Float64()
		}

		xMat := NewDense(n, 1, x)
		yMat := NewDense(n, 1, y)
		var m Dense
		m.Mul(xMat, yMat.T())
		var tmp Dense
		tmp.Mul(yMat, xMat.T())
		m.Add(&m, &tmp)
		m.Scale(alpha, &m)
		m.Add(&m, a)

		// Check with new receiver
		s := NewSymDense(n, nil)
		s.RankTwo(a, alpha, NewVecDense(len(x), x), NewVecDense(len(y), y))
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				if !scalar.EqualWithinAbsOrRel(s.At(i, j), m.At(i, j), 1e-14, 1e-14) {
					t.Errorf("unexpected element value at (%d,%d): got: %f want: %f", i, j, m.At(i, j), s.At(i, j))
				}
			}
		}

		// Check with reused receiver
		copy(s.mat.Data, a.mat.Data)
		s.RankTwo(s, alpha, NewVecDense(len(x), x), NewVecDense(len(y), y))
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				if !scalar.EqualWithinAbsOrRel(s.At(i, j), m.At(i, j), 1e-14, 1e-14) {
					t.Errorf("unexpected element value at (%d,%d): got: %f want: %f", i, j, m.At(i, j), s.At(i, j))
				}
			}
		}
	}
}

func TestSymRankK(t *testing.T) {
	t.Parallel()
	alpha := 3.0
	method := func(receiver, a, b Matrix) {
		type SymRankKer interface {
			SymRankK(a Symmetric, alpha float64, x Matrix)
		}
		rd := receiver.(SymRankKer)
		rd.SymRankK(a.(Symmetric), alpha, b)
	}
	denseComparison := func(receiver, a, b *Dense) {
		var tmp Dense
		tmp.Mul(b, b.T())
		tmp.Scale(alpha, &tmp)
		receiver.Add(a, &tmp)
	}
	legalTypes := func(a, b Matrix) bool {
		_, ok := a.(Symmetric)
		return ok
	}
	legalSize := func(ar, ac, br, bc int) bool {
		if ar != ac {
			return false
		}
		return br == ar
	}
	testTwoInput(t, "SymRankK", &SymDense{}, method, denseComparison, legalTypes, legalSize, 1e-14)
}

func TestSymOuterK(t *testing.T) {
	t.Parallel()
	for _, f := range []float64{0.5, 1, 3} {
		method := func(receiver, x Matrix) {
			type SymOuterKer interface {
				SymOuterK(alpha float64, x Matrix)
			}
			rd := receiver.(SymOuterKer)
			rd.SymOuterK(f, x)
		}
		denseComparison := func(receiver, x *Dense) {
			receiver.Mul(x, x.T())
			receiver.Scale(f, receiver)
		}
		testOneInput(t, "SymOuterK", &SymDense{}, method, denseComparison, isAnyType, isAnySize, 1e-14)
	}
}

func TestIssue250SymOuterK(t *testing.T) {
	t.Parallel()
	x := NewVecDense(5, []float64{1, 2, 3, 4, 5})
	var s1, s2 SymDense
	s1.SymOuterK(1, x)
	s2.SymOuterK(1, x)
	s2.SymOuterK(1, x)
	if !Equal(&s1, &s2) {
		t.Error("unexpected result from repeat")
	}
}

func TestScaleSym(t *testing.T) {
	t.Parallel()
	for _, f := range []float64{0.5, 1, 3} {
		method := func(receiver, a Matrix) {
			type ScaleSymer interface {
				ScaleSym(f float64, a Symmetric)
			}
			rd := receiver.(ScaleSymer)
			rd.ScaleSym(f, a.(Symmetric))
		}
		denseComparison := func(receiver, a *Dense) {
			receiver.Scale(f, a)
		}
		testOneInput(t, "ScaleSym", &SymDense{}, method, denseComparison, legalTypeSym, isSquare, 1e-14)
	}
}

func TestSubsetSym(t *testing.T) {
	t.Parallel()
	for _, test := range []struct {
		a    *SymDense
		dims []int
		ans  *SymDense
	}{
		{
			a: NewSymDense(3, []float64{
				1, 2, 3,
				0, 4, 5,
				0, 0, 6,
			}),
			dims: []int{0, 2},
			ans: NewSymDense(2, []float64{
				1, 3,
				0, 6,
			}),
		},
		{
			a: NewSymDense(3, []float64{
				1, 2, 3,
				0, 4, 5,
				0, 0, 6,
			}),
			dims: []int{2, 0},
			ans: NewSymDense(2, []float64{
				6, 3,
				0, 1,
			}),
		},
		{
			a: NewSymDense(3, []float64{
				1, 2, 3,
				0, 4, 5,
				0, 0, 6,
			}),
			dims: []int{1, 1, 1},
			ans: NewSymDense(3, []float64{
				4, 4, 4,
				0, 4, 4,
				0, 0, 4,
			}),
		},
	} {
		var s SymDense
		s.SubsetSym(test.a, test.dims)
		if !Equal(&s, test.ans) {
			t.Errorf("SubsetSym mismatch dims %v\nGot:\n% v\nWant:\n% v\n", test.dims, s, test.ans)
		}
	}

	dims := []int{0, 2}
	maxDim := dims[0]
	for _, v := range dims {
		if maxDim < v {
			maxDim = v
		}
	}
	method := func(receiver, a Matrix) {
		type SubsetSymer interface {
			SubsetSym(a Symmetric, set []int)
		}
		rd := receiver.(SubsetSymer)
		rd.SubsetSym(a.(Symmetric), dims)
	}
	denseComparison := func(receiver, a *Dense) {
		*receiver = *NewDense(len(dims), len(dims), nil)
		sz := len(dims)
		for i := 0; i < sz; i++ {
			for j := 0; j < sz; j++ {
				receiver.Set(i, j, a.At(dims[i], dims[j]))
			}
		}
	}
	legalSize := func(ar, ac int) bool {
		return ar == ac && ar > maxDim
	}

	testOneInput(t, "SubsetSym", &SymDense{}, method, denseComparison, legalTypeSym, legalSize, 0)
}

func TestViewGrowSquare(t *testing.T) {
	t.Parallel()
	// n is the size of the original SymDense.
	// The first view uses start1, span1. The second view uses start2, span2 on
	// the first view.
	for _, test := range []struct {
		n, start1, span1, start2, span2 int
	}{
		{10, 0, 10, 0, 10},
		{10, 0, 8, 0, 8},
		{10, 2, 8, 0, 6},
		{10, 2, 7, 4, 2},
		{10, 2, 6, 0, 5},
	} {
		n := test.n
		s := NewSymDense(n, nil)
		for i := 0; i < n; i++ {
			for j := i; j < n; j++ {
				s.SetSym(i, j, float64((i+1)*n+j+1))
			}
		}

		// Take a subset and check the view matches.
		start1 := test.start1
		span1 := test.span1
		v := s.sliceSym(start1, start1+span1)
		for i := 0; i < span1; i++ {
			for j := i; j < span1; j++ {
				if v.At(i, j) != s.At(start1+i, start1+j) {
					t.Errorf("View mismatch")
				}
			}
		}

		start2 := test.start2
		span2 := test.span2
		v2 := v.SliceSym(start2, start2+span2).(*SymDense)

		for i := 0; i < span2; i++ {
			for j := i; j < span2; j++ {
				if v2.At(i, j) != s.At(start1+start2+i, start1+start2+j) {
					t.Errorf("Second view mismatch")
				}
			}
		}

		// Check that a write to the view is reflected in the original.
		v2.SetSym(0, 0, 1.2)
		if s.At(start1+start2, start1+start2) != 1.2 {
			t.Errorf("Write to view not reflected in original")
		}

		// Grow the matrix back to the original view
		gn := n - start1 - start2
		g := v2.GrowSym(gn - v2.Symmetric()).(*SymDense)
		g.SetSym(1, 1, 2.2)

		for i := 0; i < gn; i++ {
			for j := 0; j < gn; j++ {
				if g.At(i, j) != s.At(start1+start2+i, start1+start2+j) {
					t.Errorf("Grow mismatch")

					fmt.Printf("g=\n% v\n", Formatted(g))
					fmt.Printf("s=\n% v\n", Formatted(s))
					os.Exit(1)
				}
			}
		}

		// View g, then grow it and make sure all the elements were copied.
		gv := g.SliceSym(0, gn-1).(*SymDense)

		gg := gv.GrowSym(2)
		for i := 0; i < gn; i++ {
			for j := 0; j < gn; j++ {
				if g.At(i, j) != gg.At(i, j) {
					t.Errorf("Expand mismatch")
				}
			}
		}
	}
}

func TestPowPSD(t *testing.T) {
	t.Parallel()
	for cas, test := range []struct {
		a   *SymDense
		pow float64
		ans *SymDense
	}{
		// Comparison with Matlab.
		{
			a:   NewSymDense(2, []float64{10, 5, 5, 12}),
			pow: 0.5,
			ans: NewSymDense(2, []float64{3.065533767740645, 0.776210486171016, 0.776210486171016, 3.376017962209052}),
		},
		{
			a:   NewSymDense(2, []float64{11, -1, -1, 8}),
			pow: 0.5,
			ans: NewSymDense(2, []float64{3.312618742210524, -0.162963396980939, -0.162963396980939, 2.823728551267709}),
		},
		{
			a:   NewSymDense(2, []float64{10, 5, 5, 12}),
			pow: -0.5,
			ans: NewSymDense(2, []float64{0.346372134547712, -0.079637515547296, -0.079637515547296, 0.314517128328794}),
		},
		{
			a:   NewSymDense(3, []float64{15, -1, -3, -1, 8, 6, -3, 6, 14}),
			pow: 0.6,
			ans: NewSymDense(3, []float64{
				5.051214323034288, -0.163162161893975, -0.612153996497505,
				-0.163162161893976, 3.283474884617009, 1.432842761381493,
				-0.612153996497505, 1.432842761381494, 4.695873060862573,
			}),
		},
	} {
		var s SymDense
		err := s.PowPSD(test.a, test.pow)
		if err != nil {
			panic("bad test")
		}
		if !EqualApprox(&s, test.ans, 1e-10) {
			t.Errorf("Case %d, pow mismatch", cas)
			fmt.Println(Formatted(&s))
			fmt.Println(Formatted(test.ans))
		}
	}

	// Compare with Dense.Pow
	rnd := rand.New(rand.NewSource(1))
	for dim := 2; dim < 10; dim++ {
		for pow := 2; pow < 6; pow++ {
			a := NewDense(dim, dim, nil)
			for i := 0; i < dim; i++ {
				for j := 0; j < dim; j++ {
					a.Set(i, j, rnd.Float64())
				}
			}
			var mat SymDense
			mat.SymOuterK(1, a)

			var sym SymDense
			err := sym.PowPSD(&mat, float64(pow))
			if err != nil {
				t.Errorf("unexpected error: %v", err)
			}

			var dense Dense
			dense.Pow(&mat, pow)

			if !EqualApprox(&sym, &dense, 1e-10) {
				t.Errorf("Dim %d: pow mismatch", dim)
			}
		}
	}
}

func BenchmarkSymSum1000(b *testing.B) { symSumBench(b, 1000) }

var symSumForBench float64

func symSumBench(b *testing.B, size int) {
	src := rand.NewSource(1)
	a := randSymDense(size, src)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		symSumForBench = Sum(a)
	}
}

func randSymDense(size int, src rand.Source) *SymDense {
	rnd := rand.New(src)
	backData := make([]float64, size*size)
	for i := 0; i < size; i++ {
		backData[i*size+i] = rnd.Float64()
		for j := i + 1; j < size; j++ {
			v := rnd.Float64()
			backData[i*size+j] = v
			backData[j*size+i] = v
		}
	}
	s := NewSymDense(size, backData)
	return s
}
