// Copyright ©2018 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 (
	"math"
	"reflect"
	"testing"

	"golang.org/x/exp/rand"

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

func TestNewDiagDense(t *testing.T) {
	t.Parallel()
	for i, test := range []struct {
		data  []float64
		n     int
		mat   *DiagDense
		dense *Dense
	}{
		{
			data: []float64{1, 2, 3, 4, 5, 6},
			n:    6,
			mat: &DiagDense{
				mat: blas64.Vector{N: 6, Inc: 1, Data: []float64{1, 2, 3, 4, 5, 6}},
			},
			dense: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
	} {
		band := NewDiagDense(test.n, test.data)
		rows, cols := band.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(band, test.mat) {
			t.Errorf("unexpected value via reflect for test %d: got: %v want: %v", i, band, test.mat)
		}
		if !Equal(band, test.mat) {
			t.Errorf("unexpected value via mat.Equal for test %d: got: %v want: %v", i, band, test.mat)
		}
		if !Equal(band, test.dense) {
			t.Errorf("unexpected value via mat.Equal(band, dense) for test %d:\ngot:\n% v\nwant:\n% v", i, Formatted(band), Formatted(test.dense))
		}
	}
}

func TestDiagDenseZero(t *testing.T) {
	t.Parallel()
	// Elements that equal 1 should be set to zero, elements that equal -1
	// should remain unchanged.
	for _, test := range []*DiagDense{
		{
			mat: blas64.Vector{
				N:   5,
				Inc: 2,
				Data: []float64{
					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 TestDiagonalStride(t *testing.T) {
	t.Parallel()
	for _, test := range []struct {
		diag  *DiagDense
		dense *Dense
	}{
		{
			diag: &DiagDense{
				mat: blas64.Vector{N: 6, Inc: 1, Data: []float64{1, 2, 3, 4, 5, 6}},
			},
			dense: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			diag: &DiagDense{
				mat: blas64.Vector{N: 6, Inc: 2, Data: []float64{
					1, 0,
					2, 0,
					3, 0,
					4, 0,
					5, 0,
					6,
				}},
			},
			dense: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			diag: &DiagDense{
				mat: blas64.Vector{N: 6, Inc: 5, Data: []float64{
					1, 0, 0, 0, 0,
					2, 0, 0, 0, 0,
					3, 0, 0, 0, 0,
					4, 0, 0, 0, 0,
					5, 0, 0, 0, 0,
					6,
				}},
			},
			dense: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
	} {
		if !Equal(test.diag, test.dense) {
			t.Errorf("unexpected value via mat.Equal for stride %d: got: %v want: %v",
				test.diag.mat.Inc, test.diag, test.dense)
		}
	}
}

func TestDiagFrom(t *testing.T) {
	t.Parallel()
	for i, test := range []struct {
		mat  Matrix
		want *Dense
	}{
		{
			mat: NewDiagDense(6, []float64{1, 2, 3, 4, 5, 6}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewBandDense(6, 6, 1, 1, []float64{
				math.NaN(), 1, math.NaN(),
				math.NaN(), 2, math.NaN(),
				math.NaN(), 3, math.NaN(),
				math.NaN(), 4, math.NaN(),
				math.NaN(), 5, math.NaN(),
				math.NaN(), 6, math.NaN(),
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewDense(6, 6, []float64{
				1, math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), 2, math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), 3, math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), 4, math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), 5, math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(), 6,
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewDense(6, 4, []float64{
				1, math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), 2, math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), 3, math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), 4,
				math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(),
			}),
			want: NewDense(4, 4, []float64{
				1, 0, 0, 0,
				0, 2, 0, 0,
				0, 0, 3, 0,
				0, 0, 0, 4,
			}),
		},
		{
			mat: NewDense(4, 6, []float64{
				1, math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), 2, math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), 3, math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), 4, math.NaN(), math.NaN(),
			}),
			want: NewDense(4, 4, []float64{
				1, 0, 0, 0,
				0, 2, 0, 0,
				0, 0, 3, 0,
				0, 0, 0, 4,
			}),
		},
		{
			mat: NewSymBandDense(6, 1, []float64{
				1, math.NaN(),
				2, math.NaN(),
				3, math.NaN(),
				4, math.NaN(),
				5, math.NaN(),
				6, math.NaN(),
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewSymDense(6, []float64{
				1, math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), 2, math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), 3, math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), 4, math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), 5, math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(), 6,
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewTriBandDense(6, 2, Upper, []float64{
				1, math.NaN(), math.NaN(),
				2, math.NaN(), math.NaN(),
				3, math.NaN(), math.NaN(),
				4, math.NaN(), math.NaN(),
				5, math.NaN(), math.NaN(),
				6, math.NaN(), math.NaN(),
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewTriBandDense(6, 2, Lower, []float64{
				math.NaN(), math.NaN(), 1,
				math.NaN(), math.NaN(), 2,
				math.NaN(), math.NaN(), 3,
				math.NaN(), math.NaN(), 4,
				math.NaN(), math.NaN(), 5,
				math.NaN(), math.NaN(), 6,
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewTriDense(6, Upper, []float64{
				1, math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), 2, math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), 3, math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), 4, math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), 5, math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(), 6,
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat: NewTriDense(6, Lower, []float64{
				1, math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), 2, math.NaN(), math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), 3, math.NaN(), math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), 4, math.NaN(), math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), 5, math.NaN(),
				math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(), 6,
			}),
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
		{
			mat:  NewVecDense(6, []float64{1, 2, 3, 4, 5, 6}),
			want: NewDense(1, 1, []float64{1}),
		},
		{
			mat: &basicMatrix{
				mat: blas64.General{
					Rows:   6,
					Cols:   6,
					Stride: 6,
					Data: []float64{
						1, math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
						math.NaN(), 2, math.NaN(), math.NaN(), math.NaN(), math.NaN(),
						math.NaN(), math.NaN(), 3, math.NaN(), math.NaN(), math.NaN(),
						math.NaN(), math.NaN(), math.NaN(), 4, math.NaN(), math.NaN(),
						math.NaN(), math.NaN(), math.NaN(), math.NaN(), 5, math.NaN(),
						math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(), 6,
					},
				},
				capRows: 6,
				capCols: 6,
			},
			want: NewDense(6, 6, []float64{
				1, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0,
				0, 0, 0, 4, 0, 0,
				0, 0, 0, 0, 5, 0,
				0, 0, 0, 0, 0, 6,
			}),
		},
	} {
		var got DiagDense
		got.DiagFrom(test.mat)
		if !Equal(&got, test.want) {
			r, c := test.mat.Dims()
			t.Errorf("unexpected value via mat.Equal for %d×%d %T test %d:\ngot:\n% v\nwant:\n% v",
				r, c, test.mat, i, Formatted(&got), Formatted(test.want))
		}
	}
}

// diagDenseViewer takes the view of the Diagonal with the underlying Diagonal
// as the DiagDense type.
type diagDenseViewer interface {
	Matrix
	DiagView() Diagonal
}

func testDiagView(t *testing.T, cas int, test diagDenseViewer) {
	// Check the DiagView matches the Diagonal.
	r, c := test.Dims()
	diagView := test.DiagView()
	for i := 0; i < min(r, c); i++ {
		if diagView.At(i, i) != test.At(i, i) {
			t.Errorf("Diag mismatch case %d, element %d", cas, i)
		}
	}

	// Check that changes to the diagonal are reflected.
	offset := 10.0
	diag := diagView.(*DiagDense)
	for i := 0; i < min(r, c); i++ {
		v := test.At(i, i)
		diag.SetDiag(i, v+offset)
		if test.At(i, i) != v+offset {
			t.Errorf("Diag set mismatch case %d, element %d", cas, i)
		}
	}

	// Check that DiagView and DiagFrom match.
	var diag2 DiagDense
	diag2.DiagFrom(test)
	if !Equal(diag, &diag2) {
		t.Errorf("Cas %d: DiagView and DiagFrom mismatch", cas)
	}
}

func TestDiagonalAtSet(t *testing.T) {
	t.Parallel()
	for _, n := range []int{1, 3, 8} {
		for _, nilstart := range []bool{true, false} {
			var diag *DiagDense
			if nilstart {
				diag = NewDiagDense(n, nil)
			} else {
				data := make([]float64, n)
				diag = NewDiagDense(n, data)
				// Test the data is used.
				for i := range data {
					data[i] = -float64(i) - 1
					v := diag.At(i, i)
					if v != data[i] {
						t.Errorf("Diag shadow mismatch. Got %v, want %v", v, data[i])
					}
				}
			}
			for i := 0; i < n; i++ {
				for j := 0; j < n; j++ {
					if i != j {
						if diag.At(i, j) != 0 {
							t.Errorf("Diag returned non-zero off diagonal element at %d, %d", i, j)
						}
					}
					v := float64(i) + 1
					diag.SetDiag(i, v)
					v2 := diag.At(i, i)
					if v2 != v {
						t.Errorf("Diag at/set mismatch. Got %v, want %v", v, v2)
					}
				}
			}
		}
	}
}

func randDiagDense(size int, rnd *rand.Rand) *DiagDense {
	t := NewDiagDense(size, nil)
	for i := 0; i < size; i++ {
		t.SetDiag(i, rnd.Float64())
	}
	return t
}
