# -*- coding: utf-8 -*-
#
# AWL simulator - FUP - Grid classes
#
# Copyright 2016 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.compat import *

from awlsim.common.xmlfactory import *

from awlsim.gui.fup.fup_base import *
from awlsim.gui.fup.fup_wire import *
from awlsim.gui.fup.fup_elem import *
from awlsim.gui.fup.fup_elembool import *
from awlsim.gui.fup.fup_elemoperand import *


class FupGrid_factory(XmlFactory):
	def parser_open(self, tag=None):
		self.grid.clear()

		if tag:
			width = tag.getAttrInt("width")
			height = tag.getAttrInt("height")
			self.grid.resize(width, height)

		XmlFactory.parser_open(self, tag)

	def parser_beginTag(self, tag):
		if tag.name == "wires":
			self.parser_switchTo(FupWire.factory(grid=self.grid))
			return
		if tag.name == "elements":
			self.parser_switchTo(FupElem.factory(grid=self.grid))
			return
		XmlFactory.parser_beginTag(self, tag)

	def parser_endTag(self, tag):
		if tag.name == "grid":
			self.grid.removeOrphanWires()
			self.parser_finish()
			return
		XmlFactory.parser_endTag(self, tag)

	def composer_getTags(self):
		wireTags = []
		for wire in sorted(self.grid.wires, key=lambda w: w.idNum):
			wireTags.extend(wire.factory(wire=wire).composer_getTags())
		elemTags = []
		for elem in self.grid.elems:
			elemTags.extend(elem.factory(elem=elem).composer_getTags())
		return [
			self.Tag(name="grid",
				tags=[
					self.Tag(name="wires",
						 tags=wireTags),
					self.Tag(name="elements",
						 tags=elemTags),
				],
				attrs={
					"width" : str(self.grid.width),
					"height" : str(self.grid.height),
				}),
		]

class FupGrid(object):
	"""FUP/FBD element grid"""

	factory = FupGrid_factory

	# Resize event callback.
	# If this is a callable, it it called with (width, height) on resize events.
	resizeEvent = None

	def __init__(self, drawWidget, width, height):
		self.__drawWidget = drawWidget
		self.width = width
		self.height = height

		self.elems = []			# The FupElem_xxx()s in this grid
		self.wires = set()		# The FupConnIn/Out()s in this grid

		self.selectedElems = set()	# Set of selected elements in this grid
		self.expandedElems = set()	# Set of expanded elements in this grid
		self.clickedElem = None		# The recently clicked element in this grid
		self.clickedConn = None		# The recently clicked connection in this grid
		self.clickedArea = None		# The recently clicked area in this grid

	def clear(self):
		for wire in self.wires:
			wire.disconnectAll()
		self.wires.clear()
		self.elems = []
		self.selectedElems.clear()

	def resize(self, width, height):
		"""Resize the grid.
		Returns True, if the resize was successfull.
		"""
		if width != self.width or height != self.height:
			if self.elems:
				minWidth = max(e.x + e.width for e in self.elems)
				minHeight = max(e.y + e.height for e in self.elems)
			else:
				minWidth = minHeight = 0
			if width >= minWidth and height >= minHeight:
				self.width = width
				self.height = height
				if callable(self.resizeEvent):
					self.resizeEvent(width, height)
				return True
		return False

	@property
	def interfDef(self):
		"""Get the block interface definition (AwlInterfDef() instance).
		"""
		return self.__drawWidget.interfDef

	def getUnusedWireIdNum(self):
		"""Get an unused wire idNum.
		"""
		if self.wires:
			return max(w.idNum for w in self.wires) + 1
		return 0

	def addWire(self, wire):
		"""Add a wire to the grid.
		Does nothing, if the wire does exist already.
		"""
		self.wires.add(wire)

	def removeWire(self, wire):
		"""Remove a wire to the grid.
		Does nothing, if the wire does not exist.
		"""
		with contextlib.suppress(KeyError):
			self.wires.remove(wire)

	def getWireById(self, wireIdNum):
		"""Get a wire by its idNum.
		"""
		if wireIdNum >= 0:
			for wire in self.wires:
				if wire.idNum == wireIdNum:
					return wire
		return None

	def removeOrphanWires(self):
		"""Remove all unconnected wires.
		"""
		newWiresSet = set()
		while self.wires:
			wire = self.wires.pop()
			if wire.connections:
				newWiresSet.add(wire)
		self.wires = newWiresSet

	def renumberWires(self):
		"""Re-assign all wire idNums.
		"""
		for i, wire in enumerate(self.wires):
			wire.idNum = i

	@property
	def cellPixWidth(self):
		if self.__drawWidget:
			return self.__drawWidget.cellPixWidth
		return 0

	@property
	def cellPixHeight(self):
		if self.__drawWidget:
			return self.__drawWidget.cellPixHeight
		return 0

	def __haveCollision(self, x, y, height, excludeElems=set()):
		if x < 0 or x >= self.width or\
		   y < 0 or y >= self.height:
			# Position is not on grid.
			return True
		for yy in range(y, y + height):
			elem = self.getElemAt(x, yy)
			if elem in excludeElems:
				continue # Element is ignored.
			if elem:
				return True # Collision with other element.
		return False

	def placeElem(self, elem):
		# Check if we have a collision.
		if self.__haveCollision(elem.x, elem.y, elem.height):
			return False
		# Add the element.
		self.elems.append(elem)
		elem.grid = self
		return True

	def removeElem(self, elem):
		with contextlib.suppress(ValueError):
			self.elems.remove(elem)
		elem.breakConnections()
		with contextlib.suppress(ValueError):
			self.selectedElems.remove(elem)

	def moveElemTo(self, elem, toX, toY,
		       relativeCoords=False,
		       checkOnly=False,
		       excludeCheckElems=set()):
		"""Move elem to position (toX, toY).
		If relativeCoords=True, the (toX, toY) coodinates are relative
		to the current position.
		If checkOnly=True, the actual move is not performed.
		All elements included in excludeCheckElems are excluded from
		the collision check.
		"""
		if relativeCoords:
			toX, toY = elem.x + toX, elem.y + toY
		if toX == elem.x and toY == elem.y:
			return True # No move needed
		# Check collision
		excludeElems = excludeCheckElems.copy()
		excludeElems.add(elem) # Can't collide with ourselves.
		if self.__haveCollision(toX, toY, elem.height,
					excludeElems=excludeElems):
			return False # Collision. Cannot move.
		# Move the element.
		if not checkOnly:
			elem.x = toX
			elem.y = toY
		return True

	def getElemAt(self, x, y):
		for elem in self.elems:
			ex, ey = elem.x, elem.y
			ew, eh = elem.width, elem.height
			if x >= ex and x < ex + ew and\
			   y >= ey and y < ey + eh:
				return elem
		return None

	def haveElemAt(self, x, y):
		return self.getElemAt(x, y) is not None

	def getElemsInRect(self, x0, y0, x1, y1):
		"""Get all elements placed within the given grid rectangle.
		"""
		return (elem for elem in self.elems
			if elem.isInGridRect(x0, y0, x1, y1))

	def selectElem(self, elem):
		if elem:
			self.selectedElems.add(elem)

	def selectElemAt(self, x, y):
		elem = self.getElemAt(x, y)
		if elem:
			self.selectElem(elem)
		else:
			self.deselectAll()

	def selectElemsInRect(self, x0, y0, x1, y1, clear=False):
		"""Select all elements within the given grid rectangle.
		"""
		if clear:
			self.deselectAll()
		for elem in self.getElemsInRect(x0, y0, x1, y1):
			self.selectElem(elem)

	def deselectElem(self, elem):
		with contextlib.suppress(KeyError):
			self.selectedElems.remove(elem)

	def deselectElemAt(self, x, y):
		elem = self.getElemAt(x, y)
		if elem:
			self.deselectElem(elem)

	def deselectAll(self):
		self.selectedElems.clear()

	def expandElem(self, elem, expand=True):
		ok = False
		if elem and expand and not elem in self.expandedElems:
			ok = elem.expand(expand)
			if ok:
				self.expandedElems.add(elem)
		if elem and not expand and elem in self.expandedElems:
			ok = elem.expand(expand)
			if ok:
				self.expandedElems.remove(elem)
		return ok

	def unexpandAllElems(self):
		if self.expandedElems:
			for elem in self.expandedElems.copy():
				self.expandElem(elem, False)
			return True
		return False
