import asyncio
import json
import os
import time
from tasks import ATask
from components import APid, AHeater
from components.sud import Sud, SudState


def _safe_filename_part(text):
	"""Sanitizes text for use in a log filename - sude/*.json names are
	short codes (e.g. "Sud-0010") in practice, but nothing stops a
	client from loading a schedule with spaces, slashes, etc. in its
	Name."""
	return "".join(c if c.isalnum() or c in "-_" else "_" for c in text) or "Sud"


class SudLogTask(ATask):
	"""Records every sample of the same data the GUI's Automatic tab
	plots live (temp_ist/temp_soll, rate_ist/rate_soll, power_set/
	power_eff - see client/brewpi_gui.py's PlotCanvas.sample()), plus
	the forecast curve it's compared against (tasks/sud.py's SudTask.
	forecast_t/forecast_theta), for the whole duration of a Sud run -
	for later offline analysis. One pair of JSON files per run, scoped
	to that run alone - unlike a continuously-rewritten trace file that
	keeps growing independently of whether a Sud run is even in
	progress.

	A fresh, uniquely-named pair of logs starts the moment a run
	actually starts (Sud.state leaves IDLE/DONE) and is written out
	whole the moment it ends (state reaches DONE, or is aborted back to
	IDLE via Stop) - never partially mid-run, since a half-written file
	is of no use before the run is over anyway. The forecast file is
	written from SudTask.forecast_t/forecast_theta as they stand at
	that point - the *final*, most-corrected version (see tasks/sud.py's
	SudTask._continue_forecast_after_confirm()), not the optimistic
	guess computed back at Load.

	Samples are taken once per interval regardless of Sud.state
	(RAMPING/HOLDING/WAIT_USER/PAUSED all keep recording, mirroring
	SudTask.on_state_changed()'s "any of these means a run is in
	progress" - a paused run's temperature is still worth capturing,
	and the real controller keeps actively driving toward its target
	throughout WAIT_USER too)."""

	def __init__(self, sud: Sud, tc: APid, heater: AHeater, heater_task, sud_task, interval, path='./logs'):
		ATask.__init__(self, interval)
		self.sud = sud
		self.tc = tc
		self.heater = heater
		self.heater_task = heater_task
		self.sud_task = sud_task
		self.path = path
		# None while no run is in progress - see on_sud_state_changed().
		self._samples = None
		# Identifies this run's pair of files (log_<run_id>_<name>.json
		# and forecast_<run_id>_<name>.json) - fixed at the moment the run
		# starts, not when it's written out, so the name reflects when
		# the run happened rather than how long it took.
		self._run_id = None

	@staticmethod
	def _is_running(state):
		return state not in (SudState.IDLE, SudState.DONE)

	def on_sud_state_changed(self, state):
		was_running = self._samples is not None
		if self._is_running(state) and not was_running:
			self._samples = []
			self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
		elif not self._is_running(state) and was_running:
			self._write()
			self._samples = None
			self._run_id = None

	def _write(self):
		if not self._samples:
			return
		os.makedirs(self.path, exist_ok=True)
		name_part = _safe_filename_part(self.sud.name)

		samples_filename = "log_{}_{}.json".format(self._run_id, name_part)
		with open(os.path.join(self.path, samples_filename), "w") as f:
			json.dump({'Name': self.sud.name, 'Samples': self._samples}, f, indent='\t')
		print("Sud log: wrote {} samples to {}".format(len(self._samples), samples_filename))

		forecast_filename = "forecast_{}_{}.json".format(self._run_id, name_part)
		with open(os.path.join(self.path, forecast_filename), "w") as f:
			json.dump({
				'Name': self.sud.name,
				'T': self.sud_task.forecast_t,
				'Theta': self.sud_task.forecast_theta,
				'Finished': self.sud_task.forecast_finished,
			}, f, indent='\t')
		print("Sud log: wrote forecast to {}".format(forecast_filename))

	async def on_process(self):
		print("SudLogTask: started with interval {} s".format(self.interval))
		self.sud.set_on_changed('state', self.on_sud_state_changed)

		while True:
			if self._samples is not None:
				# power_set mirrors the GUI's 'PowerSet' (the continuous,
				# pre-PWM commanded power) - tasks/heater.py's HeaterTask
				# only ever holds this combined value as a local loop
				# variable, recomputed here from the same two attributes
				# it derives it from, rather than restructuring that task
				# just to expose it.
				power_set = max(self.heater_task.power_soll, self.heater_task.power_actor)
				self._samples.append({
					't': self.sud.elapsed,
					'timestamp': time.time(),
					'temp_ist': self.tc.theta_ist,
					'temp_soll': self.tc.theta_soll_set,
					'rate_ist': self.tc.heatrate_ist,
					'rate_soll': self.tc.heatrate_soll,
					'power_set': power_set,
					'power_eff': self.heater.power_eff,
				})
			await asyncio.sleep(self.interval)
