# -*- coding: utf-8 -*-
# appledict/__init__.py
# Output to Apple Dictionary xml sources for Dictionary Development Kit.
#
# Copyright © 2016-2021 Saeed Rasooli <saeed.gnu@gmail.com> (ilius)
# Copyright © 2016 Ratijas <ratijas.t@me.com>
# Copyright © 2012-2015 Xiaoqiang Wang <xiaoqiangwang AT gmail DOT com>
#
# This program is a 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, version 3 of the License.
#
# You can get a copy of GNU General Public License along this program
# But you can always get it from http://www.gnu.org/licenses/gpl.txt
#
# 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.

import io
import os
import pkgutil
import shutil
import sys
from os.path import basename, isdir, join
from typing import Any, Dict, Generator, Optional

from pyglossary.core import log, pip
from pyglossary.glossary_type import EntryType, GlossaryType
from pyglossary.option import (
	BoolOption,
	DictOption,
	StrOption,
)
from pyglossary.text_utils import toStr

from ._content import prepare_content
from ._dict import (
	_normalize,
	id_generator,
	indexes_generator,
	quote_string,
)

sys.setrecursionlimit(10000)

enable = True
lname = "appledict"
format = "AppleDict"
description = "AppleDict Source"
extensions = (".apple",)
extensionCreate = ".apple/"
kind = "directory"
wiki = ""
website = (
	"https://support.apple.com/en-gu/guide/dictionary/welcome/mac",
	"Dictionary User Guide for Mac",
)
# FIXME: rename indexes arg/option to indexes_lang?
optionsProp = {
	"clean_html": BoolOption(comment="use BeautifulSoup parser"),
	"css": StrOption(
		comment="custom .css file path",
	),
	"xsl": StrOption(
		comment="custom XSL transformations file path",
	),
	"default_prefs": DictOption(
		comment="default prefs in python dict format",
		# example: {"key": "value", "version": "1"}
	),
	"prefs_html": StrOption(
		comment="preferences XHTML file path",
	),
	"front_back_matter": StrOption(
		comment="XML file path with top-level tag",
	),
	"jing": BoolOption(comment="run Jing check on generated XML"),
	"indexes": StrOption(
		customValue=False,
		values=["", "ru", "zh"],
		comment="Additional indexes to dictionary entries",
	),
}

extraDocs = [
	(
		"Also see:",
		"See [doc/apple.md](./doc/apple.md) for additional AppleDict instructions.",
	),
]


BeautifulSoup = None


def loadBeautifulSoup() -> None:
	global BeautifulSoup
	try:
		import bs4 as BeautifulSoup
	except ImportError:
		try:
			import BeautifulSoup
		except ImportError:
			return
	if int(BeautifulSoup.__version__.split(".")[0]) < 4:
		raise ImportError(
			f"BeautifulSoup is too old, required at least version 4, "
			f"{BeautifulSoup.__version__!r} found.\n"
			f"Please run `{pip} install lxml beautifulsoup4 html5lib`",
		)


def abspath_or_None(path: str) -> str:
	return os.path.abspath(os.path.expanduser(path)) if path else None


def write_header(
	glos: "GlossaryType",
	toFile: "io.TextIOBase",
	front_back_matter: "Optional[str]",
) -> None:
	# write header
	toFile.write(
		'<?xml version="1.0" encoding="UTF-8"?>\n'
		'<d:dictionary xmlns="http://www.w3.org/1999/xhtml" '
		'xmlns:d="http://www.apple.com/DTDs/DictionaryService-1.0.rng">\n',
	)

	if front_back_matter:
		with open(
			front_back_matter,
			mode="r",
			encoding="utf-8",
		) as front_back_matter:
			toFile.write(front_back_matter.read())


def format_default_prefs(default_prefs: "Optional[Dict[str, Any]]") -> str:
	"""
	:type default_prefs: dict or None

	as by 14th of Jan 2016, it is highly recommended that prefs should contain
	{"version": "1"}, otherwise Dictionary.app does not keep user changes
	between restarts.
	"""
	if not default_prefs:
		return ""
	if not isinstance(default_prefs, dict):
		raise TypeError(f"default_prefs not a dictionary: {default_prefs!r}")
	if str(default_prefs.get("version", None)) != "1":
		log.error(
			"default prefs does not contain {'version': '1'}.  prefs "
			"will not be persistent between Dictionary.app restarts.",
		)
	return "\n".join(
		f"\t\t<key>{key}</key>\n\t\t<string>{value}</string>"
		for key, value in sorted(default_prefs.items())
	).strip()


def write_css(fname: str, css_file: str) -> None:
	with open(fname, mode="wb") as toFile:
		if css_file:
			with open(css_file, mode="rb") as fromFile:
				toFile.write(fromFile.read())
		else:
			toFile.write(pkgutil.get_data(
				__name__,
				"templates/Dictionary.css",
			))


"""
write glossary to Apple dictionary .xml and supporting files.

:param dirname: directory path, must not have extension

:param clean_html: pass True to use BeautifulSoup parser.

:param css: path to custom .css file

:param xsl: path to custom XSL transformations file.

:param default_prefs: Default prefs in python dictionary literal format,
i.e. {"key1": "value1", "key2": "value2", ...}.  All keys and values
must be quoted strings; not allowed characters (e.g. single/double
quotes,equal sign "=", semicolon) must be escaped as hex code
according to python string literal rules.

:param prefs_html: path to XHTML file with user interface for
dictionary's preferences. refer to Apple's documentation for details.

:param front_back_matter: path to XML file with top-level tag
<d:entry id="front_back_matter" d:title="Your Front/Back Matter Title">
	your front/back matter entry content
</d:entry>

:param jing: pass True to run Jing check on generated XML.

# FIXME: rename to indexes_lang?
:param indexes: Dictionary.app is dummy and by default it don't know
how to perform flexible search.  we can help it by manually providing
additional indexes to dictionary entries.
"""


class Writer(object):
	depends = {
		"lxml": "lxml",
		"bs4": "beautifulsoup4",
		"html5lib": "html5lib",
	}

	_clean_html: bool = True
	_css: str = ""
	_xsl: str = ""
	_default_prefs: "Optional[Dict]" = None
	_prefs_html: str = ""
	_front_back_matter: str = ""
	_jing: bool = False
	_indexes: str = ""  # FIXME: rename to indexes_lang?

	def __init__(self, glos: GlossaryType) -> None:
		self._glos = glos
		self._dirname = None

	def finish(self) -> None:
		self._dirname = None

	def open(self, dirname: str) -> None:
		self._dirname = dirname
		if not isdir(dirname):
			os.mkdir(dirname)

	def write(self) -> "Generator[None, EntryType, None]":
		global BeautifulSoup
		from pyglossary.xdxf_transform import XdxfTransformer

		glos = self._glos
		clean_html = self._clean_html
		css = self._css
		xsl = self._xsl
		default_prefs = self._default_prefs
		prefs_html = self._prefs_html
		front_back_matter = self._front_back_matter
		jing = self._jing
		indexes = self._indexes

		xdxf_to_html = XdxfTransformer(encoding="utf-8")

		if clean_html:
			if BeautifulSoup is None:
				loadBeautifulSoup()
			if BeautifulSoup is None:
				log.warning(
					"clean_html option passed but BeautifulSoup not found. "
					f"to fix this run "
					f"`{pip} install lxml beautifulsoup4 html5lib`",
				)
		else:
			BeautifulSoup = None

		dirname = self._dirname
		fileNameBase = basename(dirname).replace(".", "_")
		filePathBase = join(dirname, fileNameBase)
		# before chdir (outside indir block)
		css = abspath_or_None(css)
		xsl = abspath_or_None(xsl)
		prefs_html = abspath_or_None(prefs_html)
		front_back_matter = abspath_or_None(front_back_matter)

		generate_id = id_generator()
		generate_indexes = indexes_generator(indexes)

		myResDir = join(dirname, "OtherResources")
		if not isdir(myResDir):
			os.mkdir(myResDir)

		with open(filePathBase + ".xml", mode="w", encoding="utf-8") as toFile:
			write_header(glos, toFile, front_back_matter)
			while True:
				entry = yield
				if entry is None:
					break
				if entry.isData():
					entry.save(myResDir)
					continue

				words = entry.l_word
				word, alts = words[0], words[1:]
				defi = entry.defi

				long_title = _normalize.title_long(
					_normalize.title(word, BeautifulSoup),
				)
				if not long_title:
					continue

				_id = next(generate_id)
				quoted_title = quote_string(long_title, BeautifulSoup)

				content_title = long_title
				if entry.defiFormat == "x":
					defi = xdxf_to_html.transformByInnerString(defi)
					content_title = None
				content = prepare_content(content_title, defi, BeautifulSoup)

				toFile.write(
					f'<d:entry id="{_id}" d:title={quoted_title}>\n' +
					generate_indexes(long_title, alts, content, BeautifulSoup) +
					content +
					"\n</d:entry>\n",
				)

			toFile.write("</d:dictionary>\n")

		if xsl:
			shutil.copy(xsl, myResDir)

		if prefs_html:
			shutil.copy(prefs_html, myResDir)

		write_css(filePathBase + ".css", css)

		with open(join(dirname, "Makefile"), mode="w", encoding="utf-8") as toFile:
			toFile.write(
				toStr(pkgutil.get_data(
					__name__,
					"templates/Makefile",
				)).format(dict_name=fileNameBase),
			)

		copyright = glos.getInfo("copyright")
		if BeautifulSoup:
			# strip html tags
			copyright = str(BeautifulSoup.BeautifulSoup(
				copyright,
				features="lxml",
			).text)

		# if DCSDictionaryXSL provided but DCSDictionaryDefaultPrefs <dict/> not
		# present in Info.plist, Dictionary.app will crash.
		with open(filePathBase + ".plist", mode="w", encoding="utf-8") as toFile:
			frontMatterReferenceID = (
				"<key>DCSDictionaryFrontMatterReferenceID</key>\n"
				"\t<string>front_back_matter</string>"
				if front_back_matter
				else ""
			)
			if glos.getInfo("CFBundleIdentifier") is None:
				bundle_id = fileNameBase.replace(" ", "")
			else:
				bundle_id = glos.getInfo("CFBundleIdentifier")
			toFile.write(
				toStr(pkgutil.get_data(
					__name__,
					"templates/Info.plist",
				)).format(
					# identifier must be unique
					CFBundleIdentifier=bundle_id,
					CFBundleDisplayName=glos.getInfo("name"),
					CFBundleName=fileNameBase,
					DCSDictionaryCopyright=copyright,
					DCSDictionaryManufacturerName=glos.author,
					DCSDictionaryXSL=basename(xsl) if xsl else "",
					DCSDictionaryDefaultPrefs=format_default_prefs(default_prefs),
					DCSDictionaryPrefsHTML=basename(prefs_html) if prefs_html else "",
					DCSDictionaryFrontMatterReferenceID=frontMatterReferenceID,
				),
			)

		if jing:
			from .jing import run as jing_run
			jing_run(filePathBase + ".xml")
