# encoding: utf-8

import yade.runtime
if not yade.runtime.hasDisplay:
	msg = "Connecting to DISPLAY at Yade startup failed, unable to activate the qt5 interface."
	import os
	if 'YADE_BATCH' in os.environ:
		msg += "\nDo not import qt when running in batch mode."
	raise ImportError(msg)

from PyQt5.QtGui import *
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

USE_QT_WEB_ENGINE = False
_WEB_BACKEND_AVAILABLE = True
try:
	from PyQt5 import QtWebKit, QtWebKitWidgets  # legacy backend
except Exception:
	try:
		from PyQt5 import QtWebEngineWidgets  # modern backend (needs separate package: python3-pyqt5.qtwebengine or PyQtWebEngine)
		USE_QT_WEB_ENGINE = True
		QtWebKit = None  # type: ignore
		QtWebKitWidgets = None  # type: ignore
	except Exception as _qt_web_import_err:
		# Final graceful fallback: no in-app web view available
		QtWebEngineWidgets = None  # type: ignore
		QtWebKit = None  # type: ignore
		QtWebKitWidgets = None  # type: ignore
		_WEB_BACKEND_AVAILABLE = False
		import warnings
		warnings.warn(
		        "Neither QtWebKit nor QtWebEngine backends are available. Help/documentation windows will open in the external browser.\n"
		        "Install one of: 'python3-pyqt5.qtwebengine' (Debian/Ubuntu), or via pip: 'pip install PyQt5 PyQtWebEngine'. Original error: %s" %
		        _qt_web_import_err,
		        RuntimeWarning,
		)

from yade.qt.ui_controller import Ui_Controller

from yade.qt.Inspector import *
from yade.qt._GLViewer import *
from yade import *
import yade.system, yade.config

maxWebWindows = 1
"Number of webkit windows that will be cycled to show help on clickable objects"
webWindows = []
"holds instances of QtWebKit windows; clicking an url will open it in the window that was the least recently updated"
sphinxOnlineDocPath = 'https://www.yade-dem.org/doc/'
"Base URL for the documentation. Packaged versions should change to the local installation directory."

import os.path
# find if we have docs installed locally from package
sphinxLocalDocPath = yade.config.prefix + '/share/doc/yade' + yade.config.suffix + '-doc/html/'
sphinxBuildDocPath = yade.config.sourceRoot + '/doc/sphinx/_build/html/'
# we prefer the packaged documentation for this version, if installed
if os.path.exists(sphinxLocalDocPath + '/index.html'):
	sphinxPrefix = 'file://' + sphinxLocalDocPath
	# otherwise look for documentation generated in the source tree
elif os.path.exists(sphinxBuildDocPath + '/index.html'):
	sphinxPrefix = 'file://' + sphinxBuildDocPath
	# fallback to online docs
else:
	sphinxPrefix = sphinxOnlineDocPath

sphinxDocWrapperPage = sphinxPrefix + '/yade.wrapper.html'


def sslErrorHandler(reply, errorList):
	reply.ignoreSslErrors()


def openUrl(url):
	global maxWebWindows, webWindows
	# If no backend, fallback to system browser
	if not _WEB_BACKEND_AVAILABLE or (QtWebEngineWidgets is None and QtWebKitWidgets is None):
		import webbrowser
		webbrowser.open(url)
		return

	reuseLast = False
	try:
		reuseLast = (len(webWindows) > 0 and str(webWindows[-1].url()).split('#')[-1].split('.')[2] == url.split('#')[-1].split('.')[2])
	except Exception:
		pass
	if not reuseLast:
		if len(webWindows) < maxWebWindows:
			if USE_QT_WEB_ENGINE and QtWebEngineWidgets is not None:
				webWindows.append(QtWebEngineWidgets.QWebEngineView())
			else:
				# fallback to QtWebKit if present
				if QtWebKitWidgets is not None:
					webWindows.append(QtWebKitWidgets.QWebView())
				else:  # ultimate fallback: external browser
					import webbrowser
					webbrowser.open(url)
					return
		else:
			webWindows = webWindows[1:] + [webWindows[0]]
	web = webWindows[-1]
	# Guard: some minimalistic builds might lack networkAccessManager (QtWebEngine differences)
	try:
		web.page().networkAccessManager().sslErrors.connect(sslErrorHandler)  # type: ignore
	except Exception:
		pass
	web.load(QUrl(url))
	web.setWindowTitle(url)
	web.setFocus()
	web.show()


controller = None


class ControllerClass(QWidget, Ui_Controller):

	def __init__(self, parent=None):
		QWidget.__init__(self)
		self.setupUi(self)
		self.generator = None  # updated automatically
		self.renderer = Renderer()  # only hold one instance, managed by OpenGLManager
		self.addPreprocessors()
		self.addRenderers()
		global controller
		controller = self
		self.refreshTimer = QtCore.QTimer()
		self.refreshTimer.timeout.connect(self.refreshEvent)
		self.refreshTimer.start(200)
		self.iterPerSecTimeout = 1  # how often to recompute the number of iterations per second
		self.iterTimes, self.iterValues, self.iterPerSec = [], [], 0  # arrays to keep track of the simulation speed
		self.dtEditUpdate = True  # to avoid updating while being edited
		# show off with this one as well now
	def addPreprocessors(self):
		self.generatorCombo.addItem('TriaxialTest')  # this generator is the first one (reproducible screenshot), and is always present.
		for c in yade.system.childClasses('FileGenerator'):
			if (c != 'TriaxialTest'):
				self.generatorCombo.addItem(c)

	def addRenderers(self):
		self.displayCombo.addItem('OpenGLRenderer')
		afterSep = 1
		for bc in ('GlShapeFunctor', 'GlStateFunctor', 'GlBoundFunctor', 'GlIGeomFunctor', 'GlIPhysFunctor'):
			if afterSep > 0:
				self.displayCombo.insertSeparator(10000)
				afterSep = 0
			for c in yade.system.childClasses(bc) | set([bc]):
				inst = eval(c + '()')
				if len(set(inst.dict().keys()) - set(['label'])) > 0:
					self.displayCombo.addItem(c)
					afterSep += 1

	def inspectSlot(self):
		self.inspector = SimulationInspector(parent=None)
		self.inspector.show()

	def setTabActive(self, what):
		if what == 'simulation':
			ix = 0
		elif what == 'display':
			ix = 1
		elif what == 'generator':
			ix = 2
		elif what == 'python':
			ix = 3
		else:
			raise ValueErorr("No such tab: " + what)
		self.controllerTabs.setCurrentIndex(ix)

	def generatorComboSlot(self, genStr):
		"update generator parameters when a new one is selected"
		gen = eval(str(genStr) + '()')
		self.generator = gen
		se = SerializableEditor(gen, parent=self.generatorArea, showType=True)
		self.generatorArea.setWidget(se)

	def pythonComboSlot(self, cmd):
		try:
			code = compile(str(cmd), '<UI entry>', 'exec')
			exec(code, globals())
		except:
			import traceback
			traceback.print_exc()

	def generateSlot(self):
		filename = str(self.generatorFilenameEdit.text())
		if self.generatorMemoryCheck.isChecked():
			filename = ':memory:' + filename
			print('BUG: Saving to memory slots freezes Yade (cause unknown). Cross fingers.')
		#print 'Will save to ',filename
		self.generator.generate(filename)
		if self.generatorAutoCheck:
			O.load(filename)
			self.setTabActive('simulation')
			if len(views()) == 0:
				v = View()
				v.center()

	def displayComboSlot(self, dispStr):
		ser = (self.renderer if dispStr == 'OpenGLRenderer' else eval(str(dispStr) + '()'))
		path = 'yade.qt.Renderer()' if dispStr == 'OpenGLRenderer' else dispStr
		se = SerializableEditor(ser, parent=self.displayArea, ignoredAttrs=set(['label']), showType=True, path=path)
		verticalFiller = QVBoxLayout()
		verticalFiller.addStretch(1000)
		se.layout().addRow(verticalFiller)
		self.displayArea.setWidget(se)

	def loadSlot(self):
		f = QFileDialog.getOpenFileName(self, 'Load simulation', '', 'Yade simulations (*.xml *.xml.bz2 *.xml.gz *.yade *.yade.gz *.yade.bz2);; *.*')
		f = str(f[0])
		if not f:
			return  # cancelled
		self.deactivateControls()
		O.load(f)

	def saveSlot(self):
		f = QFileDialog.getSaveFileName(self, 'Save simulation', '', 'Yade simulations (*.xml *.xml.bz2 *.xml.gz *.yade *.yade.gz *.yade.bz2);; *.*')
		f = str(f[0])
		splf = f.split('.')
		if (len(splf) == 1):
			print('No extension is found in the file name! Added .xml.bz2')
			f = f + '.xml.bz2'
		if not f:
			return  # cancelled
		O.save(f)

	def reloadSlot(self):
		self.deactivateControls()
		from yade import plot
		plot.splitData()
		O.reload()

	def dtFixedSlot(self):
		O.dt = O.dt
		O.dynDt = False

	def dtDynSlot(self):
		O.dt = -O.dt

	def dtEditNoupdateSlot(self):
		self.dtEditUpdate = False

	def dtEditedSlot(self):
		try:
			t = float(self.dtEdit.text())
			O.dt = t
		except ValueError:
			pass
		self.dtEdit.setText(str(O.dt))
		self.dtEditUpdate = True

	def playSlot(self):
		O.run()

	def pauseSlot(self):
		O.pause()

	def stepSlot(self):
		O.step()

	def subStepSlot(self, value):
		O.subStepping = bool(value)

	def show3dSlot(self, show):
		vv = views()
		assert (len(vv) in (0, 1))
		if show:
			if len(vv) == 0:
				View()
		else:
			if len(vv) > 0:
				vv[0].close()

	def setReferenceSlot(self):
		# sets reference periodic cell as well
		utils.setRefSe3()

	def centerSlot(self):
		for v in views():
			v.center()

	def setViewAxes(self, dir, up):
		try:
			v = views()[0]
			v.viewDir = dir
			v.upVector = up
			v.center()
		except IndexError:
			pass

	def xyzSlot(self):
		self.setViewAxes((0, 0, -1), (0, 1, 0))

	def yzxSlot(self):
		self.setViewAxes((-1, 0, 0), (0, 0, 1))

	def zxySlot(self):
		self.setViewAxes((0, -1, 0), (1, 0, 0))

	def refreshEvent(self):
		self.refreshValues()
		self.activateControls()

	def deactivateControls(self):
		self.realTimeLabel.setText('')
		self.virtTimeLabel.setText('')
		self.iterLabel.setText('')
		self.fileLabel.setText('<i>[loading]</i>')
		self.playButton.setEnabled(False)
		self.pauseButton.setEnabled(False)
		self.stepButton.setEnabled(False)
		self.subStepCheckbox.setEnabled(False)
		self.reloadButton.setEnabled(False)
		self.dtFixedRadio.setEnabled(False)
		self.dtDynRadio.setEnabled(False)
		self.dtEdit.setEnabled(False)
		self.dtEdit.setText('')
		self.dtEditUpdate = True

	def activateControls(self):
		hasSim = len(O.engines) > 0
		running = O.running
		if hasSim:
			self.playButton.setEnabled(not running)
			self.pauseButton.setEnabled(running)
			self.reloadButton.setEnabled(O.filename is not None)
			self.stepButton.setEnabled(not running)
			self.subStepCheckbox.setEnabled(not running)
		else:
			self.playButton.setEnabled(False)
			self.pauseButton.setEnabled(False)
			self.reloadButton.setEnabled(False)
			self.stepButton.setEnabled(False)
			self.subStepCheckbox.setEnabled(False)
		self.dtFixedRadio.setEnabled(True)
		self.dtDynRadio.setEnabled(O.dynDtAvailable)
		dynDt = O.dynDt
		self.dtFixedRadio.setChecked(not dynDt)
		self.dtDynRadio.setChecked(dynDt)
		if dynDt or self.dtEditUpdate:
			self.dtEdit.setText(str(O.dt))
		if dynDt:
			self.dtEditUpdate = True
		self.dtEdit.setEnabled(not dynDt)
		fn = O.filename
		self.fileLabel.setText(fn if fn else '<i>[no file]</i>')

	def refreshValues(self):
		rt = int(O.realtime)
		t = O.time
		iter = O.iter
		assert (len(self.iterTimes) == len(self.iterValues))
		if len(self.iterTimes) == 0:
			self.iterTimes.append(rt)
			self.iterValues.append(iter)
			self.iterPerSec = 0  # update always for the first time
		elif rt - self.iterTimes[-1] > self.iterPerSecTimeout:  # update after a timeout
			if len(self.iterTimes) == 1:
				self.iterTimes.append(self.iterTimes[0])
				self.iterValues.append(self.iterValues[0])  # 2 values, first one is bogus
			self.iterTimes[0] = self.iterTimes[1]
			self.iterValues[0] = self.iterValues[1]
			self.iterTimes[1] = rt
			self.iterValues[1] = iter
			self.iterPerSec = (self.iterValues[-1] - self.iterValues[-2]) / (self.iterTimes[-1] - self.iterTimes[-2])
		if not O.running:
			self.iterPerSec = 0
		stopAtIter = O.stopAtIter
		subStepInfo = ''
		if O.subStepping:
			subStep = O.subStep
			if subStep == -1:
				subStepInfo = u'→ <i>prologue</i>'
			elif subStep >= 0 and subStep < len(O.engines):
				e = O.engines[subStep]
				subStepInfo = u'→ %s' % (e.label if e.label else e.__class__.__name__)
			elif subStep == len(O.engines):
				subStepInfo = u'→ <i>epilogue</i>'
			else:
				raise RuntimeError("Invalid O.subStep value %d, should be ∈{-1,…,len(o.engines)}" % subStep)
			subStepInfo = "<br><small>sub %d/%d [%s]</small>" % (subStep, len(O.engines), subStepInfo)
		self.subStepCheckbox.setChecked(O.subStepping)  # might have been changed async
		if stopAtIter <= iter:
			self.realTimeLabel.setText('%02d:%02d:%02d' % (rt // 3600, (rt % 3600) // 60, rt % 60))
			self.iterLabel.setText('#%ld, %.1f/s %s' % (iter, self.iterPerSec, subStepInfo))
		else:
			self.realTimeLabel.setText('%02d:%02d:%02d' % (rt // 3600, (rt % 3600) // 60, rt % 60))
			self.iterLabel.setText('#%ld / %ld, %.1f/s %s' % (O.iter, stopAtIter, self.iterPerSec, subStepInfo))
		if t != float('inf'):
			s = int(t)
			ms = int(t * 1000) % 1000
			us = int(t * 1000000) % 1000
			ns = int(t * 1000000000) % 1000
			self.virtTimeLabel.setText(u'%03ds%03dm%03dμ%03dn' % (s, ms, us, ns))
		else:
			self.virtTimeLabel.setText(u'[ ∞ ] ?!')
		self.show3dButton.setChecked(len(views()) > 0)


def Generator():
	global controller
	if not controller:
		controller = ControllerClass()
	controller.show()
	controller.raise_()
	controller.setTabActive('generator')


def Controller():
	global controller
	if not controller:
		controller = ControllerClass()
	controller.show()
	controller.raise_()
	controller.setTabActive('simulation')


def Inspector():
	global controller
	if not controller:
		controller = ControllerClass()
	controller.inspectSlot()
