# 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
import markdown

# 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

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

sys.dont_write_bytecode = True


def md2html(source, dest):

    # 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}-dev"
    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)
    env.Depends(htmlFile, mdFile)
    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 + "/"])
