#!/usr/bin/python3

import locale
import argparse
import json

import requests
import xmltodict
import dateutil.parser

locale.setlocale(locale.LC_ALL, '')


class IKeesingCrossword:
	def __init__(self, clientid:str, puzzleid:str):
		self.clientid = clientid
		if clientid == 'aldagpremium':
			self.publisher = 'DPG Media Nederland'
			self.locale = 'nl_NL'
		elif clientid in ['denksportnlfree', 'denksportnlpremium']:
			self.publisher = 'Denksport'
			self.locale = 'nl_NL'
		elif clientid in ['puzzelsitefree', 'puzzelsitepremium', 'puzzelsitelite']:
			self.publisher = 'Puzzelsite'
			self.locale = 'nl_NL'
		elif clientid == 'bindinc':
			self.publisher = 'Blijtijds'
			self.locale = 'nl_NL'
		elif clientid == 'hetnieuwsbladpremium':
			self.publisher = 'Mediahuis België'
			self.locale = 'nl_BE'
		elif clientid == 'hlnapp':
			self.publisher = 'DPG Media België'
			self.locale = 'nl_BE'
		else:
			self.publisher = None
			self.locale = 'nl'

		self.download_puzzle_source(clientid, puzzleid)
		self.puzzle_source = xmltodict.parse(self.puzzle_response)['puzzle']

		self.construct_metainfo()
		self.construct_grids()
		self.construct_answer()
		self.construct_clues()

		# Some crosswords have missing clue numbers, add them manually
		if self.puzzle_source['@type'] == 'Crossword':
			self.repair_clue_numbers()

		if self.puzzle_source['@type'] == 'Arrowword':
			self.construct_arrowword()

		self.construct_ipuz()


	def __str__(self):
		return json.dumps(self.puzzle_ipuz)


	def download_puzzle_source(self, clientid:str, puzzleid:str):
		url = f"https://web.keesing.com/content/getxml?clientid={clientid}&puzzleid={puzzleid}"
		request = requests.get(url, timeout=10)
		request.raise_for_status()
		request.encoding = 'UTF-8'
		self.puzzle_response = request.text

		if request.text == 'Invalid xml request':
			raise ValueError('Puzzle does not exist')


	def construct_metainfo(self):
		if self.puzzle_source['@type'] == 'Crossword':
			self.kind = 'http://ipuz.org/crossword#1'
			title = 'Kruiswoordpuzzel'
		elif self.puzzle_source['@type'] == 'Cryptogram':
			self.kind = 'http://ipuz.org/crossword/crypticcrossword#1'
			title = 'Cryptogram'
		elif self.puzzle_source['@type'] == 'Filippine':
			self.kind = 'http://ipuz.org/crossword#1'
			title = 'Filippine'
		elif self.puzzle_source['@type'] == 'Arrowword':
			self.kind = 'http://ipuz.org/crossword/arrowword#1'
			title = 'Zweedse puzzel'
		else:
			raise NotImplementedError('Unsupported puzzle type')

		self.title = f"{title} {self.puzzle_source['@id']}"
		self.uniqueid = f"com.keesing.{self.puzzle_source['@id']}"
		self.date = dateutil.parser.isoparse(self.puzzle_source['@exported'])
		self.url = (
			"https://web.keesing.com/pub/player/v2.14.2/site/aldagpremium/"
			f"?gametype={self.puzzle_source['@type']}"
			f"&puzzleid={self.puzzle_source['@id']}"
			f"&customerid={self.clientid}"
		)
		difficulty = int(self.puzzle_source['@difficulty'])
		self.difficulty = '★'*difficulty + '☆'*(5-difficulty)
		self.clueplacement = None


	def construct_grids(self):
		self.width = int(self.puzzle_source['@width'])
		self.height = int(self.puzzle_source['@height'])
		self.puzzle = [[None]*self.width for _ in range(self.height)]
		self.solution = [[None]*self.width for _ in range(self.height)]

		for cell_info in self.puzzle_source['grid']['cells']['cell']:
			x = int(cell_info['@x'])
			y = int(cell_info['@y'])
			visible = bool(int(cell_info['@visible']))
			content = cell_info['@content']
			giveaway = bool(int(cell_info['@giveaway']))
			fillable = bool(int(cell_info['@fillable']))

			try:
				style = {'mark': {'TL': cell_info['@codenumber']}}
			except KeyError:
				style = {}

			if not visible:
				self.puzzle[y][x] = None
				self.solution[y][x] = '#'
				continue

			if fillable:
				self.puzzle[y][x] = {'cell': 0, 'style': style}
				self.solution[y][x] = content
			else:
				self.puzzle[y][x] = {'cell': '#', 'style': style}
				self.solution[y][x] = '#'

			if giveaway:
				self.puzzle[y][x]['value'] = content

		wordgroups = self.puzzle_source['wordgroups']['wordgroup']
		if isinstance(wordgroups, list):
			# Both across and down
			for wordgroup in wordgroups:
				for word in wordgroup['words']['word']:
					x = int(word['cells']['cell'][0]['@x'])
					y = int(word['cells']['cell'][0]['@y'])
					number = int(word['@number']) if '@number' in word else 0
					self.puzzle[y][x]['cell'] = number
		else:
			# Only across or down
			for number, word in enumerate(wordgroups['words']['word'], 1):
				x = int(word['cells']['cell'][0]['@x'])
				y = int(word['cells']['cell'][0]['@y'])
				self.puzzle[y][x]['cell'] = number


	def construct_answer(self):
		try:
			resultword = self.puzzle_source['resultwords']['word']
		except TypeError:
			self.answer = None
			return

		self.answer = resultword['@content']

		for index, cell in enumerate(resultword['cells']['cell'], 1):
			x = int(cell['@x'])
			y = int(cell['@y'])

			# Highlight answer cells for acrostics, number them otherwise
			if self.puzzle_source['@type'] == 'Filippine':
				self.puzzle[y][x]['style']['highlight'] = True
			else:
				if not 'mark' in self.puzzle[y][x]['style'].keys():
					self.puzzle[y][x]['style']['mark'] = {}
				if not 'BR' in self.puzzle[y][x]['style']['mark'].keys():
					self.puzzle[y][x]['style']['mark']['BR'] = {}
				self.puzzle[y][x]['style']['mark']['BR'] = str(index)


	def construct_clues(self):
		wordgroups = self.puzzle_source['wordgroups']['wordgroup']

		if isinstance(wordgroups, list):
			# Both across and down
			for wordgroup in wordgroups:
				clues = [{
					'number': (int(word['@number']) if '@number' in word else int(word['@index'])),
					'clue': (word['clue'] if 'clue' in word else None),
					'cells': [[int(cell['@x']), int(cell['@y'])] for cell in word['cells']['cell']]
				} for word in wordgroup['words']['word']]

				if wordgroup['@kind'] == 'horizontal':
					self.clues_across = clues
				elif wordgroup['@kind'] == 'vertical':
					self.clues_down = clues

		else:
			# Only across or down
			clues = [{
				'number': number,
				'clue': word['clue'],
				'cells': [[int(cell['@x']), int(cell['@y'])] for cell in word['cells']['cell']]
			} for number, word in enumerate(wordgroups['words']['word'], 1)]

			if wordgroups['@kind'] == 'horizontal':
				self.clues_across = clues
				self.clues_down = None
			elif wordgroups['@kind'] == 'vertical':
				self.clues_across = None
				self.clues_down = clues


	def repair_clue_numbers(self):
		def calc_clue_position(clue):
			return clue['cells'][0][0] + self.width * clue['cells'][0][1]

		# Merge both clue lists
		clues_all = []
		for clues in [self.clues_across, self.clues_down]:
			if clues is not None:
				clues_all += clues

		# Check if fixing is required at all
		for clue in clues_all:
			if clue['number'] > 0:
				return

		# Sort clues based on clue number
		self.clues_across.sort(key=calc_clue_position)
		self.clues_down.sort(key=calc_clue_position)
		clues_all.sort(key=calc_clue_position)

		# Generate new clue numbers
		clue_number = 0
		position_prev_clue = -1
		for clue in clues_all:
			position_clue = calc_clue_position(clue)
			if position_clue != position_prev_clue:
				clue_number += 1
				position_prev_clue = position_clue
				x = clue['cells'][0][0]
				y = clue['cells'][0][1]
				self.puzzle[y][x]['cell'] = clue_number
			clue['number'] = clue_number


	def construct_arrowword(self):
		self.clueplacement = 'blocks'

		clues_data_across = {}
		clues_data_down = {}
		for cell_info in self.puzzle_source['grid']['cells']['cell']:
			try:
				clues = cell_info['clue']
			except KeyError:
				continue
			x = int(cell_info['@x'])
			y = int(cell_info['@y'])
			if not isinstance(clues, list):
				clues = [clues]
			for clue in clues:
				if clue['@ispictorialclue'] != '0':
					continue
				index = int(clue['@wordindex'])
				clue_data = {
					'clue': clue['#text'].replace('\\', '\u00AD'),
					'location': [x, y],
					'arrow': clue['@arrow']
				}
				if clue['@groupindex'] == '1':
					clues_data_across[index] = clue_data
				elif clue['@groupindex'] == '2':
					clues_data_down[index] = clue_data

		for clue in self.clues_across:
			clue_data = clues_data_across[clue['number']]
			clue['clue'] = clue_data['clue']
			clue['location'] = clue_data['location']
			del clue['number']

		for clue in self.clues_down:
			clue_data = clues_data_down[clue['number']]
			clue['clue'] = clue_data['clue']
			clue['location'] = clue_data['location']
			del clue['number']


	def construct_ipuz(self):
		self.puzzle_ipuz = {
			'version': 'http://ipuz.org/v2',
			'kind': [self.kind],
			'copyright': '© Keesing Media Group',
			'publisher': self.publisher,
			# 'publication': None,
			'url': self.url,
			'uniqueid': self.uniqueid,
			'title': self.title,
			# 'intro': None,
			# 'explanation': None,
			# 'annotation': None,
			# 'author': None,
			# 'editor': None,
			'date': self.date.strftime('%A %-d %B %Y'),
			# 'notes': None,
			'difficulty': self.difficulty,
			'charset': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
			'origin': 'iKeesing parser - door Philip Goto',
			'dimensions': {
				'width': self.width,
				'height': self.height,
			},
			'puzzle': self.puzzle,
			# 'saved': None,
			'solution': self.solution,
			'clues': {
				'Across': self.clues_across,
				'Down': self.clues_down,
			},
			# 'showenumerations': None,
			'clueplacement': self.clueplacement,
			'answer': self.answer,
			'org.gnome.libipuz:locale': self.locale,
			'org.gnome.libipuz:charset': [
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
				'J', 'IJ', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
				'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
			],
		}


def main():
	parser = argparse.ArgumentParser(description='fetch .ipuz files from keesing.com')
	parser.add_argument(
		'clientid', type=str,
		help='which website the puzzle should be downloaded from'
	)
	parser.add_argument(
		'puzzleid', type=str,
		help='the puzzle id to be downloaded, in format KNL-???????? or {type}_{"today"/"yesterday"}_'
	)
	args = parser.parse_args()

	print(IKeesingCrossword(args.clientid, args.puzzleid))


if __name__ == '__main__':
	main()
