# -*- coding: utf-8 -*-
#
# AWL simulator - FUP widget
#
# Copyright 2016-2018 Michael Buesch <m@bues.ch>
#
# 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 2 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

from __future__ import division, absolute_import, print_function, unicode_literals
#from awlsim.common.cython_support cimport * #@cy
from awlsim.common.compat import *

from awlsim.gui.fup.fupdrawwidget import *
from awlsim.gui.fup.fupelemcontainerwidget import *
from awlsim.gui.fup.undo import *
from awlsim.gui.interfedit.interfwidget import *
from awlsim.gui.editwidget import *
from awlsim.gui.optimizerconfig import *
from awlsim.gui.util import *

from awlsim.common.blocker import *
from awlsim.fupcompiler import *
from awlsim.awloptimizer.awloptimizer import *


FUP_DEBUG = 0


class FupFactory(XmlFactory):
	FUP_VERSION = 0

	def parser_open(self, tag=None):
		self.inFup = False
		self.inGrids = False
		XmlFactory.parser_open(self, tag)

	def parser_beginTag(self, tag):
		blockTypeEdit = self.fupWidget.interf.blockTypeEdit
		interfModel = self.fupWidget.interf.interfView.model()
		drawWidget = self.fupWidget.edit.draw
		grid = drawWidget.grid
		if self.inFup:
			if self.inGrids:
				if tag.name == "grid":
					self.parser_switchTo(grid.factory(grid=grid))
					return
			else:
				if tag.name == "blockdecl":
					self.parser_switchTo(blockTypeEdit.factory(
						blockTypeWidget=blockTypeEdit))
					return
				if tag.name == "interface":
					self.parser_switchTo(interfModel.factory(
						model=interfModel))
					return
				if tag.name == "grids":
					self.inGrids = True
					return
		else:
			if tag.name == "FUP":
				version = tag.getAttrInt("version")
				zoom = tag.getAttrFloat("zoom", 1.0)
				if version != self.FUP_VERSION:
					raise self.Error("Unsupported FUP version. "
						"Got %d, but expected %d." % (
						version, self.FUP_VERSION))
				drawWidget.beginLoad()
				drawWidget.zoom = zoom
				self.inFup = True
				return
		XmlFactory.parser_beginTag(self, tag)

	def parser_endTag(self, tag):
		drawWidget = self.fupWidget.edit.draw
		if self.inFup:
			if self.inGrids:
				if tag.name == "grids":
					self.inGrids = False
					return
			else:
				if tag.name == "FUP":
					self.inFup = False
					drawWidget.finalizeLoad()
					self.parser_finish()
					return
		XmlFactory.parser_endTag(self, tag)

	def composer_getTags(self):
		childTags = []

		drawWidget = self.fupWidget.edit.draw
		grid = drawWidget.grid

		blockTypeEdit = self.fupWidget.interf.blockTypeEdit
		childTags.extend(blockTypeEdit.factory(
			blockTypeWidget=blockTypeEdit).composer_getTags())

		interfModel = self.fupWidget.interf.interfView.model()
		childTags.extend(interfModel.factory(
			model=interfModel).composer_getTags())

		childTags.append(self.Tag(name="grids",
					  tags=grid.factory(grid=grid).composer_getTags()))

		tags = [
			self.Tag(name="FUP",
				comment="Awlsim FUP/FBD source generated by awlsim-%s" % VERSION_STRING,
				attrs={
					"version"	: str(self.FUP_VERSION),
					"zoom"		: "%.1f" % float(drawWidget.zoom),
				},
				tags=childTags),
		]
		return tags

	def compose(self, *args, **kwargs):
		kwargs["stripWs"] = True
		return super(self.__class__, self).compose(*args, **kwargs)

class FupEditWidgetMenu(QMenu):
	configOpt = Signal()
	showAwlOpt = Signal()
	showAwl = Signal()
	showStlOpt = Signal()
	showStl = Signal()
	genCall = Signal()

	def __init__(self, parent=None):
		QMenu.__init__(self, parent)
		self.addAction("Configure &optimizer...",
			       lambda: self.configOpt.emit())
		self.addAction("Show AWL code (DE, optimized)...",
			       lambda: self.showAwlOpt.emit())
		self.addAction("Show AWL code (DE)...",
			       lambda: self.showAwl.emit())
		self.addAction("Show STL code (EN, optimized)...",
			       lambda: self.showStlOpt.emit())
		self.addAction("Show STL code (EN)...",
			       lambda: self.showStl.emit())
		self.addAction("Generate CALL template...",
			       lambda: self.genCall.emit())

class FupEditWidget(QWidget):
	def __init__(self, parent, interfWidget):
		QWidget.__init__(self, parent)
		self.setLayout(QGridLayout())
		self.layout().setContentsMargins(QMargins())

		self.__splitter = QSplitter(self)

		self.__leftWidget = QWidget(self)
		self.__leftWidget.setLayout(QVBoxLayout())
		self.__leftWidget.layout().setContentsMargins(QMargins())

		self.container = FupElemContainerWidget(self)
		self.__leftWidget.layout().addWidget(self.container)

		self.menu = FupEditWidgetMenu(self)

		self.menuButton = QPushButton("FUP/FBD compiler...", self)
		self.menuButton.setMenu(self.menu)
		self.__leftWidget.layout().addWidget(self.menuButton)

		self.__splitter.addWidget(self.__leftWidget)

		self.draw = FupDrawWidget(self, interfWidget)

		self.__drawScroll = QScrollArea(self)
		self.__drawScroll.setWidget(self.draw)
		self.__splitter.addWidget(self.__drawScroll)

		self.layout().addWidget(self.__splitter, 0, 0)

		self.draw.selectTipChanged.connect(self.__handleMouseTipChange)
		self.draw.moveTipChanged.connect(self.__handleMouseTipChange)

	def __handleMouseTipChange(self, x, y):
		# Scroll the draw area so that the
		# selection or element move is visible.
		self.__drawScroll.ensureVisible(x, y)

class FupWidget(QWidget):
	"""Main FUP/FBD widget."""

	diagramChanged = Signal()

	undoAvailableChanged = Signal(bool)
	redoAvailableChanged = Signal(bool)
	clipboardCopyAvailableChanged = Signal(bool)
	clipboardCutAvailableChanged = Signal(bool)
	clipboardPasteAvailableChanged = Signal(bool)

	def __init__(self, parent, getSymTabSourcesFunc):
		QWidget.__init__(self, parent)
		self.setLayout(QGridLayout())

		self.__undoStack = FupUndoStack(self)
		self.__undoStackUpdateTimer = QTimer(self)
		self.__undoStackUpdateTimer.setSingleShot(True)
		self.undoStackBlocked = Blocker()

		self.__getSymTabSourcesFunc = getSymTabSourcesFunc
		self.__source = FupSource(name = "Diagram 1")
		self.__needSourceUpdate = True

		self.splitter = QSplitter(Qt.Vertical)

		self.interf = AwlInterfWidget(self)
		self.splitter.addWidget(self.interf)

		self.edit = FupEditWidget(self, self.interf)
		self.splitter.addWidget(self.edit)

		self.layout().addWidget(self.splitter, 0, 0)

		self.interf.contentChanged.connect(self.diagramChanged)
		self.edit.draw.diagramChanged.connect(self.diagramChanged)
		self.edit.draw.selectionChanged.connect(self.__handleSelectionChange)
		self.edit.menu.configOpt.connect(self.__configureOptimizer)
		self.edit.menu.showAwlOpt.connect(self.__compileAndShowAwlOpt)
		self.edit.menu.showAwl.connect(self.__compileAndShowAwl)
		self.edit.menu.showStlOpt.connect(self.__compileAndShowStlOpt)
		self.edit.menu.showStl.connect(self.__compileAndShowStl)
		self.edit.menu.genCall.connect(self.__generateCallTemplate)
		self.diagramChanged.connect(self.__handleDiagramChange)
		self.__undoStackUpdateTimer.timeout.connect(self.__updateUndoStack)
		self.__undoStack.canUndoChanged.connect(self.undoAvailableChanged)
		self.__undoStack.canRedoChanged.connect(self.redoAvailableChanged)

	@property
	def allGrids(self):
		"""Get an iterator over all grids.
		Currently there's only one grid.
		"""
		for grid in (self.edit.draw.grid,):
			yield grid

	def regenAllUUIDs(self):
		"""Re-generate all UUIDs that belong to this FUP diagram.
		"""
		self.interf.regenAllUUIDs()
		for grid in self.allGrids:
			grid.regenAllUUIDs()

	def __handleDiagramChange(self):
		"""Handle a change in the FUP diagram or interface.
		"""
		self.__needSourceUpdate = True
		if self.undoStackBlocked:
			self.__undoStackUpdateTimer.stop()
		else:
			# Schedule an undo stack update.
			self.__undoStackUpdateTimer.start(0)

	def __updateUndoStack(self):
		"""Push an entry onto the undo stack.
		"""
		self.__undoStack.appendSourceChange(self.getSource())

	def __updateSource(self):
		# Generate XML
		try:
			xmlBytes = FupFactory(fupWidget=self).compose()
		except FupFactory.Error as e:
			raise AwlSimError("Failed to create FUP source: "
				"%s" % str(e))
		if FUP_DEBUG:
			print("Composed FUP XML:")
			print(xmlBytes.decode(FupFactory.XML_ENCODING))
		self.__source.sourceBytes = xmlBytes
		self.__needSourceUpdate = False

	def getSource(self):
		if self.__needSourceUpdate:
			try:
				self.__updateSource()
			except AwlSimError as e:
				printWarning(str(e))
		return self.__source

	def setSource(self, source, initUndoStack=True):
		self.__source = source.dup()
		self.__source.sourceBytes = b""

		# Parse XML
		if FUP_DEBUG:
			print("Parsing FUP XML:")
			print(source.sourceBytes.decode(FupFactory.XML_ENCODING))
		try:
			factory = FupFactory(fupWidget=self)
			factory.parse(source.sourceBytes)
		except FupFactory.Error as e:
			raise AwlSimError("Failed to parse FUP source: "
				"%s" % str(e))
		self.__needSourceUpdate = True
		if initUndoStack:
			self.__undoStack.initializeStack(self.getSource())

	def __configureOptimizer(self):
		grid = self.edit.draw.grid
		dlg = OptimizerConfigDialog(
				settingsContainer=grid.optSettingsCont.dup(),
				parent=self)
		if dlg.exec_() == QDialog.Accepted:
			grid.optSettingsCont = dlg.getSettingsContainer()
			self.diagramChanged.emit()

	def __compileAndShow(self, mnemonics, showCall, optimize=False):
		fupSource = self.getSource()
		try:
			symTabSources = self.__getSymTabSourcesFunc()
			optSettCont = None
			if not optimize:
				optSettCont = AwlOptimizerSettingsContainer(globalEnable=False)
			compiler = FupCompiler()
			blockAwlSource = compiler.compile(fupSource=fupSource,
							  symTabSources=symTabSources,
							  mnemonics=mnemonics,
							  optimizerSettingsContainer=optSettCont)
			if showCall:
				callAwlSource = compiler.generateCallTemplate()
		except AwlSimError as e:
			MessageBox.handleAwlSimError(self, "FUP compiler error", e)
			return
		dlg = EditDialog(self, readOnly=True, withHeader=False, withCpuStats=False)
		if showCall:
			title = "FUP/FBD CALL template - %s"
			awlSource = callAwlSource
		else:
			title = "Compiled FUP/FBD diagram - %s"
			awlSource = blockAwlSource
		dlg.setWindowTitle(title % self.__source.name)
		dlg.edit.setSource(awlSource)
		dlg.show()

	def __compileAndShowAwlOpt(self):
		self.__compileAndShow(S7CPUConfig.MNEMONICS_DE, showCall=False, optimize=True)

	def __compileAndShowAwl(self):
		self.__compileAndShow(S7CPUConfig.MNEMONICS_DE, showCall=False, optimize=False)

	def __compileAndShowStlOpt(self):
		self.__compileAndShow(S7CPUConfig.MNEMONICS_EN, showCall=False, optimize=True)

	def __compileAndShowStl(self):
		self.__compileAndShow(S7CPUConfig.MNEMONICS_EN, showCall=False, optimize=False)

	def __generateCallTemplate(self):
		self.__compileAndShow(S7CPUConfig.MNEMONICS_AUTO, showCall=True)

	def undoIsAvailable(self):
		return self.__undoStack.canUndo()

	def undo(self):
		self.__undoStack.undo()
		return True

	def redoIsAvailable(self):
		return self.__undoStack.canRedo()

	def redo(self):
		self.__undoStack.redo()
		return True

	def clipboardCopyIsAvailable(self):
		grid = self.edit.draw.grid
		return bool(grid) and bool(grid.selectedElems)

	def clipboardCopy(self):
		return self.edit.draw.clipboardCopy()

	def clipboardCutIsAvailable(self):
		return self.clipboardCopyIsAvailable()

	def clipboardCut(self):
		return self.edit.draw.clipboardCut()

	def clipboardPasteIsAvailable(self):
		grid = self.edit.draw.grid
		return bool(grid) and bool(grid.selectedCells)

	def clipboardPaste(self, text=None):
		if text:
			return False
		else:
			return self.edit.draw.clipboardPaste()

	def __handleSelectionChange(self, cellsAreSelected, elemsAreSelected, grid):
		self.clipboardCopyAvailableChanged.emit(bool(elemsAreSelected))
		self.clipboardCutAvailableChanged.emit(bool(elemsAreSelected))
		self.clipboardPasteAvailableChanged.emit(bool(cellsAreSelected))
