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

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

func TestNewBand(t *testing.T) {
	t.Parallel()
	for i, test := range []struct {
		data   []float64
		r, c   int
		kl, ku int
		mat    *BandDense
		dense  *Dense
	}{
		{
			data: []float64{
				-1, 1, 2, 3,
				4, 5, 6, 7,
				8, 9, 10, 11,
				12, 13, 14, 15,
				16, 17, 18, -1,
				19, 20, -1, -1,
			},
			r: 6, c: 6,
			kl: 1, ku: 2,
			mat: &BandDense{
				mat: blas64.Band{
					Rows:   6,
					Cols:   6,
					KL:     1,
					KU:     2,
					Stride: 4,
					Data: []float64{
						-1, 1, 2, 3,
						4, 5, 6, 7,
						8, 9, 10, 11,
						12, 13, 14, 15,
						16, 17, 18, -1,
						19, 20, -1, -1,
					},
				},
			},
			dense: NewDense(6, 6, []float64{
				1, 2, 3, 0, 0, 0,
				4, 5, 6, 7, 0, 0,
				0, 8, 9, 10, 11, 0,
				0, 0, 12, 13, 14, 15,
				0, 0, 0, 16, 17, 18,
				0, 0, 0, 0, 19, 20,
			}),
		},
		{
			data: []float64{
				-1, 1, 2, 3,
				4, 5, 6, 7,
				8, 9, 10, 11,
				12, 13, 14, 15,
				16, 17, 18, -1,
				19, 20, -1, -1,
				21, -1, -1, -1,
			},
			r: 10, c: 6,
			kl: 1, ku: 2,
			mat: &BandDense{
				mat: blas64.Band{
					Rows:   10,
					Cols:   6,
					KL:     1,
					KU:     2,
					Stride: 4,
					Data: []float64{
						-1, 1, 2, 3,
						4, 5, 6, 7,
						8, 9, 10, 11,
						12, 13, 14, 15,
						16, 17, 18, -1,
						19, 20, -1, -1,
						21, -1, -1, -1,
					},
				},
			},
			dense: NewDense(10, 6, []float64{
				1, 2, 3, 0, 0, 0,
				4, 5, 6, 7, 0, 0,
				0, 8, 9, 10, 11, 0,
				0, 0, 12, 13, 14, 15,
				0, 0, 0, 16, 17, 18,
				0, 0, 0, 0, 19, 20,
				0, 0, 0, 0, 0, 21,
				0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0,
			}),
		},
		{
			data: []float64{
				-1, 1, 2, 3,
				4, 5, 6, 7,
				8, 9, 10, 11,
				12, 13, 14, 15,
				16, 17, 18, 19,
				20, 21, 22, 23,
			},
			r: 6, c: 10,
			kl: 1, ku: 2,
			mat: &BandDense{
				mat: blas64.Band{
					Rows:   6,
					Cols:   10,
					KL:     1,
					KU:     2,
					Stride: 4,
					Data: []float64{
						-1, 1, 2, 3,
						4, 5, 6, 7,
						8, 9, 10, 11,
						12, 13, 14, 15,
						16, 17, 18, 19,
						20, 21, 22, 23,
					},
				},
			},
			dense: NewDense(6, 10, []float64{
				1, 2, 3, 0, 0, 0, 0, 0, 0, 0,
				4, 5, 6, 7, 0, 0, 0, 0, 0, 0,
				0, 8, 9, 10, 11, 0, 0, 0, 0, 0,
				0, 0, 12, 13, 14, 15, 0, 0, 0, 0,
				0, 0, 0, 16, 17, 18, 19, 0, 0, 0,
				0, 0, 0, 0, 20, 21, 22, 23, 0, 0,
			}),
		},
	} {
		band := NewBandDense(test.r, test.c, test.kl, test.ku, test.data)
		rows, cols := band.Dims()

		if rows != test.r {
			t.Errorf("unexpected number of rows for test %d: got: %d want: %d", i, rows, test.r)
		}
		if cols != test.c {
			t.Errorf("unexpected number of cols for test %d: got: %d want: %d", i, cols, test.c)
		}
		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 TestNewDiagonalRect(t *testing.T) {
	t.Parallel()
	for i, test := range []struct {
		data  []float64
		r, c  int
		mat   *BandDense
		dense *Dense
	}{
		{
			data: []float64{1, 2, 3, 4, 5, 6},
			r:    6, c: 6,
			mat: &BandDense{
				mat: blas64.Band{
					Rows:   6,
					Cols:   6,
					Stride: 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,
			}),
		},
		{
			data: []float64{1, 2, 3, 4, 5, 6},
			r:    7, c: 6,
			mat: &BandDense{
				mat: blas64.Band{
					Rows:   7,
					Cols:   6,
					Stride: 1,
					Data:   []float64{1, 2, 3, 4, 5, 6},
				},
			},
			dense: NewDense(7, 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,
				0, 0, 0, 0, 0, 0,
			}),
		},
		{
			data: []float64{1, 2, 3, 4, 5, 6},
			r:    6, c: 7,
			mat: &BandDense{
				mat: blas64.Band{
					Rows:   6,
					Cols:   7,
					Stride: 1,
					Data:   []float64{1, 2, 3, 4, 5, 6},
				},
			},
			dense: NewDense(6, 7, []float64{
				1, 0, 0, 0, 0, 0, 0,
				0, 2, 0, 0, 0, 0, 0,
				0, 0, 3, 0, 0, 0, 0,
				0, 0, 0, 4, 0, 0, 0,
				0, 0, 0, 0, 5, 0, 0,
				0, 0, 0, 0, 0, 6, 0,
			}),
		},
	} {
		band := NewDiagonalRect(test.r, test.c, test.data)
		rows, cols := band.Dims()

		if rows != test.r {
			t.Errorf("unexpected number of rows for test %d: got: %d want: %d", i, rows, test.r)
		}
		if cols != test.c {
			t.Errorf("unexpected number of cols for test %d: got: %d want: %d", i, cols, test.c)
		}
		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 TestBandDenseZero(t *testing.T) {
	t.Parallel()
	// Elements that equal 1 should be set to zero, elements that equal -1
	// should remain unchanged.
	for _, test := range []*BandDense{
		{
			mat: blas64.Band{
				Rows:   6,
				Cols:   7,
				Stride: 8,
				KL:     1,
				KU:     2,
				Data: []float64{
					-1, 1, 1, 1, -1, -1, -1, -1,
					1, 1, 1, 1, -1, -1, -1, -1,
					1, 1, 1, 1, -1, -1, -1, -1,
					1, 1, 1, 1, -1, -1, -1, -1,
					1, 1, 1, -1, -1, -1, -1, -1,
					1, 1, -1, -1, -1, -1, -1, -1,
				},
			},
		},
		{
			mat: blas64.Band{
				Rows:   6,
				Cols:   7,
				Stride: 8,
				KL:     2,
				KU:     1,
				Data: []float64{
					-1, -1, 1, 1, -1, -1, -1, -1,
					-1, 1, 1, 1, -1, -1, -1, -1,
					1, 1, 1, 1, -1, -1, -1, -1,
					1, 1, 1, 1, -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 TestBandDiagView(t *testing.T) {
	t.Parallel()
	for cas, test := range []*BandDense{
		NewBandDense(1, 1, 0, 0, []float64{1}),
		NewBandDense(6, 6, 1, 2, []float64{
			-1, 2, 3, 4,
			5, 6, 7, 8,
			9, 10, 11, 12,
			13, 14, 15, 16,
			17, 18, 19, -1,
			21, 22, -1, -1,
		}),
		NewBandDense(6, 6, 2, 1, []float64{
			-1, -1, 1, 2,
			-1, 3, 4, 5,
			6, 7, 8, 9,
			10, 11, 12, 13,
			14, 15, 16, 17,
			18, 19, 20, -1,
		}),
	} {
		testDiagView(t, cas, test)
	}
}

func TestBandAtSet(t *testing.T) {
	t.Parallel()
	// 2  3  4  0  0  0
	// 5  6  7  8  0  0
	// 0  9 10 11 12  0
	// 0  0 13 14 15 16
	// 0  0  0 17 18 19
	// 0  0  0  0 21 22
	band := NewBandDense(6, 6, 1, 2, []float64{
		-1, 2, 3, 4,
		5, 6, 7, 8,
		9, 10, 11, 12,
		13, 14, 15, 16,
		17, 18, 19, -1,
		21, 22, -1, -1,
	})

	rows, cols := band.Dims()
	kl, ku := band.Bandwidth()

	// Explicitly test all indexes.
	want := bandImplicit{rows, cols, kl, ku, func(i, j int) float64 {
		return float64(i*(kl+ku) + j + kl + 1)
	}}
	for i := 0; i < 6; i++ {
		for j := 0; j < 6; j++ {
			if band.At(i, j) != want.At(i, j) {
				t.Errorf("unexpected value for band.At(%d, %d): got:%v want:%v", i, j, band.At(i, j), want.At(i, j))
			}
		}
	}
	// Do that same thing via a call to Equal.
	if !Equal(band, want) {
		t.Errorf("unexpected value via mat.Equal:\ngot:\n% v\nwant:\n% v", Formatted(band), Formatted(want))
	}

	// Check At out of bounds
	for _, row := range []int{-1, rows, rows + 1} {
		panicked, message := panics(func() { band.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() { band.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() { band.SetBand(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() { band.SetBand(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
	}{
		{row: 0, col: 3},
		{row: 0, col: 4},
		{row: 0, col: 5},
		{row: 1, col: 4},
		{row: 1, col: 5},
		{row: 2, col: 5},
		{row: 2, col: 0},
		{row: 3, col: 1},
		{row: 4, col: 2},
		{row: 5, col: 3},
	} {
		panicked, message := panics(func() { band.SetBand(st.row, st.col, 1.2) })
		if !panicked || message != ErrBandSet.Error() {
			t.Errorf("expected panic for %+v %s", st, message)
		}
	}

	for _, st := range []struct {
		row, col  int
		orig, new float64
	}{
		{row: 1, col: 2, orig: 7, new: 15},
		{row: 2, col: 3, orig: 11, new: 15},
	} {
		if e := band.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)
		}
		band.SetBand(st.row, st.col, st.new)
		if e := band.At(st.row, st.col); e != st.new {
			t.Errorf("unexpected value for At(%d, %d) after SetBand(%[1]d, %d, %v): got: %v want: %[3]v", st.row, st.col, st.new, e)
		}
	}
}

// bandImplicit is an implicit band matrix returning val(i, j)
// for the value at (i, j).
type bandImplicit struct {
	r, c, kl, ku int
	val          func(i, j int) float64
}

func (b bandImplicit) Dims() (r, c int) {
	return b.r, b.c
}

func (b bandImplicit) T() Matrix {
	return Transpose{b}
}

func (b bandImplicit) At(i, j int) float64 {
	if i < 0 || b.r <= i {
		panic("row")
	}
	if j < 0 || b.c <= j {
		panic("col")
	}
	if j < i-b.kl || i+b.ku < j {
		return 0
	}
	return b.val(i, j)
}
