#!/usr/bin/python3

#	control++gui : GUI for the glorious control++ app
#	Copyright (C) 2024 Alexey Appolonov
#
#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program.  If not, see <http://www.gnu.org/licenses/>.

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

import kivy
kivy.require('2.0.0')
import os
from functools            import partial
from threading            import Thread
from kivy.app             import App
from kivy.properties      import ObjectProperty
from kivy.core.window     import Window
from kivy.uix.boxlayout   import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.popup       import Popup
from controlpp_intf.controlpp       import ControlppInterface
from controlpp_intf.mode            import DecodeModeId
from controlppgui_widgets.common    import GetWidget
from controlppgui_widgets.constants import CLR_WHITE, CLR_LIGHTBLUE
from controlppgui_widgets.errpopup  import ErrPopup
from controlppgui_widgets.modetable import ModeTable, UpdateModeTable, \
	TriggerSubModes, DeactivateAllModes, DetermineSelectedMode
from controlppgui_widgets.flags     import Flags, CheckBoxIDs
from controlppgui_widgets.log       import Log

WINDOW_WIDTH  = 1024
WINDOW_HEIGHT = 600

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

class Buttons(BoxLayout):
	pass

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

class SaveDialog(FloatLayout):

    save = ObjectProperty(None)
    text_input = ObjectProperty(None)
    cancel = ObjectProperty(None)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

class ControlppGUIApp(App):

	GUIDE = ('Select a mode from the table above (current mode is highlighted '
		'in bold), use the control buttons to perform operations with a '
		'selected mode. This text will be replaced with the control++ stdout '
		'and messages from the control++ GUI itself (in case of an error) as '
		'soon as control++ is started.')

	title = 'control++gui'
	return_code = 0
	mode_name = ''
	unit_name = ''
	mode_selection_in_progress = False

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def build(self):

		Window.size = (WINDOW_WIDTH, WINDOW_HEIGHT)
		Window.clearcolor = CLR_WHITE

		self.controlpp = ControlppInterface()
		self.modes, err = self.controlpp.GetModes()
		if err:
			self.return_code = 1
			return ErrPopup('Can\'t get a list of control++ modes: \n' + err)

		self.main = BoxLayout(orientation='vertical')
		self.main.add_widget(ModeTable(self.modes))
		self.main.add_widget(Flags())
		self.main.add_widget(Buttons())
		self.log = Log()
		self.log.Write(text=self.GUIDE)
		self.main.add_widget(self.log)

		# Bind events to methods
		GetWidget(self.main, 'control_set')     \
			.bind(on_press = self.SetMode)
		GetWidget(self.main, 'control_reset')   \
			.bind(on_press = self.ResetMode)
		GetWidget(self.main, 'control_check')   \
			.bind(on_press = self.CheckMode)
		GetWidget(self.main, 'control_savelog') \
			.bind(on_press = self.ShowSaveDialog)
		for mode in self.modes:
			widget_id = mode.GenerateId()
			w = GetWidget(self.main, widget_id)
			w.bind(state = partial(self.ModeButtonState, widget=w))

		return self.main

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def __OpeartionButtons(self, enable):

		for op in ('set', 'reset', 'check', 'savelog'):
			GetWidget(self.main, f'control_{op}').disabled = not enable

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def __ControlppCmdHandler(self, cmd):

		# Run the command and print the results
		out, err = cmd.Run()
		self.log.Write(text=(out if not err else ''), err=err)

		# Get current mode
		self.modes, err = self.controlpp.GetModes()
		if err:
			self.return_code = 1
			self.log.Write(err=f"Can\'t get a list of control++ modes: {err}")
			return

		# Update the mode selection table according to the current state
		if not UpdateModeTable(self.main, self.modes):
			self.return_code = 1
			self.log.Write(err=f"Can\'t update the mode selection table: {err}")
			return

		# Activate the operation buttons back again
		self.__OpeartionButtons(True)

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def __Run(self, cmd=None, mode_name=None, unit_name=None):

		for flag, widget_id in CheckBoxIDs():
			active = GetWidget(self.main, widget_id).active
			self.controlpp.SetFlag(flag, active)

		params = [param for param in (mode_name, unit_name) if param != None]

		cmd, err = cmd(*params)
		if err:
			self.log.Write(err=err)
			return

		self.log.Write(cmd=cmd.Stringify())

		# Deactivate the operation buttons while the control++ is running
		self.__OpeartionButtons(False)

		# Run the command asynchronously
		Thread(target=self.__ControlppCmdHandler, args=(cmd,)).start()

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def SetMode(self, *args):

		self.__Run(cmd=self.controlpp.SetModeCmd, mode_name=self.mode_name,
			unit_name=self.unit_name)

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def ResetMode(self, *args):

		self.__Run(cmd=self.controlpp.ResetModeCmd, unit_name=self.unit_name)

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def CheckMode(self, *args):

		self.__Run(cmd=self.controlpp.CheckModeCmd, unit_name=self.unit_name)

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def ModeButtonState(self, *args, widget=None, **kwargs):

		if self.mode_selection_in_progress:
			return

		self.mode_selection_in_progress = True

		mode, err = DecodeModeId(widget.id)
		if err:
			self.return_code = 1
			#return ErrPopup('Can\'t decode id of a mode button widget')
			self.log.Write(
				err=f'Can\'t decode id of a mode button widget: {err}')
			return

		if widget.state == 'down':
			DeactivateAllModes(self.main, self.modes, mode)
			if mode.unit_name == 'macro':
				TriggerSubModes(
					self.main, self.modes, mode.macro_mode_name, True)
		elif widget.state == 'normal':
			TriggerSubModes(self.main, self.modes, mode.macro_mode_name, False)

		self.mode_name, self.unit_name = \
			DetermineSelectedMode(self.main, self.modes)

		self.mode_selection_in_progress = False

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def __DismissPopup(self):

		self._popup.dismiss()

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def __SaveFile(self, path, filename):

		with open(os.path.join(path, filename), 'w') as stream:
			stream.write(self.log.GetText())
		self.__DismissPopup()

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def ShowSaveDialog(self, *args):

		content = SaveDialog(save=self.__SaveFile, cancel=self.__DismissPopup)
		self._popup = Popup(title='Save file', content=content,
			size_hint=(0.9, 0.9), separator_color=CLR_LIGHTBLUE)
		self._popup.open()

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

if __name__ == '__main__':

	app = ControlppGUIApp()
	app.run()

	exit(app.return_code)
