"""
VCD tracer for the CBD Simulator.

Hint:
	VCD files can be read by gtkWave (for instance).
"""

from .baseTracer import BaseTracer
from pyCBD.util import PYCBD_VERSION
import re, math

_PRECS = ["s", "ms", "us", "ps", "fs"]


class VCDTracer(BaseTracer):
	"""
	VCD tracer for the CBD Simulator.

	Note:
		During the simulation, all intermediary results will be constantly
		written to the file.

	See Also:
		- `Value Change Dump <https://en.wikipedia.org/wiki/Value_change_dump>`_
		- `Verilog Hardware Description Language Reference <https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=954909&tag=1>`_
	"""

	def __init__(self, uid=-1, filename=None):
		super().__init__(uid, filename)
		self.__mapper = {}
		self.__map_range = 33, 126
		self.__assign_id = self.__map_range[0]

	def _groups(self, names, root=""):
		out = {root: {}}
		for name in names:
			vals = name.split(".")
			if len(vals) == 1:
				out[root][vals[0]] = {}
			else:
				cur = out[root]
				for v in vals:
					cur.setdefault(v, {})
					cur = cur[v]
		return out

	def traceVars(self, groups, prefix=""):
		for key in sorted(groups.keys()):
			if len(groups[key]) == 0:
				self.__mapper[prefix + key] = chr(self.__assign_id)
				self.traceln("$var real 32 %s %s $end" % (chr(self.__assign_id), re.sub('[^0-9a-zA-Z]+', "_", key)))
				self.__assign_id += 1
				if self.__assign_id > self.__map_range[1]:
					raise ValueError("Too many outputs to map for VCD!")
			else:
				self.traceln("$scope module %s $end" % re.sub('[^0-9a-zA-Z]+', "_", key))
				self.traceVars(groups[key], prefix + key + ".")
				self.traceln("$upscope $end")


	def startTracer(self, recover=False):
		super().startTracer(recover)
		if self._interpolator.is_ci_set():
			self._prec = self._interpolator.get_precision()

		self.traceln("$date\n\tGenerated at %s.\n$end\n$version\n\tGenerated by pyCBD %s.\n$end" % (self.timeInfo(), PYCBD_VERSION))
		self.traceln("$timescale 1%s $end" % _PRECS[self._prec // 3])

		header = self._groups(self._interpolator.get_header())[""]
		# Common VCD plotters have the time attribute available already,
		#   so this is overkill
		# self.traceln("$scope module globals $end")
		# self.traceVars({"time": {}})
		# self.traceln("$upscope $end")
		self.traceln("$scope module %s $end" % self._model_name)
		self.traceVars(header)
		self.traceln("$upscope $end")
		self.traceln("$enddefinitions $end")

	def traceEndNewIteration(self, curIt, time):
		if self._interpolator.is_ci_set():
			cnt = self._interpolator.get_deltas_passed(time)
			if cnt > 0:
				if curIt > 0:
					self.traceln("#%d" % round(self._interpolator.get_closest_time(time) * (10 ** self._prec)))
				self.traceln("$dumpvars")
			for i in range(cnt):
				x = self._interpolator.get_next_computation_point()
				if x <= time:
					vals = self._interpolator.compute(x)
					# self.traceln("r%.6f %s" % (x, self.__mapper["time"]))
					for ix, h in enumerate(self._interpolator.get_header()):
						self.traceln("r%.6f %s" % (vals[ix], self.__mapper[h]))
				self._interpolator.update_time()
			self._interpolator.post_compute()
			if cnt > 0:
				self.traceln("$end")
		else:
			# NOTE: multi-rate simulation assumes zero-order hold, thus this will be valid
			if curIt > 0:
				self.traceln("#%d" % round(time * (10 ** self._prec)))
			self.traceln("$dumpvars")
			# self.traceln("r%.6f %s" % (time, self.__mapper["time"]))
			for sig in self._interpolator.get_header():
				self.traceln("r%.6f %s" % (self._interpolator.get_curr_signal(sig)[1], self.__mapper[sig]))
			self.traceln("$end")

	def traceEndSimulation(self, stime):
		self.traceln("#%d" % round(stime * (10 ** self._prec)))

