# NVDA add-on template  SCONSTRUCT file
# Copyright (C) 2012-2023 Rui Batista, Noelia Martinez, Joseph Lee
# This file is covered by the GNU General Public License.
# See the file COPYING.txt for more details.

import codecs
import gettext
import os
import os.path
import zipfile
import sys

# While names imported below are available by default in every SConscript
# Linters aren't aware about them.
# To avoid Flake8 F821 warnings about them they are imported explicitly.
# When using other  Scons functions please add them to the line below.
from SCons.Script import BoolVariable, Builder, Copy, Environment, Variables

sys.dont_write_bytecode = True

# Bytecode should not be written for build vars module to keep the repository root folder clean.
import buildVars  # NOQA: E402


def md2html(source, dest):
	import markdown
	# Use extensions if defined.
	mdExtensions = buildVars.markdownExtensions
	lang = os.path.basename(os.path.dirname(source)).replace('_', '-')
	localeLang = os.path.basename(os.path.dirname(source))
	try:
		_ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang]).gettext
		summary = _(buildVars.addon_info["addon_summary"])
	except Exception:
		summary = buildVars.addon_info["addon_summary"]
	title = "{addonSummary} {addonVersion}".format(
		addonSummary=summary, addonVersion=buildVars.addon_info["addon_version"]
	)
	headerDic = {
		"[[!meta title=\"": "# ",
		"\"]]": " #",
	}
	with codecs.open(source, "r", "utf-8") as f:
		mdText = f.read()
		for k, v in headerDic.items():
			mdText = mdText.replace(k, v, 1)
		htmlText = markdown.markdown(mdText, extensions=mdExtensions)
	# Optimization: build resulting HTML text in one go instead of writing parts separately.
	docText = "\n".join([
		"<!DOCTYPE html>",
		"<html lang=\"%s\">" % lang,
		"<head>",
		"<meta charset=\"UTF-8\">"
		"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
		"<link rel=\"stylesheet\" type=\"text/css\" href=\"../style.css\" media=\"screen\">",
		"<title>%s</title>" % title,
		"</head>\n<body>",
		htmlText,
		"</body>\n</html>"
	])
	with codecs.open(dest, "w", "utf-8") as f:
		f.write(docText)


def mdTool(env):
	mdAction = env.Action(
		lambda target, source, env: md2html(source[0].path, target[0].path),
		lambda target, source, env: 'Generating % s' % target[0],
	)
	mdBuilder = env.Builder(
		action=mdAction,
		suffix='.html',
		src_suffix='.md',
	)
	env['BUILDERS']['markdown'] = mdBuilder


def validateVersionNumber(key, val, env):
	# Used to make sure version major.minor.patch are integers to comply with NV Access add-on store.
	# Ignore all this if version number is not specified, in which case json generator will validate this info.
	if val == "0.0.0":
		return
	versionNumber = val.split(".")
	if len(versionNumber) < 3:
		raise ValueError("versionNumber must have three parts (major.minor.patch)")
	if not all([part.isnumeric() for part in versionNumber]):
		raise ValueError("versionNumber (major.minor.patch) must be integers")


vars = Variables()
vars.Add("version", "The version of this build", buildVars.addon_info["addon_version"])
vars.Add("versionNumber", "Version number of the form major.minor.patch", "0.0.0", validateVersionNumber)
vars.Add(BoolVariable("dev", "Whether this is a daily development version", False))
vars.Add("channel", "Update channel for this build", buildVars.addon_info["addon_updateChannel"])

env = Environment(variables=vars, ENV=os.environ, tools=['gettexttool', mdTool])
env.Append(**buildVars.addon_info)

if env["dev"]:
	import datetime
	buildDate = datetime.datetime.now()
	year, month, day = str(buildDate.year), str(buildDate.month), str(buildDate.day)
	versionTimestamp = "".join([year, month.zfill(2), day.zfill(2)])
	env["addon_version"] = f"{versionTimestamp}.0.0"
	env["versionNumber"] = f"{versionTimestamp}.0.0"
	env["channel"] = "dev"
elif env["version"] is not None:
	env["addon_version"] = env["version"]
if "channel" in env and env["channel"] is not None:
	env["addon_updateChannel"] = env["channel"]

buildVars.addon_info["addon_version"] = env["addon_version"]
buildVars.addon_info["addon_updateChannel"] = env["addon_updateChannel"]

addonFile = env.File("${addon_name}-${addon_version}.nvda-addon")


def addonGenerator(target, source, env, for_signature):
	action = env.Action(
		lambda target, source, env: createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None,
		lambda target, source, env: "Generating Addon %s" % target[0]
	)
	return action


def manifestGenerator(target, source, env, for_signature):
	action = env.Action(
		lambda target, source, env: generateManifest(source[0].abspath, target[0].abspath) and None,
		lambda target, source, env: "Generating manifest %s" % target[0]
	)
	return action


def translatedManifestGenerator(target, source, env, for_signature):
	dir = os.path.abspath(os.path.join(os.path.dirname(str(source[0])), ".."))
	lang = os.path.basename(dir)
	action = env.Action(
		lambda target, source, env: generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) and None,
		lambda target, source, env: "Generating translated manifest %s" % target[0]
	)
	return action


env['BUILDERS']['NVDAAddon'] = Builder(generator=addonGenerator)
env['BUILDERS']['NVDAManifest'] = Builder(generator=manifestGenerator)
env['BUILDERS']['NVDATranslatedManifest'] = Builder(generator=translatedManifestGenerator)


def createAddonHelp(dir):
	docsDir = os.path.join(dir, "doc")
	if os.path.isfile("style.css"):
		cssPath = os.path.join(docsDir, "style.css")
		cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE"))
		env.Depends(addon, cssTarget)
	if os.path.isfile("readme.md"):
		readmePath = os.path.join(docsDir, buildVars.baseLanguage, "readme.md")
		readmeTarget = env.Command(readmePath, "readme.md", Copy("$TARGET", "$SOURCE"))
		env.Depends(addon, readmeTarget)


def createAddonBundleFromPath(path, dest):
	""" Creates a bundle from a directory that contains an addon manifest file."""
	basedir = os.path.abspath(path)
	with zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) as z:
		# FIXME: the include/exclude feature may or may not be useful. Also python files can be pre-compiled.
		for dir, dirnames, filenames in os.walk(basedir):
			relativePath = os.path.relpath(dir, basedir)
			for filename in filenames:
				pathInBundle = os.path.join(relativePath, filename)
				absPath = os.path.join(dir, filename)
				if pathInBundle not in buildVars.excludedFiles:
					z.write(absPath, pathInBundle)
	createAddonStoreJson(dest)
	return dest


def createAddonStoreJson(bundle):
	"""Creates add-on store JSON file from an add-on package and manifest data."""
	import json
	import hashlib
	# Set different json file names and version number properties based on version number parsing results.
	if env["versionNumber"] == "0.0.0":
		env["versionNumber"] = buildVars.addon_info["addon_version"]
	versionNumberParsed = env["versionNumber"].split(".")
	if all([part.isnumeric() for part in versionNumberParsed]):
		if len(versionNumberParsed) == 1:
			versionNumberParsed += ["0", "0"]
		elif len(versionNumberParsed) == 2:
			versionNumberParsed.append("0")
	else:
		versionNumberParsed = []
	if len(versionNumberParsed):
		major, minor, patch = [int(part) for part in versionNumberParsed]
		jsonFilename = f'{major}.{minor}.{patch}.json'
	else:
		jsonFilename = f'{buildVars.addon_info["addon_version"]}.json'
		major, minor, patch = 0, 0, 0
	print('Generating % s' % jsonFilename)
	sha256 = hashlib.sha256()
	with open(bundle, "rb") as f:
		for byte_block in iter(lambda: f.read(65536), b""):
			sha256.update(byte_block)
	hashValue = sha256.hexdigest()
	try:
		minimumNVDAVersion = buildVars.addon_info["addon_minimumNVDAVersion"].split(".")
	except AttributeError:
		minimumNVDAVersion = [0, 0, 0]
	minMajor, minMinor = minimumNVDAVersion[:2]
	minPatch = minimumNVDAVersion[-1] if len(minimumNVDAVersion) == 3 else "0"
	try:
		lastTestedNVDAVersion = buildVars.addon_info["addon_lastTestedNVDAVersion"].split(".")
	except AttributeError:
		lastTestedNVDAVersion = [0, 0, 0]
	lastTestedMajor, lastTestedMinor = lastTestedNVDAVersion[:2]
	lastTestedPatch = lastTestedNVDAVersion[-1] if len(lastTestedNVDAVersion) == 3 else "0"
	channel = buildVars.addon_info["addon_updateChannel"]
	if channel is None:
		channel = "stable"
	addonStoreEntry = {
		"addonId": buildVars.addon_info["addon_name"],
		"displayName": buildVars.addon_info["addon_summary"],
		"URL": "",
		"description": buildVars.addon_info["addon_description"],
		"sha256": hashValue,
		"homepage": buildVars.addon_info["addon_url"],
		"addonVersionName": buildVars.addon_info["addon_version"],
		"addonVersionNumber": {
			"major": major,
			"minor": minor,
			"patch": patch
		},
		"minNVDAVersion": {
			"major": int(minMajor),
			"minor": int(minMinor),
			"patch": int(minPatch)
		},
		"lastTestedVersion": {
			"major": int(lastTestedMajor),
			"minor": int(lastTestedMinor),
			"patch": int(lastTestedPatch)
		},
		"channel": channel,
		"publisher": "",
		"sourceURL": buildVars.addon_info["addon_sourceURL"],
		"license": buildVars.addon_info["addon_license"],
		"licenseURL": buildVars.addon_info["addon_licenseURL"],
	}
	with open(jsonFilename, "w") as addonStoreJson:
		json.dump(addonStoreEntry, addonStoreJson, indent="\t")


def generateManifest(source, dest):
	addon_info = buildVars.addon_info
	with codecs.open(source, "r", "utf-8") as f:
		manifest_template = f.read()
	manifest = manifest_template.format(**addon_info)
	with codecs.open(dest, "w", "utf-8") as f:
		f.write(manifest)


def generateTranslatedManifest(source, language, out):
	_ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext
	vars = {}
	for var in ("addon_summary", "addon_description"):
		vars[var] = _(buildVars.addon_info[var])
	with codecs.open(source, "r", "utf-8") as f:
		manifest_template = f.read()
	result = manifest_template.format(**vars)
	with codecs.open(out, "w", "utf-8") as f:
		f.write(result)


def expandGlobs(files):
	return [f for pattern in files for f in env.Glob(pattern)]


addon = env.NVDAAddon(addonFile, env.Dir('addon'))

langDirs = [f for f in env.Glob(os.path.join("addon", "locale", "*"))]

# Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated
for dir in langDirs:
	poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po"))
	moFile = env.gettextMoFile(poFile)
	env.Depends(moFile, poFile)
	translatedManifest = env.NVDATranslatedManifest(
		dir.File("manifest.ini"),
		[moFile, os.path.join("manifest-translated.ini.tpl")]
	)
	env.Depends(translatedManifest, ["buildVars.py"])
	env.Depends(addon, [translatedManifest, moFile])

pythonFiles = expandGlobs(buildVars.pythonSources)
for file in pythonFiles:
	env.Depends(addon, file)

# Convert markdown files to html
# We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager
createAddonHelp("addon")
for mdFile in env.Glob(os.path.join('addon', 'doc', '*', '*.md')):
	htmlFile = env.markdown(mdFile)
	try:  # It is possible that no moFile was set, because an add-on has no translations.
		moFile
	except NameError:  # Runs if there is no moFile
		env.Depends(htmlFile, mdFile)
	else:  # Runs if there is a moFile
		env.Depends(htmlFile, [mdFile, moFile])
	env.Depends(addon, htmlFile)

# Pot target
i18nFiles = expandGlobs(buildVars.i18nSources)
gettextvars = {
	'gettext_package_bugs_address': 'nvda-translations@groups.io',
	'gettext_package_name': buildVars.addon_info['addon_name'],
	'gettext_package_version': buildVars.addon_info['addon_version']
}

pot = env.gettextPotFile("${addon_name}.pot", i18nFiles, **gettextvars)
env.Alias('pot', pot)
env.Depends(pot, i18nFiles)
mergePot = env.gettextMergePotFile("${addon_name}-merge.pot", i18nFiles, **gettextvars)
env.Alias('mergePot', mergePot)
env.Depends(mergePot, i18nFiles)

# Generate Manifest path
manifest = env.NVDAManifest(os.path.join("addon", "manifest.ini"), os.path.join("manifest.ini.tpl"))
# Ensure manifest is rebuilt if buildVars is updated.
env.Depends(manifest, "buildVars.py")

env.Depends(addon, manifest)
env.Default(addon)
env.Clean(addon, ['.sconsign.dblite', 'addon/doc/' + buildVars.baseLanguage + '/'])