// Package msp implements matrix operations for elements in GF(2^128).
package msp

type Row []FieldElem

// NewRow returns a row of length s with all zero entries.
func NewRow(s int) Row {
	out := Row(make([]FieldElem, s))

	for i := 0; i < s; i++ {
		out[i] = NewFieldElem()
	}

	return out
}

// AddM adds two vectors.
func (e Row) AddM(f Row) {
	le, lf := e.Size(), f.Size()
	if le != lf {
		panic("Can't add rows that are different sizes!")
	}

	for i, fI := range f {
		e[i].AddM(fI)
	}

	return
}

// MulM multiplies the row by a scalar.
func (e Row) MulM(f FieldElem) {
	for i := range e {
		e[i] = e[i].Mul(f)
	}
}

func (e Row) Mul(f FieldElem) Row {
	out := NewRow(e.Size())

	for i := 0; i < e.Size(); i++ {
		out[i] = e[i].Mul(f)
	}

	return out
}

// DotProduct computes the dot product of two vectors.
func (e Row) DotProduct(f Row) FieldElem {
	if e.Size() != f.Size() {
		panic("Can't get dot product of rows of different length!")
	}

	out := NewFieldElem()

	for i := 0; i < e.Size(); i++ {
		out.AddM(e[i].Mul(f[i]))
	}

	return out
}

func (e Row) Size() int {
	return len(e)
}

type Matrix []Row

// Mul right-multiplies a matrix by a row.
func (e Matrix) Mul(f Row) Row {
	out, in := e.Size()
	if in != f.Size() {
		panic("Can't multiply by row that is wrong size!")
	}

	res := NewRow(out)

	for i := 0; i < out; i++ {
		res[i] = e[i].DotProduct(f)
	}

	return res
}

// Recovery returns the row vector that takes this matrix to the target vector [1 0 0 ... 0].
func (e Matrix) Recovery() (Row, bool) {
	a, b := e.Size()

	// aug is the target vector.
	aug := NewRow(a)
	aug[0] = One.Dup()

	// Duplicate e away so we don't mutate it; transpose it at the same time.
	f := make([]Row, b)
	for i := range f {
		f[i] = NewRow(a)
	}

	for i := 0; i < a; i++ {
		for j := 0; j < b; j++ {
			f[j][i] = e[i][j].Dup()
		}
	}

	for row := range f {
		if row >= b { // The matrix is tall and thin--we've finished before exhausting all the rows.
			break
		}

		// Find a row with a non-zero entry in the (row)th position
		candId := -1
		for j, fJ := range f[row:] {
			if !fJ[row].IsZero() {
				candId = j + row
				break
			}
		}

		if candId == -1 { // If we can't find one, fail and return our partial work.
			return aug, false
		}

		// Move it to the top
		f[row], f[candId] = f[candId], f[row]
		aug[row], aug[candId] = aug[candId], aug[row]

		// Make the pivot 1.
		fInv := f[row][row].Invert()

		f[row].MulM(fInv)
		aug[row] = aug[row].Mul(fInv)

		// Cancel out the (row)th position for every row above and below it.
		for i := range f {
			if i != row && !f[i][row].IsZero() {
				c := f[i][row].Dup()

				f[i].AddM(f[row].Mul(c))
				aug[i].AddM(aug[row].Mul(c))
			}
		}
	}

	return aug, true
}

func (e Matrix) Size() (int, int) {
	return len(e), e[0].Size()
}
