# -*- coding: utf-8 -*-
# FlashGBX
# Author: Lesserkuma (github.com/Lesserkuma)

import zipfile, serial, struct, time, random, hashlib, datetime
try:
	from . import Util
except ImportError:
	import Util

class FirmwareUpdater():
	PORT = ""

	def __init__(self, app_path=".", port=None):
		self.APP_PATH = app_path
		self.PORT = port

	def WriteFirmware(self, zipfn, fncSetStatus):
		with zipfile.ZipFile(zipfn) as archive:
			with archive.open("fw.ini") as f: buffer1 = bytearray(f.read())
			with archive.open("fw.bin") as f: buffer2 = bytearray(f.read())
		while len(buffer1) < len(buffer2): buffer1 = buffer1 + buffer1
		random.seed(struct.unpack("<I", buffer2[-0x18:-0x14])[0])
		chk = buffer2[-0x14:]
		buffer = bytearray()
		for i in range(0, len(buffer2[0:-0x18])):
			r = int(random.random()*256) % 256
			buffer.append(buffer2[0:-0x18][len(buffer2[0:-0x18]) - i - 1] ^ r ^ buffer1[len(buffer1) - i - 1])
		if (chk != hashlib.sha1(buffer).digest()):
			fncSetStatus("The firmware update file is corrupted.")
			return 3
		
		if self.PORT is None:
			ports = []
			comports = serial.tools.list_ports.comports()
			for i in range(0, len(comports)):
				if comports[i].vid == 0x1A86 and comports[i].pid == 0x7523:
					ports.append(comports[i].device)
			if len(ports) == 0:
				fncSetStatus("No device found.")
				return 2
			port = ports[0]
		else:
			port = self.PORT
		data = buffer
		buffer = bytearray()
		
		fncSetStatus(text="Connecting...")
		try:
			dev = serial.Serial(port=port, baudrate=57600, timeout=1)
		except:
			fncSetStatus(text="Device not accessible.", enableUI=True)
			return 2
		dev.reset_input_buffer()

		# Write firmware
		fncSetStatus("Updating firmware...", setProgress=0)
		
		size = len(data)
		counter = 0
		while counter < size:
			byte = data[counter:counter+1]
			dev.write(byte)
			tmp_byte = dev.read(1)
			if (tmp_byte != byte):
				try:
					tmp_byte = int.from_bytes(tmp_byte, byteorder="little")
				except:
					tmp_byte = 0
				byte = int.from_bytes(byte, byteorder="little")
				if counter == 0:
					fncSetStatus(text="Update failed!", enableUI=True)
				else:
					fncSetStatus(text="Update failed at offset 0x{:04X}!".format(counter), enableUI=True)
				return 2
			
			counter += 1
			percent = float(counter)/size*100
			fncSetStatus(text="Updating firmware... Do not unplug the device!", setProgress=percent)

		dev.close()
		time.sleep(0.8)
		fncSetStatus("Done.")
		time.sleep(0.2)
		return 1

try:
	from .pyside import QtCore, QtWidgets, QtGui, QDesktopWidget
	class FirmwareUpdaterWindow(QtWidgets.QDialog):
		APP = None
		DEVICE = None
		FWUPD = None
		DEV_NAME = "GBxCart RW"
		FW_VER = ""
		PCB_VER = ""

		def __init__(self, app, app_path, file=None, icon=None, device=None):
			QtWidgets.QDialog.__init__(self)
			if icon is not None: self.setWindowIcon(QtGui.QIcon(icon))
			self.setStyleSheet("QMessageBox { messagebox-text-interaction-flags: 5; }")
			self.setWindowTitle("FlashGBX – Firmware Updater for GBxCart RW")
			self.setWindowFlags((self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint) & ~QtCore.Qt.WindowContextHelpButtonHint)

			self.APP = app
			if device is not None:
				self.FWUPD = FirmwareUpdater(app_path, device.GetPort())
				self.DEV_NAME = device.GetName()
				self.FW_VER = device.GetFirmwareVersion(more=True)
				self.PCB_VER = device.GetPCBVersion()
				self.DEVICE = device
			else:
				self.APP.QT_APP.processEvents()
				self.FWUPD = FirmwareUpdater(app_path, None)

			self.layout = QtWidgets.QGridLayout()
			self.layout.setContentsMargins(-1, 8, -1, 8)
			self.layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
			self.layout_device = QtWidgets.QVBoxLayout()
			
			# ↓↓↓ Current Device Information
			self.grpDeviceInfo = QtWidgets.QGroupBox("Current Firmware")
			self.grpDeviceInfo.setMinimumWidth(420)
			self.grpDeviceInfoLayout = QtWidgets.QVBoxLayout()
			self.grpDeviceInfoLayout.setContentsMargins(-1, 3, -1, -1)
			rowDeviceInfo1 = QtWidgets.QHBoxLayout()
			self.lblDeviceName = QtWidgets.QLabel("Device:")
			self.lblDeviceName.setMinimumWidth(120)
			self.lblDeviceNameResult = QtWidgets.QLabel("GBxCart RW")
			rowDeviceInfo1.addWidget(self.lblDeviceName)
			rowDeviceInfo1.addWidget(self.lblDeviceNameResult)
			rowDeviceInfo1.addStretch(1)
			self.grpDeviceInfoLayout.addLayout(rowDeviceInfo1)
			rowDeviceInfo3 = QtWidgets.QHBoxLayout()
			self.lblDeviceFWVer = QtWidgets.QLabel("Firmware version:")
			self.lblDeviceFWVer.setMinimumWidth(120)
			self.lblDeviceFWVerResult = QtWidgets.QLabel("R30+L1")
			rowDeviceInfo3.addWidget(self.lblDeviceFWVer)
			rowDeviceInfo3.addWidget(self.lblDeviceFWVerResult)
			rowDeviceInfo3.addStretch(1)
			self.grpDeviceInfoLayout.addLayout(rowDeviceInfo3)
			rowDeviceInfo2 = QtWidgets.QHBoxLayout()
			self.lblDevicePCBVer = QtWidgets.QLabel("PCB version:")
			self.lblDevicePCBVer.setMinimumWidth(120)
			self.optDevicePCBVer14 = QtWidgets.QRadioButton("v1.4")
			self.connect(self.optDevicePCBVer14, QtCore.SIGNAL("clicked()"), self.SetPCBVersion)
			self.optDevicePCBVer14a = QtWidgets.QRadioButton("v1.4a/b/c")
			self.connect(self.optDevicePCBVer14a, QtCore.SIGNAL("clicked()"), self.SetPCBVersion)
			rowDeviceInfo2.addWidget(self.lblDevicePCBVer)
			rowDeviceInfo2.addWidget(self.optDevicePCBVer14)
			rowDeviceInfo2.addWidget(self.optDevicePCBVer14a)
			rowDeviceInfo2.addStretch(1)
			self.grpDeviceInfoLayout.addLayout(rowDeviceInfo2)
			self.grpDeviceInfo.setLayout(self.grpDeviceInfoLayout)
			self.layout_device.addWidget(self.grpDeviceInfo)
			# ↑↑↑ Current Device Information
			
			# ↓↓↓ Available Firmware Updates
			self.grpAvailableFwUpdates = QtWidgets.QGroupBox("Available Firmware")
			self.grpAvailableFwUpdates.setMinimumWidth(400)
			self.grpAvailableFwUpdatesLayout = QtWidgets.QVBoxLayout()
			self.grpAvailableFwUpdatesLayout.setContentsMargins(-1, 3, -1, -1)

			rowDeviceInfo4 = QtWidgets.QHBoxLayout()
			self.lblDeviceFWVer2 = QtWidgets.QLabel("Firmware version:")
			self.lblDeviceFWVer2.setMinimumWidth(120)
			self.lblDeviceFWVer2Result = QtWidgets.QLabel("(Please choose the PCB version)")
			rowDeviceInfo4.addWidget(self.lblDeviceFWVer2)
			rowDeviceInfo4.addWidget(self.lblDeviceFWVer2Result)
			rowDeviceInfo4.addStretch(1)
			self.grpAvailableFwUpdatesLayout.addLayout(rowDeviceInfo4)

			self.rowUpdate = QtWidgets.QHBoxLayout()
			self.btnUpdate = QtWidgets.QPushButton("Install Firmware Update")
			self.btnUpdate.setMinimumWidth(200)
			self.btnUpdate.setContentsMargins(20, 20, 20, 20)
			self.connect(self.btnUpdate, QtCore.SIGNAL("clicked()"), lambda: [ self.UpdateFirmware() ])
			self.rowUpdate.addStretch()
			self.rowUpdate.addWidget(self.btnUpdate)
			self.rowUpdate.addStretch()

			self.grpAvailableFwUpdatesLayout.addSpacing(3)
			self.grpAvailableFwUpdatesLayout.addItem(self.rowUpdate)
			self.grpAvailableFwUpdates.setLayout(self.grpAvailableFwUpdatesLayout)
			self.layout_device.addWidget(self.grpAvailableFwUpdates)
			# ↑↑↑ Available Firmware Updates

			self.grpStatus = QtWidgets.QGroupBox("")
			self.grpStatusLayout = QtWidgets.QGridLayout()
			self.prgStatus = QtWidgets.QProgressBar()
			self.prgStatus.setMinimum(0)
			self.prgStatus.setMaximum(1000)
			self.prgStatus.setValue(0)
			self.lblStatus = QtWidgets.QLabel("Ready.")

			self.grpStatusLayout.addWidget(self.prgStatus, 1, 0)
			self.grpStatusLayout.addWidget(self.lblStatus, 2, 0)
			
			self.grpStatus.setLayout(self.grpStatusLayout)
			self.layout_device.addWidget(self.grpStatus)
			
			self.grpFooterLayout = QtWidgets.QHBoxLayout()
			self.btnClose = QtWidgets.QPushButton("&Close")
			self.connect(self.btnClose, QtCore.SIGNAL("clicked()"), lambda: [ self.reject() ])
			self.grpFooterLayout.addStretch()
			self.grpFooterLayout.addWidget(self.btnClose)
			self.layout_device.addItem(self.grpFooterLayout)

			self.layout.addLayout(self.layout_device, 0, 0)
			self.setLayout(self.layout)

			self.lblDeviceNameResult.setText(self.DEV_NAME)
			self.lblDeviceFWVerResult.setText(self.FW_VER)
			if self.PCB_VER == "v1.4":
				self.optDevicePCBVer14.setChecked(True)
				self.optDevicePCBVer14a.setEnabled(False)
			elif self.PCB_VER == "v1.4a/b/c":
				self.optDevicePCBVer14a.setChecked(True)
				self.optDevicePCBVer14.setEnabled(False)
			self.SetPCBVersion()
		
		def SetPCBVersion(self):
			if self.optDevicePCBVer14.isChecked():
				file_name = self.FWUPD.APP_PATH + "/res/fw_GBxCart_RW_v1_4.zip"
			elif self.optDevicePCBVer14a.isChecked():
				file_name = self.FWUPD.APP_PATH + "/res/fw_GBxCart_RW_v1_4a.zip"
			else:
				return

			with zipfile.ZipFile(file_name) as zip:
				with zip.open("fw.ini") as f: ini_file = f.read()
				ini_file = ini_file.decode(encoding="utf-8")
				self.INI = Util.IniSettings(ini=ini_file, main_section="Firmware")
				self.OFW_VER = self.INI.GetValue("fw_ver")
				self.OFW_BUILDTS = self.INI.GetValue("fw_buildts")
				self.OFW_TEXT = self.INI.GetValue("fw_text")
			
			self.lblDeviceFWVer2Result.setText("{:s} ({:s})".format(self.OFW_VER, datetime.datetime.fromtimestamp(int(self.OFW_BUILDTS)).astimezone().replace(microsecond=0).isoformat()))

		def run(self):
			try:
				self.layout.update()
				self.layout.activate()
				screenGeometry = QDesktopWidget().screenGeometry(self)
				x = (screenGeometry.width() - self.width()) / 2
				y = (screenGeometry.height() - self.height()) / 2
				self.move(x, y)
				self.show()
			except:
				return
		
		def hideEvent(self, event):
			if self.DEVICE is None:
				self.APP.ConnectDevice()
			self.APP.activateWindow()
		
		def reject(self):
			if self.CloseDialog():
				super().reject()
		
		def CloseDialog(self):
			if self.btnClose.isEnabled() is False:
				text = "<b>WARNING:</b> If you close this window while a firmware update is still running, it may leave the device in an unbootable state. You can still recover it by running the Firmware Updater again later.<br><br>Are you sure you want to close this window?"
				msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
				msgbox.setDefaultButton(QtWidgets.QMessageBox.No)
				answer = msgbox.exec()
				if answer == QtWidgets.QMessageBox.No: return False
			return True
		
		def UpdateFirmware(self):
			if self.optDevicePCBVer14.isChecked():
				device_name = "v1.4"
				file_name = self.FWUPD.APP_PATH + "/res/fw_GBxCart_RW_v1_4.zip"
				led = "Done"
			elif self.optDevicePCBVer14a.isChecked():
				device_name = "v1.4a/b/c"
				file_name = self.FWUPD.APP_PATH + "/res/fw_GBxCart_RW_v1_4a.zip"
				led = "Status"
			else:
				msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="Please select the PCB version of your GBxCart RW device.", standardButtons=QtWidgets.QMessageBox.Ok)
				answer = msgbox.exec()
				return False

			self.APP.DisconnectDevice()

			text = "Please follow these steps to proceed with the firmware update:<ol><li>Disconnect the USB cable of your GBxCart RW {dev_name:s} device.</li><li>On the circuit board of your GBxCart RW {dev_name:s}, press and hold down the small button while connecting the USB cable again.</li><li>Keep the small button held for at least 2 seconds, then let go of it. If done right, the green LED labeled “{led:s}” should remain lit.</li><li>Click OK to continue.</li></ol>".format(dev_name=device_name, led=led)
			msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
			msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
			msgbox.setTextFormat(QtCore.Qt.TextFormat.RichText)
			answer = msgbox.exec()
			if answer == QtWidgets.QMessageBox.Cancel: return
			self.btnUpdate.setEnabled(False)
			self.btnClose.setEnabled(False)
			self.optDevicePCBVer14.setEnabled(False)
			self.optDevicePCBVer14a.setEnabled(False)
			
			while True:
				ret = self.FWUPD.WriteFirmware(file_name, self.SetStatus)
				if ret == 1: 
					text = "The firmware update is complete!"
					self.btnUpdate.setEnabled(True)
					self.btnClose.setEnabled(True)
					self.optDevicePCBVer14.setEnabled(True)
					self.optDevicePCBVer14a.setEnabled(True)
					msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Ok)
					answer = msgbox.exec()
					self.DEVICE = None
					self.reject()
					return True
				elif ret == 2:
					text = "The firmware update has failed. Please try again."
					self.btnUpdate.setEnabled(True)
					self.btnClose.setEnabled(True)
					self.optDevicePCBVer14.setEnabled(True)
					self.optDevicePCBVer14a.setEnabled(True)
					msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Ok)
					answer = msgbox.exec()
					return False
				elif ret == 3:
					text = "The firmware update file is corrupted. Please re-install the application."
					self.btnUpdate.setEnabled(True)
					self.btnClose.setEnabled(True)
					self.optDevicePCBVer14.setEnabled(True)
					self.optDevicePCBVer14a.setEnabled(True)
					msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Ok)
					answer = msgbox.exec()
					return False
		
		def SetStatus(self, text, enableUI=False, setProgress=None):
			self.lblStatus.setText("Status: {:s}".format(text))
			if setProgress is not None:
				self.prgStatus.setValue(setProgress * 10)
			if enableUI:
				self.btnUpdate.setEnabled(True)
				self.btnClose.setEnabled(True)
				self.optDevicePCBVer14.setEnabled(True)
				self.optDevicePCBVer14a.setEnabled(True)
			self.APP.QT_APP.processEvents()
except ImportError:
	pass
