package reedsolomon

import (
	errors "golang.org/x/xerrors"
)

type ReedSolomonDecoder struct {
	field *GenericGF
}

func NewReedSolomonDecoder(field *GenericGF) *ReedSolomonDecoder {
	return &ReedSolomonDecoder{field}
}

func (this *ReedSolomonDecoder) Decode(received []int, twoS int) ReedSolomonException {
	poly, e := NewGenericGFPoly(this.field, received)
	if e != nil {
		return WrapReedSolomonException(e)
	}
	syndromeCoefficients := make([]int, twoS)
	noError := true
	for i := 0; i < twoS; i++ {
		eval := poly.EvaluateAt(this.field.Exp(i + this.field.GetGeneratorBase()))
		syndromeCoefficients[len(syndromeCoefficients)-1-i] = eval
		if eval != 0 {
			noError = false
		}
	}
	if noError {
		return nil
	}
	syndrome, e := NewGenericGFPoly(this.field, syndromeCoefficients)
	if e != nil {
		return WrapReedSolomonException(e)
	}
	monomial, e := this.field.BuildMonomial(twoS, 1)
	if e != nil {
		return WrapReedSolomonException(e)
	}
	sigma, omega, e := this.runEuclideanAlgorithm(monomial, syndrome, twoS)
	if e != nil {
		return WrapReedSolomonException(e)
	}
	errorLocations, e := this.findErrorLocations(sigma)
	if e != nil {
		return WrapReedSolomonException(e)
	}
	errorMagnitudes, e := this.findErrorMagnitudes(omega, errorLocations)
	if e != nil {
		return WrapReedSolomonException(e)
	}
	for i := 0; i < len(errorLocations); i++ {
		log, e := this.field.Log(errorLocations[i])
		if e != nil {
			return WrapReedSolomonException(e)
		}
		position := len(received) - 1 - log
		if position < 0 {
			return NewReedSolomonException("Bad error location")
		}
		received[position] = GenericGF_addOrSubtract(received[position], errorMagnitudes[i])
	}
	return nil
}

func (this *ReedSolomonDecoder) runEuclideanAlgorithm(a, b *GenericGFPoly, R int) (sigma, omega *GenericGFPoly, e error) {
	// Assume a's degree is >= b's
	if a.GetDegree() < b.GetDegree() {
		a, b = b, a
	}

	rLast := a
	r := b
	tLast := this.field.GetZero()
	t := this.field.GetOne()

	// Run Euclidean algorithm until r's degree is less than R/2
	for r.GetDegree() >= R/2 {
		rLastLast := rLast
		tLastLast := tLast
		rLast = r
		tLast = t

		// Divide rLastLast by rLast, with quotient in q and remainder in r
		if rLast.IsZero() {
			// Oops, Euclidean algorithm already terminated?
			return nil, nil, NewReedSolomonException("r_{i-1} was zero")
		}
		r = rLastLast
		q := this.field.GetZero()
		denominatorLeadingTerm := rLast.GetCoefficient(rLast.GetDegree())
		dltInverse, e := this.field.Inverse(denominatorLeadingTerm)
		if e != nil {
			return nil, nil, e
		}
		for r.GetDegree() >= rLast.GetDegree() && !r.IsZero() {
			degreeDiff := r.GetDegree() - rLast.GetDegree()
			scale := this.field.Multiply(r.GetCoefficient(r.GetDegree()), dltInverse)
			monomial, e := this.field.BuildMonomial(degreeDiff, scale)
			if e != nil {
				return nil, nil, e
			}
			q, e = q.AddOrSubtract(monomial)
			if e != nil {
				return nil, nil, e
			}
			polynomial, e := rLast.MultiplyByMonomial(degreeDiff, scale)
			if e != nil {
				return nil, nil, e
			}
			r, e = r.AddOrSubtract(polynomial)
			if e != nil {
				return nil, nil, e
			}
		}

		q, e = q.Multiply(tLast)
		if e != nil {
			return nil, nil, e
		}
		t, e = q.AddOrSubtract(tLastLast)
		if e != nil {
			return nil, nil, e
		}

		if r.GetDegree() >= rLast.GetDegree() {
			return nil, nil, errors.New("IllegalStateException: Division algorithm failed to reduce polynomial?")
		}
	}

	sigmaTildeAtZero := t.GetCoefficient(0)
	if sigmaTildeAtZero == 0 {
		return nil, nil, NewReedSolomonException("sigmaTilde(0) was zero")
	}

	inverse, e := this.field.Inverse(sigmaTildeAtZero)
	if e != nil {
		return nil, nil, e
	}

	return t.MultiplyBy(inverse), r.MultiplyBy(inverse), nil
}

func (this *ReedSolomonDecoder) findErrorLocations(errorLocator *GenericGFPoly) ([]int, error) {
	// This is a direct application of Chien's search
	numErrors := errorLocator.GetDegree()
	if numErrors == 1 { // shortcut
		return []int{errorLocator.GetCoefficient(1)}, nil
	}
	result := make([]int, numErrors)
	e := 0
	for i := 1; i < this.field.GetSize() && e < numErrors; i++ {
		if errorLocator.EvaluateAt(i) == 0 {
			var err error
			result[e], err = this.field.Inverse(i)
			if err != nil {
				return nil, err
			}
			e++
		}
	}
	if e != numErrors {
		return nil, NewReedSolomonException("Error locator degree does not match number of roots")
	}
	return result, nil
}

func (this *ReedSolomonDecoder) findErrorMagnitudes(errorEvaluator *GenericGFPoly, errorLocations []int) ([]int, error) {
	// This is directly applying Forney's Formula
	s := len(errorLocations)
	result := make([]int, s)
	for i := 0; i < s; i++ {
		xiInverse, e := this.field.Inverse(errorLocations[i])
		if e != nil {
			return nil, e
		}
		denominator := 1
		for j := 0; j < s; j++ {
			if i != j {
				//denominator = field.multiply(denominator,
				//    GenericGF.addOrSubtract(1, field.multiply(errorLocations[j], xiInverse)));
				// Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug.
				// Below is a funny-looking workaround from Steven Parkes
				term := this.field.Multiply(errorLocations[j], xiInverse)
				var termPlus1 int
				if (term & 0x1) == 0 {
					termPlus1 = term | 1
				} else {
					termPlus1 = term & ^1
				}
				denominator = this.field.Multiply(denominator, termPlus1)
			}
		}
		inverse, e := this.field.Inverse(denominator)
		if e != nil {
			return nil, e
		}
		result[i] = this.field.Multiply(errorEvaluator.EvaluateAt(xiInverse), inverse)
		if this.field.GetGeneratorBase() != 0 {
			result[i] = this.field.Multiply(result[i], xiInverse)
		}
	}
	return result, nil
}
