package ar

import (
	"math/big"

	"github.com/ethereum/go-ethereum/ethutil"
)

type Entry struct {
	op   OpsFunc
	i, j *big.Int
}

type Tape struct {
	tape  []Entry
	block Block
}

func NewTape(block Block) *Tape {
	return &Tape{nil, block}
}

func (self *Tape) gen(w, h int64, gen NumberGenerator) {
	self.tape = nil

	for v := int64(0); v < h; v++ {
		op := ops[gen.rand64(lenops).Int64()]
		r := gen.rand64(100).Uint64()

		var j *big.Int
		if r < 20 && v > 20 {
			j = self.tape[len(self.tape)-1].i
		} else {
			j = gen.rand64(w)
		}

		i := gen.rand64(w)
		self.tape = append(self.tape, Entry{op, i, j})
	}
}

func (self *Tape) runTape(w, h int64, gen NumberGenerator) *big.Int {
	var mem []*big.Int
	for i := int64(0); i < w; i++ {
		mem = append(mem, gen.rand(ethutil.BigPow(2, 64)))
	}

	set := func(i, j int) Entry {
		entry := self.tape[i*100+j]
		mem[entry.i.Uint64()] = entry.op(entry.i, entry.j)

		return entry
	}

	dir := true
	for i := 0; i < int(h)/100; i++ {
		var entry Entry
		if dir {
			for j := 0; j < 100; j++ {
				entry = set(i, j)
			}
		} else {
			for j := 99; i >= 0; j-- {
				entry = set(i, j)
			}
		}

		t := mem[entry.i.Uint64()]
		if big.NewInt(2).Cmp(new(big.Int).Mod(t, big.NewInt(37))) < 0 {
			dir = !dir
		}
	}

	return Sha3(mem)
}

func (self *Tape) Verify(header, nonce []byte) bool {
	n := ethutil.BigD(nonce)

	var w int64 = 10000
	var h int64 = 150000
	gen := Rnd(Sha3([]interface{}{header, new(big.Int).Div(n, big.NewInt(1000))}))
	self.gen(w, h, gen)

	gen = Rnd(Sha3([]interface{}{header, new(big.Int).Mod(n, big.NewInt(1000))}))
	hash := self.runTape(w, h, gen)

	it := self.block.Trie().Iterator()
	next := it.Next(string(new(big.Int).Mod(hash, ethutil.BigPow(2, 160)).Bytes()))

	req := ethutil.BigPow(2, 256)
	req.Div(req, self.block.Diff())
	return Sha3([]interface{}{hash, next}).Cmp(req) < 0
}

func (self *Tape) Run(header []byte) []byte {
	nonce := big.NewInt(0)
	var w int64 = 10000
	var h int64 = 150000

	req := ethutil.BigPow(2, 256)
	req.Div(req, self.block.Diff())

	for {
		if new(big.Int).Mod(nonce, b(1000)).Cmp(b(0)) == 0 {
			gen := Rnd(Sha3([]interface{}{header, new(big.Int).Div(nonce, big.NewInt(1000))}))
			self.gen(w, h, gen)
		}

		gen := Rnd(Sha3([]interface{}{header, new(big.Int).Mod(nonce, big.NewInt(1000))}))
		hash := self.runTape(w, h, gen)

		it := self.block.Trie().Iterator()
		next := it.Next(string(new(big.Int).Mod(hash, ethutil.BigPow(2, 160)).Bytes()))

		if Sha3([]interface{}{hash, next}).Cmp(req) < 0 {
			return nonce.Bytes()
		} else {
			nonce.Add(nonce, ethutil.Big1)
		}
	}
}