#Main Application Central Data Store and core functionality

import vts
import vbox
import gui
from unit_info import Unit
from micropython import const
import backlight
import json
import vbo
from coldstart import coldstart
from gui_utils import Colours

class ApplicationError(Exception):
	pass

class Application:
	'''Use like a Singleton to store application information'''
	version = '1.0.0.1'
	dev_mode = False
	image_bank = None
	first_fix = False
	gnss_valid = False
	vehicle_static = True
	min_speed_threshold = 0.1389
	state_period_ms = 0
	last_sample = None
	rtc_mask = vbox.VBOX_FLG_VALID_DATE | vbox.VBOX_FLG_VALID_TIME
	rtc_validated = False
	batt_pack = None
	background_tasks = []

	class Data:
		#Store like 'Model Data' from main app
		sats = 0
		tod_ms = 0
		fix_type = 0
		speed_ms = 0.0
		solution_type = 0
		lat_degE7 = 0
		lng_degE7 = 0
		link_time = 0
		heading_raw = 0
		alt_msl_m = 0.0
		range = 0.0
		latRange = 0.0
		lngRange = 0.0
		t2c = 0.0
		angle = 0.0
		heading_locked = 0.0
		adas_sample = None
		link_stability = 0
		dropped_last_sample = False
		reference_point = None

	class UserSettings:
		#TODO: Redo settings entirely, its overly complicated and doesn't require all these indexes.
		LOG_CONTINUOUS  = const(0)
		LOG_WHEN_MOVING = const(1)
		LOG_MANUAL      = const(-1) #Not a user-selectable settings option

		SPEED_UNITS_KMH = const(0)
		SPD_UNITS_MPH   = const(1)
		SPD_UNITS_MS    = const(2)

		ACCEL_UNITS_MS2 = const(0)
		ACCEL_UNITS_G   = const(1)

		RTK_BAUD_115200 = const(0)
		RTK_BAUD_38400 	= const(1)

		BRIGHTNESS_AUTO = const(0)

		speed_units_dict = {
			SPEED_UNITS_KMH: ('KM/H', 3.6),
			SPD_UNITS_MPH: ('MPH', 2.2369362921),
			# SPD_UNITS_MS: ('M/S', 1.0),
		}
		accel_units_dict = {
			ACCEL_UNITS_MS2: ('M/S2', 1.0),
			ACCEL_UNITS_G: ('G', 0.1019716213),
		}
		log_mode_dict = {
			# LOG_CONTINUOUS: ("CONTINUOUS", 0),
			# LOG_WHEN_MOVING: ("MOVING", 1),			
			LOG_CONTINUOUS: ('Continuous', 0),
			LOG_WHEN_MOVING: ('Moving', 1),
		}
		rtk_baud_dict = {
			RTK_BAUD_115200: ('115200', 115200),
			RTK_BAUD_38400: ('38400', 38400),
		}
		screen_brightness_dict = {
			BRIGHTNESS_AUTO: ('AUTO', 0),
			1: ('1', (1 * 20)**2),
			2: ('2', (2 * 20)**2),
			3: ('3', (3 * 20)**2),
			4: ('4', (4 * 20)**2),
			5: ('5', (5 * 20)**2),
		}
		if Unit.info['Light Sensor'] == 'NONE':
			screen_brightness_dict[BRIGHTNESS_AUTO] = ('0', 200)

		_speed_units_index = SPEED_UNITS_KMH
		_accel_units_index = ACCEL_UNITS_MS2
		_log_mode_index = LOG_CONTINUOUS
		_rtk_baud_index = RTK_BAUD_115200
		_screen_brightness_index = BRIGHTNESS_AUTO

		##### SETTINGS #####
		speed_units = speed_units_dict[_speed_units_index][1]
		accel_units = accel_units_dict[_accel_units_index][1]
		log_mode = log_mode_dict[_log_mode_index][1]
		rtk_baud = rtk_baud_dict[_rtk_baud_index][1]
		screen_brightness = screen_brightness_dict[_screen_brightness_index][1]

		@classmethod
		def set_user_speed_units(cls, index):
			try:
				cls.speed_units = cls.speed_units_dict[index][1]
				cls._speed_units_index = index

			except IndexError:
				cls.speed_units = cls.speed_units_dict[cls.SPEED_UNITS_KMH][1]
				cls._speed_units_index = cls.SPEED_UNITS_KMH

		@classmethod
		def set_user_accel_units(cls, index):
			try:
				cls.accel_units = cls.accel_units_dict[index][1]
				cls._accel_units_index = index
			except IndexError:
				cls.accel_units = cls.accel_units_dict[cls.ACCEL_UNITS_MS2][1]
				cls._accel_units_index = cls.ACCEL_UNITS_MS2

		@classmethod
		def set_log_mode(cls, index):
			# import vbo
			try:
				cls.log_mode = cls.log_mode_dict[index][1]
				cls._log_mode_index = index
			except IndexError:
				cls.log_mode = cls._log_mode_index = cls.LOG_CONTINUOUS

			# vbo.set_resume_mode(1 if cls.log_mode == cls.LOG_WHEN_MOVING else 0)
			# print("LOG_MODE_SET: ", index)

		@classmethod
		def set_rtk_baud(cls, index):
			try:
				cls.rtk_baud = cls.rtk_baud_dict[index][1]
				cls._rtk_baud_index = index
			except IndexError:
				cls.rtk_baud = cls.rtk_baud_dict[cls.RTK_BAUD_115200][1]
				cls._rtk_baud_index = cls.RTK_BAUD_115200
			if Unit.supports_RTK():
				vbox.cfg_gnss({'DGPS Baudrate':cls.rtk_baud})

		@classmethod
		def set_brightness(cls, index):
			try:
				if index == cls.BRIGHTNESS_AUTO and cls.screen_brightness_dict[index][1] == cls.BRIGHTNESS_AUTO:
						backlight.set_auto(25, 1000, 400)
				else:
					backlight.set(cls.screen_brightness_dict[index][1])
				cls.screen_brightness = cls._screen_brightness_index = index
			except IndexError:
				backlight.set_auto(25, 1000, 400)
				cls.screen_brightness = cls._screen_brightness_index = cls.BRIGHTNESS_AUTO

		@classmethod
		def load_from_file(cls):
			try:
				with open('/sd/app_settings.json') as file:
					settings_file = json.load(file)

				cls.set_user_speed_units(settings_file['speed_units'])
				cls.set_user_accel_units(settings_file['accel_units'])
				cls.set_log_mode(settings_file['log_mode'])
				cls.set_brightness(settings_file['screen_brightness'])
				if Unit.supports_RTK():
					cls.set_rtk_baud(settings_file['rtk_baud'])

			except OSError:
				#No SD Card or file not yet created
				try:
					cls.save_settings()
				except Exception:
					pass 

			except Exception as e:
				#Unknown Settings error, remove the file
				print("Settings File Error: ", str(e))
				try:
					from os import unlink
					unlink('/sd/app_settings.json')
				except Exception as e:
					print("Unable to remove Settings File: ", str(e))

		@staticmethod
		def get_index_by_value_str(value, settings_dict, ignore_case=False):
			# print("Searching for ", value)
			for index, options in settings_dict.items():
				# print("\t - Checking", options[0], end=" - ")
				param = str(options[0]) if ignore_case else str(options[0]).lower()
				if param  == value:
					# print("FOUND")
					return index
				# print("Incorrect")
			raise AttributeError

		@staticmethod
		def get_index_by_value_int(value, settings_dict):
			# print("Searching for ", value)
			for index, options in settings_dict.items():
				# print("\t - Checking", options[0], end=" - ")
				if int(options[0]) == value:
					# print("FOUND")
					return index
				# print("Incorrect")
			raise AttributeError

		@classmethod
		def load_from_eeprom(cls):
			import eeprom
			if Unit.supports_RTK():
				cfg_gnss = eeprom.get_gnss() # {'Options': 0, 'DGPS Baudrate': 38400, 'GNSS Used': 3}
				try:
					cls.set_rtk_baud(cls.get_index_by_value_int(cfg_gnss['DGPS Baudrate'], cls.rtk_baud_dict))
				except AttributeError:
					pass

			cfg_sys = eeprom.get_sys()
			try:
				cls.set_user_speed_units(cls.get_index_by_value_str(cfg_sys['Speed Units'], cls.speed_units_dict))
			except AttributeError:
				pass
			try:
				cls.set_user_accel_units(cls.get_index_by_value_str(cfg_sys['Accel Units'], cls.accel_units_dict))
			except AttributeError:
				pass
			try:
				cls.set_brightness(cfg_sys['Screen Brightness'])
			except KeyError:
				pass

			cfg_log = eeprom.get_logging()
			try:
				cls.set_log_mode(cls.get_index_by_value_str(cfg_log['Mode'], cls.log_mode_dict, True))
			except (KeyError, AttributeError):
				pass


		@classmethod
		def save_to_eeprom(cls):
			import eeprom
			if Unit.supports_RTK():
				cfg_gnss = eeprom.get_gnss() # {'Options': 0, 'DGPS Baudrate': 38400, 'GNSS Used': 3}
				cfg_gnss['DGPS Baudrate'] = cls.rtk_baud
				eeprom.set_gnss(cfg_gnss)


			cfg_sys = eeprom.get_sys()
			cfg_sys['Speed Units'] = cls.speed_units_dict[cls._speed_units_index][0].lower()
			cfg_sys['Accel Units'] = cls.accel_units_dict[cls._accel_units_index][0].lower()
			cfg_sys['Screen Brightness'] = cls.screen_brightness
			eeprom.set_sys(cfg_sys)

			cfg_log = eeprom.get_logging()
			cfg_log['Mode'] = cls.log_mode_dict[cls._log_mode_index][0]
			eeprom.set_logging(cfg_log)


		@classmethod
		def save_settings_to_file(cls):
			#TODO: use eeprom for basic settings instead?
			file_to_save = {
				"speed_units": cls._speed_units_index,
				"accel_units": cls._accel_units_index,
				"log_mode": cls._log_mode_index,
				"screen_brightness": cls._screen_brightness_index,
				"rtk_baud": cls._rtk_baud_index
			}
			try:
				with open('/sd/app_settings.json', 'w+') as file:
					json.dump(file_to_save, file)
			except OSError:
				pass #No SD Card

	class Icons:
		buttons = None
		# Convenience for common icons #
		gnss = None
		media = None
		logging = None
		next_page = None
		previous_page = None


		@classmethod
		def get_picture_button(cls, name):
			try:
				pb = next(pb for pb in cls.buttons if pb.name == name)
			except:
				pb = None
			return pb

		@classmethod
		def set_gnss_btn_state(cls, colour=None):
			if colour:
				cls.gnss.set_colour(colour)
			else:
				fix_type = Application.Data.fix_type
				if (fix_type in [vbox.VBOX_FIXTYPE_2D,
								vbox.VBOX_FIXTYPE_3D,
								vbox.VBOX_FIXTYPE_GNSS_DR,
								vbox.VBOX_FIXTYPE_DGNSS]):
					cls.gnss.set_colour(gui.DL_COLOR_RGB(0, 255, 0))
				elif (fix_type == vbox.VBOX_FIXTYPE_RTK_FLOAT):
					cls.gnss.set_colour(gui.DL_COLOR_RGB(255, 128, 0))
				elif (fix_type == vbox.VBOX_FIXTYPE_RTK_FIXED):
					cls.gnss.set_colour( gui.DL_COLOR_RGB(255, 0, 255))
				else:
					cls.gnss.set_colour( gui.DL_COLOR_RGB(255, 0, 0))

		@classmethod
		def set_media_btn_state(cls, colour=None):
			if colour:
				cls.media.set_colour(colour)
			else:
				if Application.sd_card_ok():
					cls.media.set_colour(gui.DL_COLOR(Colours.green))
				else:
					cls.media.set_colour(gui.DL_COLOR(Colours.red))

		@classmethod
		def set_logging_btn_state(cls, colour=None):
			if colour:
				cls.logging.set_colour(colour)
			else:
				if Application.logging_active():
					cls.logging.set_colour(gui.DL_COLOR(Colours.red))
				else:
					# cls.logging.set_colour(gui.DL_COLOR(Colours.rl_header))
					cls.logging.hide()

	################ MAIN APP RUN LOOP ###############
	@classmethod
	def run_loop(cls, use_priority = False):
		for task in cls.background_tasks:
			if not task.thread:
				task.initialise()

		if use_priority:
			# UNLESS YOU NEED PRIORTY SCHEDULING, DO NOT USE
			# If timings are not correct then tasks can easily be continuously blocked
			active_task = None
			while True:
				for task in cls.background_tasks:
					if task.state == Task.READY:
						if (active_task == None) or (task.priority <= active_task.priority):
							active_task = task
					elif task.state == Task.BLOCKED:
						task.evaluate_state()
					elif task.state == Task.RUNNING:
						if (active_task == None) or \
							(task.priority <= active_task.priority):
							active_task = task
						else:
							task.state = Task.READY

				if active_task:
					try:
						active_task.run()
					except StopIteration:
						cls.background_tasks.remove(active_task)

					active_task = None

		else:

			while True:
				for task in cls.background_tasks:
					if task.state < 2: #READY | RUNNING
						try:
							task.run()
						except StopIteration:
							cls.background_tasks.remove(task)
					else:
						task.evaluate_state()


	@classmethod
	def add_task(cls, task, init=True):
		if not isinstance(task, Task):
			raise ApplicationError("Tasks must be of type 'Task' from Application Module ")
		if init:
			task.initialise()
		cls.background_tasks.append(task)

	@classmethod
	def remove_task(cls, task):
		try:
			cls.background_tasks.remove(task)
		except ValueError:
			pass

	@classmethod
	def get_task_by_name(cls, name):
		try:
			task = next(task for task in cls.background_tasks if task.name == name)
		except Exception:
			task = None
		return task

	##################################################
	@classmethod
	def set_rtc(cls, sample):
		time = cls.tod_to_hmsm(sample.tod_ms)
		rtc = vts.clock_get()
		rtc['year'] = sample.year
		rtc['month'] = sample.month
		rtc['day of month'] = sample.day
		rtc['hours'] = time[0]
		rtc['minutes'] = time[1]
		rtc['seconds'] = time[2]
		rtc['day of week'] = 0
		try:
			vts.clock_set(rtc)
			Application.rtc_validated = True
		except RuntimeError:
			pass

	@staticmethod
	def tod_to_hmsm(ms):
		hmsm = 4 * [None]
		hmsm[0] = ms // 3600000
		ms -= (hmsm[0] * 3600000)
		hmsm[1] = ms // 60000
		ms -= (hmsm[1] * 60000)
		hmsm[2] = ms // 1000
		ms -= (hmsm[2] * 1000)
		hmsm[3] = ms
		return hmsm

	@classmethod
	def sd_callback(cls, state):
		cls.update_logging_status()

	@classmethod
	def sd_card_ok(cls):
		return (vts.get_sdcard_state() == 2)

	@classmethod
	def start_logging(cls):
		try:
			if not (cls.logging_active()) and cls.sd_card_ok():
				vbo.start()
		except OSError:
			#Should never get here!
			print("OS ERROR - LOGGING ABORTED")

	@classmethod
	def pause_logging(cls, pause):
		vbo.pause(pause)

	@classmethod
	def stop_logging(cls):
		vbo.stop()

	@classmethod
	def logging_active(cls):
		from vbo import get_status, VBO_STATUS_OPENING, VBO_STATUS_READY, VBO_STATUS_PAUSED
		status = get_status()
		if (status & (VBO_STATUS_OPENING | VBO_STATUS_READY) != 0):
			if (status & VBO_STATUS_PAUSED == 0):
				return True
		return False

	@classmethod
	def logging_paused(cls):
		from vbo import get_status, VBO_STATUS_PAUSED
		return True if (get_status() & (VBO_STATUS_PAUSED)==(VBO_STATUS_PAUSED)) else False

	@classmethod
	def logging_file_open(cls):
		from vbo import get_status, VBO_STATUS_READY
		return True if (get_status() & (VBO_STATUS_READY) == VBO_STATUS_READY) else False

	@classmethod
	def toggle_logging(cls, l=None):
		cls.UserSettings.log_mode = cls.UserSettings.LOG_MANUAL
		if (cls.logging_active()):
			# Currently logging is active so close the current file and stop logging
			cls.stop_logging()
		elif (cls.logging_file_open()):
			# Currently logging is paused so close the current file and restart logging
			cls.stop_logging()
			vts.delay_ms(100) #safety to allow for closing and reopening file
			cls.start_logging()
		else:
			# Not currently logging so start if media is available
			cls.start_logging()

	@classmethod
	def update_logging_status(cls):
		if not cls.sd_card_ok():
			if cls.logging_file_open():
				cls.stop_logging()
			return

		log_mode = cls.UserSettings.log_mode
		if log_mode == cls.UserSettings.LOG_WHEN_MOVING:

			if cls.gnss_valid:
				if not cls.logging_active() and not (cls.vehicle_static):
					cls.start_logging() if (not cls.logging_paused()) else cls.pause_logging(False)

				elif cls.logging_active() and not cls.sd_card_ok():
					cls.stop_logging()

				else:
					if (cls.logging_active()) and (abs(cls.state_period_ms) > 3000):
						#check logging active and stationary for more than 3 seconds
						cls.pause_logging(True)

		elif log_mode == cls.UserSettings.LOG_CONTINUOUS:
			if not cls.logging_file_open() and cls.gnss_valid:
				cls.start_logging()

		elif log_mode == cls.UserSettings.LOG_MANUAL:
			pass #Don't try to control logging when in manual mode

		else: #Unknown logging mode, set to default
			cls.UserSettings.set_log_mode(cls.UserSettings.LOG_CONTINUOUS)
			cls.update_logging_status()

	@staticmethod
	def coldstart_cb(l=None):
		vts.leds(*[0,20,0]*4)
		vbo.set_coldstart()
		coldstart()
		vts.delay_ms(250)
		vts.leds(*[0]*12)

class Task():
	RUNNING   = 0   # Currently executing on the processor
	READY     = 1   # Ready to run but task of higher or equal priority is currently running
	BLOCKED   = 2   # Task is waiting for some condition to be met to move to READY state
	PENDING   = 3   # Task READY but was blocked from running last time

	def __init__(self, func, priority=255, name="Anonymous", frequency=0, wake_flag=None):
		self.func = func
		self.priority = priority
		self.name = name
		self._in_messages = []
		self.state = self.READY
		self.wake_flag = wake_flag
		self.frequency = frequency
		self.evaluate_state = self.set_run_state()
		self.period = vts.MChrono.time()
		self.thread = None  # This is for the generator object

	def set_run_state(self):
		if self.frequency and self.wake_flag:
			raise ApplicationError("Task cannot use frequency and flags - select only one")

		if self.frequency:
			return self.run_at_frequency
		elif self.wake_flag:
			return self.wait_for_flag
		else:
			return self.run_always

	def wait_for_flag(self):
		#Use 'Wake Up Task' to set this task's state to ready
		if self.state == self.RUNNING:
			self.state = self.BLOCKED

	def wake_up_task(self):
		self.state = self.READY

	def initialise(self):
		# This will get the generator for it, then it will start,
		# run its init code and then yield.
		self.thread = self.func()
		self.period = vts.MChrono.time()
		next(self.thread)

	def run_always(self):
		self.state = self.RUNNING

	def run_at_frequency(self):
		#TODO: CHeck what happens when MChrono Wraps around (50ish hours?)
		if vts.MChrono.time_since_ms(self.period) > self.frequency:
			if self.state != self.RUNNING:
				self.state = self.READY
		else:
			self.state = self.BLOCKED

	def run(self):
		# print("Running task: {}".format(self.name))
		self.state = self.RUNNING
		next(self.thread)
		self.period = vts.MChrono.time()
		self.evaluate_state()

	def run_now(self):
		self.run()


def default_cb(*args, **kwargs):
	pass