"""
----------
paddachee.py -- Bob's "Pattern Achievment" game. This is a re-implementation of
                java applet that I implemented in the 2003..2005 time frame.
                The game itself is one I invented circa 1985, originally called
                "Diz".
----------
$$$ fill this in
----------
"""

from urllib.parse    import parse_qs
from dataclasses     import replace
from js              import document,window
from pyodide.ffi     import create_proxy
import json
import random

from game            import Game
from patterns        import GameSpec,ScoringGoal,games,get_game_spec,get_pattern
from renderer        import Renderer
from computer_player import ComputerPlayer
from settings        import Settings


class PaddacheeApp:
	numSuggestions  = 3
	# nota bene: the following list must match the items in board-size-select
	#            in paddachee.html
	validBoardSizes = ((11,11),(13,13),(15,15),(17,17),(11,17))

	def __init__(self):
		document.body.dataset.redrawCount = "0"
		self.redrawCount = 0

		self.has_permission()

		self.canvas                = document.getElementById("playfield")
		self.urlParams             = parse_qs(window.location.search[1:],keep_blank_values=True)
		self.screenGrabbing        = ("selfie" in self.urlParams) or ("screenGrabbing" in self.urlParams)
		self.settings              = Settings()
		self.game                  = Game(self.game_from_url())
		self.renderer              = Renderer(self.canvas,self.game)
		self.playerType            = self.player_types_from_settings()
		self.computers             = [ComputerPlayer(0),ComputerPlayer(1)]
		self.smartness             = self.smartness_from_url()
		self.patternInspector      = None
		self.patternInspectionCell = None
		self.patternInspectionKind = None
		self.patternInspectionText = None
		self.suggestedMoves        = []
		self.interactionMode       = "move"
		self.proxies               = []
		self.install_events()
		self.load_about_text()
		self.aboutTextIsTurnOrder = False

		for playerNum in (0,1):
			panel = document.getElementById(f"player-{playerNum+1}-panel")
			panel.style.backgroundColor = self.renderer.playerBackgroundColors[playerNum]

		self.populate_game_select()
		self.sync_board_size_select()
		self.sync_controls()
		self.cellCentersPublished = False
		self.redraw()
		self.maybe_take_computer_turn()


	def game_from_url(self):
		gameName = self.urlParams.get("game",[None])[0]
		if (gameName in games):
			gameSpec = get_game_spec(gameName)
		else:
			gridType = self.urlParams.get("grid" ,[None])[0]
			goalSpec = self.urlParams.get("goals",[None])[0]
			if ((gridType in ("square","hex")) and goalSpec):
				goals = []
				for item in goalSpec.split(","):
					(worthText,patternName) = item.split("*",1)
					pattern = get_pattern(patternName)
					if (pattern.gridType != gridType):
						raise ValueError("goal pattern does not match grid")
					goals += [ScoringGoal(patternName,int(worthText))]
				gameSpec = GameSpec("custom","Custom Paddachee",gridType,11,11,tuple(goals))
			else:
				gameSpec = get_game_spec("square_line")

		return self.apply_initial_board_size(gameSpec)


	def apply_initial_board_size(self,gameSpec):
		try:
			rowsParam    = self.urlParams.get("rows"   ,[None])[0]
			columnsParam = self.urlParams.get("columns",[None])[0]
			if ((rowsParam is not None) or (columnsParam is not None)):
				rows    = int(rowsParam) if (rowsParam is not None) else gameSpec.rows
				columns = int(columnsParam) if (columnsParam is not None) else rows
				if ((rows,columns) in self.validBoardSizes):
					return replace(gameSpec,rows=rows,columns=columns)
		except ValueError:
			pass

		sizeText = self.settings.get("boardSize")
		if (sizeText is not None):
			try:
				(rowsText,columnsText) = sizeText.split("x",1)
				size = (int(rowsText),int(columnsText))
				if (size in self.validBoardSizes):
					return replace(gameSpec,rows=size[0],columns=size[1])
			except ValueError:
				pass

		return gameSpec


	def player_types_from_settings(self):
		playerType = [self.settings.get("player1Type","human"),
		              self.settings.get("player2Type","computer")]
		for playerNum in (0,1):
			if (playerType[playerNum] not in ("human","computer")):
				playerType[playerNum] = ("human" if (playerNum == 0) else "computer")
		if (playerType == ["computer","computer"]):
			playerType[0] = "human"
		return playerType


	def smartness_from_url(self):
		smartness = [self.settings.get_int("player1Smartness",ComputerPlayer.defaultSmartness),
		             self.settings.get_int("player2Smartness",ComputerPlayer.defaultSmartness)]
		for playerNum in (0,1):
			try:
				paramName = f"smartness{playerNum+1}"
				smartness[playerNum] = int(self.urlParams.get(paramName,[smartness[playerNum]])[0])
			except ValueError:
				pass   # (allow the saved/default value to remain)
			smartness[playerNum] = max(0,min(ComputerPlayer.maxSmartness,smartness[playerNum]))
		return smartness


	def set_smartness(self,playerNum,value):
		try:
			value = int(value)
		except ValueError:
			value = ComputerPlayer.defaultSmartness
		value = max(0,min(ComputerPlayer.maxSmartness,value))
		self.smartness[playerNum] = value
		self.settings.set(f"player{playerNum+1}Smartness",value)
		document.getElementById(f"player-{playerNum+1}-smartness").value = str(value)
		document.getElementById(f"player-{playerNum+1}-smartness-value").textContent = str(value)


	def load_about_text(self):
		document.getElementById("about-text").textContent = self.fetch_text("about.txt")


	def fetch_text(self,filename):
		with open(filename,encoding="utf-8") as f:
			text = f.read()
		return self.unwrap_text(text.strip())


	def unwrap_text(self,text):
		paragraphs = []
		for p in text.split("\n\n"):
			hasByline = ("\n  " in p)
			if (hasByline): paragraphs += [p.strip()]
			else:           paragraphs += [" ".join(p.split())]
		return "\n\n".join(paragraphs)


	def maybe_take_computer_turn(self):
		if (not self.game.gameInProgress):
			return
		playerNum = self.game.whosTurn
		if (self.playerType[playerNum] != "computer"):
			return
		cell = self.computers[playerNum].choose_move(self.game,self.smartness[playerNum])
		if (cell is not None):
			self.clear_pattern_inspection()
			self.game.make_move(cell)
			self.redraw()


	def populate_game_select(self):
		select = document.getElementById("game-select")
		select.innerHTML = ""
		gameNames = [(games[gameName][0],gameName) for gameName in games]
		gameNames = [gameName for (_,gameName) in sorted(gameNames)]
		for gameName in gameNames:
			option = document.createElement("option")
			option.value = gameName
			option.textContent = games[gameName][1].displayName
			if (gameName == self.game.gameSpec.name):
				option.selected = True
			select.appendChild(option)


	def sync_board_size_select(self):
		select = document.getElementById("board-size-select")
		sizeValue = f"{self.game.gameSpec.rows}x{self.game.gameSpec.columns}"
		validValues = [option.value for option in select.options]
		if (sizeValue in validValues):
			select.value = sizeValue


	def selected_board_size(self):
		sizeText = document.getElementById("board-size-select").value
		(rowsText,columnsText) = sizeText.split("x",1)
		return (int(rowsText),int(columnsText))


	def start_game(self,gameSpec):
		self.set_interaction_mode("move")
		self.clear_pattern_inspection()
		self.clear_suggested_moves()
		self.renderer.clear_emphasis()
		self.renderer.clear_annotation()
		self.game = Game(gameSpec)
		self.renderer.set_game(self.game)
		self.cellCentersPublished = False
		self.redraw()
		self.maybe_take_computer_turn()


	def draw_goals(self):
		container = document.getElementById("goal-list")
		container.innerHTML = ""
		goalCanvases = []
		for goalNumber,goal in enumerate(self.game.referee.goals):
			item = document.createElement("div")
			item.className = "goal-item"
			canvas = document.createElement("canvas")
			canvas.className = "goal-canvas"
			text = document.createElement("div")
			text.className = "goal-text"
			text.textContent = f"{goal.worth} × {goal.pattern.displayName}"
			item.appendChild(canvas)
			item.appendChild(text)
			container.appendChild(item)
			goalCanvases += [(canvas,goal)]

		# All items must be in the DOM before measuring their canvases. Adding a
		# second goal can change the CSS width of the first one.
		commonScale = min([self.renderer.goal_scale(canvas,goal)
		                   for (canvas,goal) in goalCanvases])
		for (canvas,goal) in goalCanvases:
			self.renderer.draw_goal(canvas,goal,commonScale)


	def clear_pattern_inspection(self):
		self.patternInspector      = None
		self.patternInspectionCell = None
		self.patternInspectionKind = None
		self.patternInspectionText = None
		self.renderer.set_pattern_highlight(None)


	def clear_suggested_moves(self):
		self.suggestedMoves = []
		self.renderer.set_suggested_moves([])


	def set_interaction_mode(self,mode,redraw=True,clearInspection=True):
		if (mode not in ("move","viable","threat","pre-threat","scored")):
			mode = "move"

		self.interactionMode = mode
		for playerNum in (0,1):
			document.getElementById(f"player-{playerNum+1}-interaction-mode").value = mode

		if (clearInspection):
			self.clear_pattern_inspection()
		if (mode != "move"):
			self.clear_suggested_moves()
		if (redraw):
			self.redraw()


	def suggest_moves(self,playerNum):
		if (not self.game.gameInProgress): return
		if (self.game.whosTurn != playerNum): return
		if (self.playerType[playerNum] != "human"): return

		self.clear_pattern_inspection()
		self.suggestedMoves = self.computers[playerNum].suggest_moves(self.game,self.numSuggestions)
		self.renderer.set_suggested_moves(self.suggestedMoves)
		self.redraw()


	def random_move(self,playerNum):
		if (not self.screenGrabbing): return
		if (not self.game.gameInProgress): return
		if (self.game.whosTurn != playerNum): return
		if (self.playerType[playerNum] != "human"): return

		possibleMoves = self.game.empty_cells()
		if (possibleMoves == []): return

		self.clear_pattern_inspection()
		self.clear_suggested_moves()
		self.game.make_move(random.choice(possibleMoves))
		self.redraw()
		self.maybe_take_computer_turn()


	def inspect_pattern(self,cell,kind):
		if ((self.patternInspector is None)
		  or (self.patternInspectionCell != cell)
		  or (self.patternInspectionKind != kind)):
			if (kind == "viable"):
				self.patternInspector = self.game.referee.viable_placements(self.game,cell)
			elif (kind == "threat"):
				self.patternInspector = self.game.referee.threat_placements(self.game,cell)
			elif (kind == "pre-threat"):
				self.patternInspector = self.game.referee.pre_threat_placements(self.game,cell)
			else: # if (kind == "scored"):
				self.patternInspector = self.game.referee.scored_placements(self.game,cell)

			self.patternInspectionCell = cell
			self.patternInspectionKind = kind

		try:
			(pattern,explanation) = next(self.patternInspector)
		except StopIteration:
			self.clear_pattern_inspection()
		else:
			self.renderer.set_pattern_highlight(pattern)
			self.patternInspectionText = explanation

		self.clear_suggested_moves()
		self.redraw()


	def redraw(self):
		self.renderer.draw()
		if (self.screenGrabbing) and (not self.cellCentersPublished):
			self.publish_cell_centers()
		self.draw_goals()
		document.getElementById("game-title").textContent = self.game.gameSpec.displayName
		document.getElementById("player-1-score").textContent = f"{self.game.score[0]}★"
		document.getElementById("player-2-score").textContent = f"{self.game.score[1]}★"
		document.body.dataset.turn = str(self.game.whosTurn+1)

		player1Panel = document.getElementById("player-1-panel")
		player2Panel = document.getElementById("player-2-panel")
		player1Panel.classList.toggle("current-player",(self.game.gameInProgress) and (self.game.whosTurn == 0))
		player2Panel.classList.toggle("current-player",(self.game.gameInProgress) and (self.game.whosTurn == 1))
		for playerNum in (0,1):
			suggest = document.getElementById(f"player-{playerNum+1}-suggest")
			suggest.disabled = ((not self.game.gameInProgress) or (self.game.whosTurn != playerNum))

		turnIndicator = document.getElementById("turn-indicator")
		if (self.game.gameInProgress):
			playerNum = self.game.whosTurn
			turnIndicator.style.display = "inline-block"
			turnIndicator.style.backgroundColor = self.renderer.playerColors[playerNum]
			if (self.game.gameSpec.gridType == "hex"):
				turnIndicator.style.borderRadius = "50%"
			else:
				turnIndicator.style.borderRadius = "0"
		else:
			turnIndicator.style.display = "none"

		status = document.getElementById("status")
		if (self.game.gameInProgress):
			statusText = f"Player {self.game.whosTurn+1}’s turn"
			if ((self.game.movesTaken == 0) and (self.game.prohibitedOpeningCell is not None)):
				statusText += "; the middle cell is not allowed for the first move"
			if (self.patternInspectionText is not None):
				statusText = f"{self.patternInspectionText}; {statusText}"
			elif (self.suggestedMoves != []):
				numMoves = len(self.suggestedMoves)
				moveWord = "move" if (numMoves == 1) else "moves"
				statusText = f"{numMoves} {moveWord} suggested; {statusText}"
			status.textContent = statusText
		else:
			statusText = "The game is finished"
			if (self.patternInspectionText is not None):
				statusText = f"{self.patternInspectionText}; {statusText}"
			status.textContent = statusText

		self.redrawCount += 1
		document.body.dataset.redrawCount = str(self.redrawCount)


	def install_events(self):
		def on_mouse_move(event):
			(self.mouseX,self.mouseY) = click_to_xy(event)

		def on_click(event):
			(x,y) = click_to_xy(event)
			cell = self.game.geometry.pick_cell(x,y,self.renderer.layoutSpec)
			if (cell is None):
				needToRedraw = False
				if (self.suggestedMoves != []):
					self.clear_suggested_moves()
					needToRedraw = True
				if (self.patternInspector is not None):
					self.clear_pattern_inspection()
					needToRedraw = True
				if (needToRedraw): self.redraw()
				return

			if (event.shiftKey):
				self.set_interaction_mode("move",redraw=False,clearInspection=False)
				self.inspect_pattern(cell,"viable")
				return

			if (event.ctrlKey):
				event.preventDefault()
				self.set_interaction_mode("move",redraw=False,clearInspection=False)
				self.inspect_pattern(cell,"threat")
				return

			if (event.altKey):
				event.preventDefault()
				self.set_interaction_mode("move",redraw=False,clearInspection=False)
				self.inspect_pattern(cell,"pre-threat")
				return

			if (self.interactionMode != "move"):
				self.inspect_pattern(cell,self.interactionMode)
				return

			hadInspection = (self.patternInspector is not None)
			self.clear_pattern_inspection()
			if (self.playerType[self.game.whosTurn] == "human"):
				if (self.game.make_move(cell)):
					self.clear_suggested_moves()
					self.redraw()
					self.maybe_take_computer_turn()
				elif (hadInspection):
					self.redraw()

		def click_to_xy(event):
			rect = self.canvas.getBoundingClientRect()
			x = event.clientX - rect.left
			y = event.clientY - rect.top
			return (x,y)

		def on_key_down(event):
			if (event.target.tagName in ("INPUT","TEXTAREA","SELECT")):
				return

			if (event.key == "^"):
				self.renderer.dbgShowCoordinates = (not self.renderer.dbgShowCoordinates)
				self.redraw()
				return

			if (event.key == "#"):
				if (self.screenGrabbing) and (event.shiftKey) and (event.ctrlKey):
					self.aboutTextIsTurnOrder = True
					return
				self.renderer.dbgShowTurnNumbers = (not self.renderer.dbgShowTurnNumbers)
				self.redraw()
				return

			if (event.key == "@"):
				self.set_interaction_mode("move",redraw=False,clearInspection=False)
				(x,y) = self.mouseX,self.mouseY
				if (self.mouseX is None): return
				cell = self.game.geometry.pick_cell(x,y,self.renderer.layoutSpec)
				if (cell is None): return
				self.inspect_pattern(cell,"scored")
				return

			if (self.screenGrabbing) and (event.key == "`"):  # (tick mark)
				(x,y) = self.mouseX,self.mouseY
				if (self.mouseX is None): return
				cell = self.game.geometry.pick_cell(x,y,self.renderer.layoutSpec)
				if (cell is None): return
				if (cell not in self.renderer.emphasis):
					self.renderer.emphasis.add(cell)
				else:
					self.renderer.emphasis.remove(cell)
				self.redraw()
				return

			if (self.screenGrabbing) and (event.key == "="):
				(x,y) = (self.mouseX,self.mouseY)
				if (self.mouseX is None): return
				cell = self.game.geometry.pick_cell(x,y,self.renderer.layoutSpec)
				if (cell is None): return
				if (cell not in self.renderer.annotation):
					self.renderer.annotation[cell] = "A"
				elif (self.renderer.annotation[cell] < "Z"):
					self.renderer.annotation[cell] = chr(ord(self.renderer.annotation[cell])+1)
				else:
					del self.renderer.annotation[cell]
				self.redraw()
				return

			if (self.screenGrabbing) and (event.key == "?"):
				event.preventDefault()
				self.random_move(self.game.whosTurn)
				return

			if (event.code == "Space"):
				event.preventDefault()
				self.suggest_moves(self.game.whosTurn)


		def on_context_menu(event):
			if (event.ctrlKey):
				event.preventDefault()

		def on_resize(event):
			self.cellCentersPublished = False
			self.redraw()

		def on_about(event):
			if (self.aboutTextIsTurnOrder):
				document.getElementById("about-text").textContent = self.turn_order_info()
			document.getElementById("about-dialog").showModal()

		def on_about_close(event):
			document.getElementById("about-dialog").close()

		def on_new_game(event):
			self.clear_pattern_inspection()
			self.clear_suggested_moves()
			self.renderer.clear_emphasis()
			self.renderer.clear_annotation()
			self.game.restart()
			self.redraw()
			self.maybe_take_computer_turn()

		def on_game_change(event):
			gameName = document.getElementById("game-select").value
			(rows,columns) = self.selected_board_size()
			gameSpec = replace(get_game_spec(gameName),rows=rows,columns=columns)
			self.start_game(gameSpec)

		def on_board_size_change(event):
			(rows,columns) = self.selected_board_size()
			self.settings.set("boardSize",f"{rows}x{columns}")
			gameSpec = replace(self.game.gameSpec,rows=rows,columns=columns)
			self.start_game(gameSpec)

		self.mouseX = self.mouseY = None
		self.listen(self.canvas,"mousemove",on_mouse_move)

		self.listen(self.canvas,"click",on_click)
		self.listen(document,"keydown",on_key_down)
		self.listen(self.canvas,"contextmenu",on_context_menu)
		self.listen(window,"resize",on_resize)
		self.listen(document.getElementById("new-game"),"click",on_new_game)
		self.listen(document.getElementById("about-button"),"click",on_about)
		self.listen(document.getElementById("about-close"),"click",on_about_close)
		self.listen(document.getElementById("about-ok"),"click",on_about_close)
		self.listen(document.getElementById("game-select"),"change",on_game_change)
		self.listen(document.getElementById("board-size-select"),"change",on_board_size_change)

		for playerNum in (0,1):
			for kind in ("human","computer"):
				element = document.getElementById(f"player-{playerNum+1}-{kind}")
				self.listen(element,"change",lambda event,p=playerNum,k=kind: self.set_player_type(p,k))
			suggest = document.getElementById(f"player-{playerNum+1}-suggest")
			self.listen(suggest,"click",lambda event,p=playerNum: self.suggest_moves(p))
			interactionMode = document.getElementById(f"player-{playerNum+1}-interaction-mode")
			self.listen(interactionMode,"change",lambda event: self.set_interaction_mode(event.target.value))
			smarts = document.getElementById(f"player-{playerNum+1}-smartness")
			value = document.getElementById(f"player-{playerNum+1}-smartness-value")
			self.listen(smarts,"input",lambda event,v=value: setattr(v,"textContent",event.target.value))
			self.listen(smarts,"change",lambda event,p=playerNum: self.set_smartness(p,event.target.value))


	def listen(self,element,eventName,function):
		proxy = create_proxy(function)
		self.proxies += [proxy]
		element.addEventListener(eventName,proxy)


	def set_player_type(self,playerNum,kind):
		other = 1-playerNum
		if ((kind == "computer") and (self.playerType[other] == "computer")):
			document.getElementById(f"player-{playerNum+1}-human").checked = True
			return
		self.playerType[playerNum] = kind
		self.settings.set(f"player{playerNum+1}Type",kind)
		self.clear_suggested_moves()
		self.sync_controls()
		self.maybe_take_computer_turn()


	def sync_controls(self):
		for playerNum in (0,1):
			document.getElementById(f"player-{playerNum+1}-{self.playerType[playerNum]}").checked = True

			smartness = document.getElementById(f"player-{playerNum+1}-smartness")
			smartness.max = str(ComputerPlayer.maxSmartness)
			smartness.value = str(self.smartness[playerNum])
			document.getElementById(f"player-{playerNum+1}-smartness-value").textContent = str(self.smartness[playerNum])

			smartnessControl = smartness.closest(".smartness-control")
			smartnessControl.hidden = (self.playerType[playerNum] != "computer")

			humanAidControls = document.getElementById(f"player-{playerNum+1}-suggest").closest(".human-aid-controls")
			humanAidControls.hidden = (self.playerType[playerNum] != "human")

			suggest = document.getElementById(f"player-{playerNum+1}-suggest")
			suggest.disabled = ((not self.game.gameInProgress) or (self.game.whosTurn != playerNum))

			interactionMode = document.getElementById(f"player-{playerNum+1}-interaction-mode")
			interactionMode.value = self.interactionMode

	def turn_order_info(self):
		moves = sorted([(self.game.cellToTurnNumber[cell],cell) for cell in self.game.cellToTurnNumber])

		lines = []
		for (turnNumber,cell) in moves:
			(row,col) = cell
			lines += [f"move{turnNumber} = ({row},{col})"]

		return "\n".join(lines)


	def publish_cell_centers(self):
		""" make cell centers available to browser automation """

		cellCenters = {}
		for cell in self.game.geometry.cells():
			cellStr = "(%s)" % ",".join(map(str,cell))
			cellCenters[cellStr] = self.game.geometry.center(cell,self.renderer.layoutSpec)
		self.canvas.dataset.cellCenters = json.dumps(cellCenters)
		self.cellCentersPublished = True


	def has_permission(self):
		permittedDomain = "bumblebeagle.org"
		host = str(window.location.hostname).lower()

		# also consider adding these hosts: "127.0.0.1","::1"

		if (   (host == permittedDomain)
			or (host.endswith("." + permittedDomain))
			or (host in ("localhost",))    # (consider adding other hosts here)
			or (host in ("10.0.0.84",))
			):
			return True

		window.document.querySelector(".app").remove()
		window.alert(
			"This application is distributed only from bumblebeagle.org."
			" If you reached this page elsewhere, please use the official site instead."
		)

		raise SystemExit()
		return False


# create the app instance

app = PaddacheeApp()
