from functools import partial

from kivy.clock import Clock
from kivy.logger import Logger
from kivy.properties import (DictProperty, ListProperty, NumericProperty,
								ObjectProperty, ReferenceListProperty)
from kivy.uix.relativelayout import RelativeLayout
from kivy.lang.builder import Builder
from .spot import GridSpot
from .pawn import GridPawn
from ..boardview import BoardView


class GridBoard(RelativeLayout):
	selection = ObjectProperty()
	selection_candidates = ListProperty()
	character = ObjectProperty()
	tile_width = NumericProperty()
	tile_height = NumericProperty()
	tile_size = ReferenceListProperty(tile_width, tile_height)
	pawn = DictProperty({})
	spot = DictProperty({})
	spot_cls = ObjectProperty(GridSpot)
	pawn_cls = ObjectProperty(GridPawn)
	_parented: bool

	def do_layout(self, *args):
		Logger.debug("GridBoard laying out at size {}".format(self.size))
		rows = cols = 0
		colw = rowh = 0
		for name, spot in self.spot.items():
			row, col = name
			rows = max((rows, row))
			cols = max((cols, col))
			colw = max((spot.width, colw))
			rowh = max((spot.height, rowh))
		self.width = colw * cols
		self.height = rowh * rows
		for name, tile in self.spot.items():
			gx, gy = name
			tile.pos = gx * colw, gy * rowh

	def add_spot(self, placen, *args):
		if placen not in self.character.place:
			raise KeyError(f"No such place for spot: {placen}")
		self.add_widget(self.make_spot(self.character.place[placen]))

	def make_spot(self, place):
		placen = place["name"]
		if placen in self.spot:
			raise KeyError("Already have a Spot for this Place")
		if not isinstance(placen, tuple) or len(placen) != 2 or not isinstance(
			placen[0], int) or not isinstance(placen[1], int):
			raise TypeError(
				"GridBoard can only display places named with pairs of ints")

		r = self.spot_cls(board=self, proxy=place)
		self.spot[place["name"]] = r
		return r

	def make_pawn(self, thing):
		if thing["name"] in self.pawn:
			raise KeyError("Already have a Pawn for this Thing")
		r = self.pawn_cls(board=self, proxy=thing)
		self.pawn[thing["name"]] = r
		return r

	def _trigger_add_tile(self, placen):
		part = partial(self.add_spot, placen)
		Clock.unschedule(part)
		Clock.schedule_once(part, 0)

	def add_new_spots(self, *args):
		return self._maybe_add_nodes(GridSpot.default_image_paths,
										self.character.place, self.spot,
										self.make_spot, self.add_widget)

	def add_pawn(self, thingn, *args):
		if thingn not in self.character.thing:
			raise KeyError(f"No such thing: {thingn}")
		if thingn in self.pawn:
			raise KeyError(f"Already have a pawn for {thingn}")
		pwn = self.make_pawn(self.character.thing[thingn])
		whereat = self.spot[pwn.proxy['location']]
		whereat.add_widget(pwn)
		self.pawn[thingn] = pwn

	def _trigger_add_pawn(self, thingn):
		part = partial(self.add_pawn, thingn)
		Clock.unschedule(part)
		Clock.schedule_once(part, 0)

	@staticmethod
	def _maybe_add_nodes(default_image_paths, nodemap, mymap, maker, adder):
		default_zeroes = [0] * len(default_image_paths)
		to_add = []
		patchmap = {}
		for noden, node in nodemap.items():
			if noden not in mymap:
				to_add.append(node)
				patch = {}
				if '_image_paths' in node:
					zeroes = [0] * len(node['_image_paths'])
				else:
					patch['_image_paths'] = default_image_paths
					zeroes = default_zeroes
				if '_offxs' not in node:
					patch['_offxs'] = zeroes
				if '_offys' not in node:
					patch['_offys'] = zeroes
				if patch:
					patchmap[noden] = patch
		if patchmap:
			nodemap.patch(patchmap)
		added = []
		for node in to_add:
			try:
				mine = maker(node)
			except TypeError:
				continue
			added.append(mine)
			adder(mine)
		return added

	def add_new_pawns(self, *args):

		def adder(pwn):
			self.spot[pwn.proxy['location']].add_widget(pwn)

		return self._maybe_add_nodes(GridPawn.default_image_paths,
										self.character.thing, self.pawn,
										self.make_pawn, adder)

	def on_parent(self, *args):
		if not self.parent or hasattr(self, '_parented'):
			return
		self._parented = True
		self.update()

	def remove_absent_pawns(self):
		pawnmap = self.pawn
		thingmap = self.character.thing
		for name, pawn in list(pawnmap.items()):
			if name not in thingmap:
				pawn.parent.remove_widget(pawn)
				del pawnmap[name]

	def remove_absent_spots(self):
		spotmap = self.spot
		placemap = self.character.place
		remove_widget = self.remove_widget
		for name, spot in list(spotmap.items()):
			if name not in placemap:
				remove_widget(spot)
				del spotmap[name]

	def update(self, *args):
		self.remove_absent_pawns()
		self.remove_absent_spots()
		self.add_new_spots()
		self.add_new_pawns()
		for spot in self.spot.values():
			spot.finalize()
		for pawn in self.pawn.values():
			pawn.finalize()

	def rm_spot(self, name):
		spot = self.spot.pop(name)
		if spot in self.selection_candidates:
			self.selection_candidates.remove(spot)
		for pwn in spot.children:
			del self.pawn[pwn.name]
		self.remove_widget(spot)

	def rm_pawn(self, name):
		pwn = self.pawn.pop(name)
		if pwn in self.selection_candidates:
			self.selection_candidates.remove(pwn)
		pwn.parent.remove_widget(pwn)

	def update_from_delta(self, delta, *args):
		pawnmap = self.pawn
		spotmap = self.spot
		add_pawn = self.add_pawn
		add_spot = self.add_spot
		selection_candidates = self.selection_candidates

		def rm_pawn(name):
			pwn = pawnmap.pop(name)
			if pwn in selection_candidates:
				selection_candidates.remove(pwn)
			pwn.parent.remove_widget(pwn)

		remove_widget = self.remove_widget

		def rm_spot(name):
			spot = spotmap.pop(name)
			if spot in selection_candidates:
				selection_candidates.remove(spot)
			for pwn in spot.children:
				del pawnmap[pwn.name]
			remove_widget(spot)

		if 'nodes' in delta:
			for node, extant in delta['nodes'].items():
				if extant:
					if 'node_val' in delta \
                                     and node in delta['node_val'] \
                                     and 'location' in delta['node_val'][node] \
                                     and node not in pawnmap:
						add_pawn(node)
					elif node not in spotmap:
						add_spot(node)
				else:
					if node in pawnmap:
						rm_pawn(node)
					if node in spotmap:
						rm_spot(node)
		if 'node_val' in delta:
			for node, stats in delta['node_val'].items():
				if node in spotmap and '_image_paths' in stats:
					spotmap[node].paths = stats['_image_paths'] \
                                                         or GridSpot.default_image_paths
				elif node in pawnmap:
					pawn = pawnmap[node]
					if 'location' in stats:
						pawn.loc_name = stats['location']
					if '_image_paths' in stats:
						pawn.paths = stats['_image_paths'] \
                                                       or GridPawn.default_image_paths
				else:
					Logger.warning(
						"GridBoard: diff tried to change stats of node {}"
						"but I don't have a widget for it".format(node))

	def trigger_update_from_delta(self, delta, *args):
		part = partial(self.update_from_delta, delta)
		Clock.unschedule(part)
		Clock.schedule_once(part, 0)


class GridBoardView(BoardView):
	pass


Builder.load_string("""
<GridBoard>:
    app: app
    size_hint: None, None
<GridBoardView>:
    plane: boardplane
    BoardScatterPlane:
        id: boardplane
        board: root.board
        scale_min: root.scale_min
        scale_max: root.scale_max
""")
