package circuitcompiler

import (
	"errors"
	"math/big"
	"strconv"

	"github.com/arnaucube/go-snark/r1csqap"
)

// Circuit is the data structure of the compiled circuit
type Circuit struct {
	NVars         int
	NPublic       int
	NSignals      int
	PrivateInputs []string
	PublicInputs  []string
	Signals       []string
	Witness       []*big.Int
	Constraints   []Constraint
	R1CS          struct {
		A [][]*big.Int
		B [][]*big.Int
		C [][]*big.Int
	}
}

// Constraint is the data structure of a flat code operation
type Constraint struct {
	// v1 op v2 = out
	Op      string
	V1      string
	V2      string
	Out     string
	Literal string

	PrivateInputs []string // in func declaration case
	PublicInputs  []string // in func declaration case
}

func indexInArray(arr []string, e string) int {
	for i, a := range arr {
		if a == e {
			return i
		}
	}
	return -1
}
func isValue(a string) (bool, int) {
	v, err := strconv.Atoi(a)
	if err != nil {
		return false, 0
	}
	return true, v
}
func insertVar(arr []*big.Int, signals []string, v string, used map[string]bool) ([]*big.Int, map[string]bool) {
	isVal, value := isValue(v)
	valueBigInt := big.NewInt(int64(value))
	if isVal {
		arr[0] = new(big.Int).Add(arr[0], valueBigInt)
	} else {
		if !used[v] {
			panic(errors.New("using variable before it's set"))
		}
		arr[indexInArray(signals, v)] = new(big.Int).Add(arr[indexInArray(signals, v)], big.NewInt(int64(1)))
	}
	return arr, used
}
func insertVarNeg(arr []*big.Int, signals []string, v string, used map[string]bool) ([]*big.Int, map[string]bool) {
	isVal, value := isValue(v)
	valueBigInt := big.NewInt(int64(value))
	if isVal {
		arr[0] = new(big.Int).Add(arr[0], valueBigInt)
	} else {
		if !used[v] {
			panic(errors.New("using variable before it's set"))
		}
		arr[indexInArray(signals, v)] = new(big.Int).Add(arr[indexInArray(signals, v)], big.NewInt(int64(-1)))
	}
	return arr, used
}

// GenerateR1CS generates the R1CS polynomials from the Circuit
func (circ *Circuit) GenerateR1CS() ([][]*big.Int, [][]*big.Int, [][]*big.Int) {
	// from flat code to R1CS

	var a [][]*big.Int
	var b [][]*big.Int
	var c [][]*big.Int

	used := make(map[string]bool)
	for _, constraint := range circ.Constraints {
		aConstraint := r1csqap.ArrayOfBigZeros(len(circ.Signals))
		bConstraint := r1csqap.ArrayOfBigZeros(len(circ.Signals))
		cConstraint := r1csqap.ArrayOfBigZeros(len(circ.Signals))

		// if existInArray(constraint.Out) {
		// if used[constraint.Out] {
		// panic(errors.New("out variable already used: " + constraint.Out))
		// }
		used[constraint.Out] = true
		if constraint.Op == "in" {
			for i := 0; i <= len(circ.PublicInputs); i++ {
				aConstraint[indexInArray(circ.Signals, constraint.Out)] = new(big.Int).Add(aConstraint[indexInArray(circ.Signals, constraint.Out)], big.NewInt(int64(1)))
				aConstraint, used = insertVar(aConstraint, circ.Signals, constraint.Out, used)
				bConstraint[0] = big.NewInt(int64(1))
			}
			continue

		} else if constraint.Op == "+" {
			cConstraint[indexInArray(circ.Signals, constraint.Out)] = big.NewInt(int64(1))
			aConstraint, used = insertVar(aConstraint, circ.Signals, constraint.V1, used)
			aConstraint, used = insertVar(aConstraint, circ.Signals, constraint.V2, used)
			bConstraint[0] = big.NewInt(int64(1))
		} else if constraint.Op == "-" {
			cConstraint[indexInArray(circ.Signals, constraint.Out)] = big.NewInt(int64(1))
			aConstraint, used = insertVarNeg(aConstraint, circ.Signals, constraint.V1, used)
			aConstraint, used = insertVarNeg(aConstraint, circ.Signals, constraint.V2, used)
			bConstraint[0] = big.NewInt(int64(1))
		} else if constraint.Op == "*" {
			cConstraint[indexInArray(circ.Signals, constraint.Out)] = big.NewInt(int64(1))
			aConstraint, used = insertVar(aConstraint, circ.Signals, constraint.V1, used)
			bConstraint, used = insertVar(bConstraint, circ.Signals, constraint.V2, used)
		} else if constraint.Op == "/" {
			cConstraint, used = insertVar(cConstraint, circ.Signals, constraint.V1, used)
			cConstraint[indexInArray(circ.Signals, constraint.Out)] = big.NewInt(int64(1))
			bConstraint, used = insertVar(bConstraint, circ.Signals, constraint.V2, used)
		}

		a = append(a, aConstraint)
		b = append(b, bConstraint)
		c = append(c, cConstraint)

	}
	circ.R1CS.A = a
	circ.R1CS.B = b
	circ.R1CS.C = c
	return a, b, c
}

func grabVar(signals []string, w []*big.Int, vStr string) *big.Int {
	isVal, v := isValue(vStr)
	vBig := big.NewInt(int64(v))
	if isVal {
		return vBig
	} else {
		return w[indexInArray(signals, vStr)]
	}
}

type Inputs struct {
	Private []*big.Int
	Public  []*big.Int
}

// CalculateWitness calculates the Witness of a Circuit based on the given inputs
// witness = [ one, output, publicInputs, privateInputs, ...]
func (circ *Circuit) CalculateWitness(privateInputs []*big.Int, publicInputs []*big.Int) ([]*big.Int, error) {
	if len(privateInputs) != len(circ.PrivateInputs) {
		return []*big.Int{}, errors.New("given privateInputs != circuit.PublicInputs")
	}
	if len(publicInputs) != len(circ.PublicInputs) {
		return []*big.Int{}, errors.New("given publicInputs != circuit.PublicInputs")
	}
	w := r1csqap.ArrayOfBigZeros(len(circ.Signals))
	w[0] = big.NewInt(int64(1))
	for i, input := range publicInputs {
		w[i+1] = input
	}
	for i, input := range privateInputs {
		w[i+len(publicInputs)+1] = input
	}
	for _, constraint := range circ.Constraints {
		if constraint.Op == "in" {
		} else if constraint.Op == "+" {
			w[indexInArray(circ.Signals, constraint.Out)] = new(big.Int).Add(grabVar(circ.Signals, w, constraint.V1), grabVar(circ.Signals, w, constraint.V2))
		} else if constraint.Op == "-" {
			w[indexInArray(circ.Signals, constraint.Out)] = new(big.Int).Sub(grabVar(circ.Signals, w, constraint.V1), grabVar(circ.Signals, w, constraint.V2))
		} else if constraint.Op == "*" {
			w[indexInArray(circ.Signals, constraint.Out)] = new(big.Int).Mul(grabVar(circ.Signals, w, constraint.V1), grabVar(circ.Signals, w, constraint.V2))
		} else if constraint.Op == "/" {
			w[indexInArray(circ.Signals, constraint.Out)] = new(big.Int).Div(grabVar(circ.Signals, w, constraint.V1), grabVar(circ.Signals, w, constraint.V2))
		}
	}
	return w, nil
}
