# Helper functions for fsbuild
# This file is automatically generated by fs-package

import datetime
import json
import locale
import os
import platform
import re
import shutil
import subprocess
import sys
import time
from os import path
from typing import Dict, List, Optional

macos = sys.platform == "darwin"
windows = sys.platform == "win32"
linux = sys.platform == "linux"

if windows:
    os_name = "Windows"
elif macos:
    os_name = "macOS"
elif linux:
    os_name = "Linux"
else:
    os_name = "Unknown"

machine = platform.machine()
if machine == "arm64" or machine == "aarch64":
    arch_name = "ARM64"
else:
    arch_name = "x86-64"


class PackageInformation:
    def __init__(self):
        self.values: Dict[str, str] = {}

    @property
    def macos_bundle_id(self) -> str:
        return self.values.get(
            "PACKAGE_BUNDLE_ID", self.values.get("PACKAGE_MACOS_BUNDLE_ID", "")
        )

    @property
    def display_name(self) -> str:
        """E.g. FS-UAE Launcher."""
        return self.values.get("PACKAGE_DISPLAY_NAME", self.pretty_name)

    @property
    def name(self) -> str:
        """E.g. fs-uae-launcher."""
        return self.values["PACKAGE_NAME"]

    @property
    def pretty_name(self) -> str:
        """E.g. FS-UAE-Launcher."""
        return self.values["PACKAGE_NAME_PRETTY"]

    @property
    def type(self):
        return self.values["PACKAGE_TYPE"]

    @property
    def version(self):
        return self.values["PACKAGE_VERSION"]

    @property
    def simple_version(self):
        return "{}.{}.{}".format(
            self.values["PACKAGE_VERSION_MAJOR"],
            self.values["PACKAGE_VERSION_MINOR"],
            self.values["PACKAGE_VERSION_REVISION"],
        )


_packageInformation: Optional[PackageInformation] = None


def getPackageInformation() -> PackageInformation:
    global _packageInformation
    if _packageInformation is None:
        _packageInformation = PackageInformation()
        with open("PACKAGE.FS", "r") as f:
            for line in f:
                try:
                    key, value = line.split("=", 1)
                    key = key.strip()
                    value = value.strip()
                    value = value.strip('"')
                    _packageInformation.values[key] = value
                except ValueError:
                    pass
    return _packageInformation


def getAppName() -> str:
    return getPackageInformation().display_name + ".app"


def getArchitecture() -> str:
    machine = platform.machine()
    if machine == "arm64":
        arch = "ARM64"
    else:
        arch = "x86-64"  # FIXME
    return arch


def getBundleName() -> str:
    packageInformation = getPackageInformation()
    if packageInformation.type == "fs-library-plugin":
        return getFrameworkName()
    else:
        return getAppName()


def getBundlePath(prefix: str = "build/_build/") -> str:
    # prettyName = getPackageInformation().pretty_name
    # bundleName = getBundleName()
    # arch = getArchitecture()
    # path = f"{prefix}{prettyName}/macOS/{arch}/{bundleName}"
    bundle_name = getBundleName()
    path = f"{prefix}/{bundle_name}"
    return path


def getDmgPath() -> str:
    packageInformation = getPackageInformation()
    prettyName = packageInformation.pretty_name
    version = packageInformation.version
    arch = getArchitecture()
    osDist = getOperatingSystemDist()
    path = f"build/_dist/{prettyName}_{version}_{osDist}_{arch}.dmg"
    return path


def getFrameworkName() -> str:
    return getPackageInformation().pretty_name + ".framework"


def getAppleTeamId() -> str:
    return os.environ.get("APPLE_TEAM_ID", "")


def getAppleId() -> str:
    return os.environ.get("APPLE_ID", "")


def getApplePassword() -> str:
    return os.environ.get("APPLE_PASSWORD", "")


def getAppleCodesignIdentity() -> str:
    return os.environ.get(
        "APPLE_CODESIGN_IDENTITY", "Developer ID Application"
    )


def getOperatingSystemDist() -> str:
    envValue = os.environ.get("SYSTEM_OS_DIST", "")
    if envValue:
        return envValue
    elif sys.platform == "linux":
        return "Linux"
    elif sys.platform == "darwin":
        return "macOS"
    elif sys.platform == "win32":
        return "Windows"
    return "Unknown"


def isMacOS() -> bool:
    return sys.platform == "darwin"


def notarizeApp(pathToNotarize: str, bundleId: str):
    assert path.exists(pathToNotarize)
    print(f"Notarizing {path.basename(pathToNotarize)}")
    result = shell(
        "xcrun notarytool submit "
        "--apple-id {appleId} "
        "--password {applePassword} "
        "--team-id {appleTeamId} "
        "--wait "
        "{pathToNotarize}".format(
            appleTeamId=getAppleTeamId(),
            appleId=getAppleId(),
            applePassword=getApplePassword(),
            pathToNotarize=pathToNotarize,
        ),
        censor=True,
    )
    print(result)
    if "status: Accepted" not in result:
        raise Exception("Notarization failed")


def run(args: List[str]) -> str:
    print(quoteArgs(args))
    return subprocess.check_output(args).decode("UTF-8")


def runCodeSign(args: List[str]) -> None:
    # Signing sometimes fails due to Apple errors (timeouts, etc). So we try
    # multiple times before giving up.
    for i in range(20):
        try:
            shell(quoteArgs(args))
        except Exception:
            time.sleep(1.0 * i)
            print("Attempt", i + 2)
        else:
            break
    else:
        raise Exception("Giving up signing")


def rmtree_if_exists(path: str) -> None:
    if os.path.exists(path):
        print("rmtree", path)
        shutil.rmtree(path)


def quoteArg(arg: str) -> str:
    if " " in arg:
        return f'"{arg}"'
    return arg


def quoteArgs(args: List[str]) -> str:
    return " ".join(f"{quoteArg(a)}" for a in args)


def shell(cmd: str, censor: bool = False) -> str:
    if censor:
        print("Running command (censored)")
    else:
        print(cmd)
    return subprocess.run(
        cmd, shell=True, check=True, stdout=subprocess.PIPE
    ).stdout.decode("UTF-8")


class Version:
    def __init__(self, version) -> None:
        self.version = version
        m = re.match("([0-9.]+[0-9])(.*)", version)
        assert m is not None
        parts = m.group(1).split(".")
        assert 2 <= len(parts) <= 4
        self.major = int(parts[0])
        self.minor = int(parts[1])
        self.build: Optional[int]
        if len(parts) > 3:
            self.build = int(parts[3])
        else:
            self.build = None
        if len(parts) > 2:
            self.revision = int(parts[2])
        else:
            self.revision = 0
        self.tag = m.group(2)
        self.commit = ""

    def setLast(self, n: int) -> None:
        if self.build is not None:
            self.build = n
        else:
            self.revision = n

    def __str__(self) -> str:
        numbers = [self.major, self.minor, self.revision]
        if self.build is not None:
            numbers.append(self.build)
        version = ".".join(str(x) for x in numbers)
        return version + self.tag


def numCommitsSince(base: str) -> int:
    to = "HEAD"
    result = int(
        subprocess.check_output(
            ["git", "rev-list", f"{base}..{to}", "--count"]
        ).decode()
    )
    return result


def findLastCommitForFile(path: str) -> str:
    commit = subprocess.check_output(
        ["git", "log", "-n", "1", "--pretty=format:%H", "--", path]
    ).decode()
    return commit


def findlastCommit() -> str:
    commit = subprocess.check_output(
        ["git", "log", "-n", "1", "--pretty=format:%H"]
    ).decode()
    return commit


def updateConfigureAc(version: Version, commit: str = "") -> None:
    print("Updating configure.ac")
    lines = []
    with open("configure.ac", "r", encoding="UTF-8") as f:
        for line in f:
            if line.startswith("m4_define([fsbuild_version"):
                if "_major" in line:
                    k = "FSBUILD_VERSION_MAJOR"
                    v = str(version.major)
                    # d = "Major version"
                elif "_minor" in line:
                    k = "FSBUILD_VERSION_MINOR"
                    v = str(version.minor)
                    # d = "Minor version"
                elif "_revision" in line:
                    k = "FSBUILD_VERSION_REVISION"
                    v = str(version.revision)
                    # d = "Revision"
                else:
                    k = "FSBUILD_VERSION"
                    v = str(version)
                    # d = "Full version"
                line = "m4_define([{}], [{}])\n".format(k.lower(), v)
            # if line.startswith("AC_DEFINE_UNQUOTED([FSBUILD_VERSION"):
            #     if "_MAJOR" in line:
            #         k = "FSBUILD_VERSION_MAJOR"
            #         v = version.major
            #         d = "Major version"
            #     elif "_MINOR" in line:
            #         k = "FSBUILD_VERSION_MINOR"
            #         v = version.minor
            #         d = "Minor version"
            #     elif "_REVISION" in line:
            #         k = "FSBUILD_VERSION_REVISION"
            #         v = version.revision
            #         d = "Revision"
            #     else:
            #         k = "FSBUILD_VERSION"
            #         v = str(version)
            #         d = "Full version"
            #     line = "AC_DEFINE_UNQUOTED([{}], [{}], [{}])\n".format(k, v, d)
            if line.startswith("m4_define([fsbuild_commit"):
                line = "m4_define([{}], [{}])\n".format(
                    "fsbuild_commit", commit
                )
            # if line.startswith("AC_DEFINE_UNQUOTED([FSBUILD_COMMIT"):
            #     k = "FSBUILD_COMMIT"
            #     v = commit
            #     d = "Package commit"
            #     line = "AC_DEFINE_UNQUOTED([{}], [{}], [{}])\n".format(k, v, d)
            lines.append(line)
    with open("configure.ac", "w", encoding="UTF-8") as f:
        for line in lines:
            f.write(line)


def updateDebianChangelog(version: Version) -> None:
    print("Updating debian/changelog")
    lines = []
    first_line = True
    first_line_changed = False
    deb_package = "unknown"
    deb_version = str(version)
    # deb_version = deb_version.replace("alpha", "~alpha")
    # deb_version = deb_version.replace("beta", "~beta")
    # deb_version = deb_version.replace("dev", "~dev")
    with open("debian/changelog", "r", encoding="UTF-8") as f:
        for line in f:
            if first_line:
                first_line = False
                deb_package = line.split(" ", 1)[0]
                lines.append(
                    "{} ({}-0) unstable; urgency=low\n".format(
                        deb_package, deb_version
                    )
                )
                if lines[-1] != line:
                    first_line_changed = True
            elif line.startswith(" -- ") and first_line_changed:
                # Only update date if version was changed
                author, date = line.split("  ")
                date = datetime.datetime.utcnow().strftime(
                    "%a, %d %b %Y %H:%M:%S +0000"
                )
                lines.append("{}  {}\n".format(author, date))
            else:
                lines.append(line)
    with open("debian/changelog", "w", encoding="UTF-8") as f:
        for line in lines:
            f.write(line)


def updateSpecFile(path: str, version: Version) -> None:
    print("Updating", path)
    lines = []
    rpm_version = str(version)
    # rpm_version = rpm_version.replace("alpha", "-0.1alpha")
    # rpm_version = rpm_version.replace("beta", "-0.1~beta")
    # rpm_version = rpm_version.replace("dev", "-0.1dev")
    # if not "-" in rpm_version:
    #     rpm_version += "-1"
    with open(path, "r", encoding="UTF-8") as f:
        for line in f:
            if line.startswith("%define fsbuild_version "):
                lines.append(
                    "%define fsbuild_version {}\n".format(rpm_version)
                )
            # elif line.startswith("%define unmangled_version "):
            #     lines.append("%define unmangled_version {0}\n".format(version))
            else:
                lines.append(line)
    with open(path, "w", newline="\n") as f:
        f.write("".join(lines))


def updatePackageFs(version: Version) -> None:
    print("Updating PACKAGE.FS")
    lines = []
    with open("PACKAGE.FS", "r", encoding="UTF-8") as f:
        for line in f:
            if line.startswith("PACKAGE_VERSION="):
                lines.append(f"PACKAGE_VERSION={str(version)}\n")
            elif line.startswith("PACKAGE_VERSION_MAJOR="):
                lines.append(f"PACKAGE_VERSION_MAJOR={str(version.major)}\n")
            elif line.startswith("PACKAGE_VERSION_MINOR="):
                lines.append(f"PACKAGE_VERSION_MINOR={str(version.minor)}\n")
            elif line.startswith("PACKAGE_VERSION_REVISION="):
                lines.append(
                    f"PACKAGE_VERSION_REVISION={str(version.revision)}\n"
                )
            elif line.startswith("PACKAGE_VERSION_TAG="):
                lines.append(f"PACKAGE_VERSION_TAG={str(version.tag)}\n")
            elif line.startswith("PACKAGE_COMMIT="):
                lines.append(f"PACKAGE_COMMIT={version.commit}\n")
            else:
                lines.append(line)
    with open("PACKAGE.FS", "w", newline="\n") as f:
        f.write("".join(lines))


def updateVersionFs(version: Version) -> None:
    print("Updating VERSION.FS")
    with open("VERSION.FS", "w") as f:
        f.write(str(version))
        f.write("\n")


def updateCommitFs(version: Version) -> None:
    print("Updating COMMIT.FS")
    with open("COMMIT.FS", "w") as f:
        if version.commit:
            f.write(version.commit)
            f.write("\n")


def updatePyProjectToml(version: Version) -> None:
    print("Updating pyproject.toml")
    with open("pyproject.toml", "r") as f:
        text = f.read()
    text = text.replace(
        'version = "0.0.0"',
        f'version = "{version.major}.{version.minor}.{version.revision}"',
    )
    with open("pyproject.toml", "w") as f:
        f.write(text)


def calculateVersion(
    auto_revision: bool = False,
    increment_revision: bool = False,
    include_commit: bool = False,
) -> Version:
    # with open("build/VERSION") as f:
    with open("BASEVERSION.FS") as f:
        version_str = f.read().strip()
    if version_str.startswith("BASEVERSION_FS="):
        version_str = version_str[len("BASEVERSION_FS=") :].strip()
    # with open("PACKAGE.FS") as f:
    #     for line in f:
    #         if line.startswith("PACKAGE_VERSION="):
    #             version_str = line[16:].strip()
    version = Version(version_str)
    if auto_revision:
        version_commit = findLastCommitForFile("BASEVERSION.FS")
        increment = numCommitsSince(version_commit)
        if increment_revision:
            increment += 1
        if version.build is not None:
            version.build += increment
        else:
            version.revision += increment
    if "--commit" in sys.argv:
        version.commit = findlastCommit()

    if True:
        branch = None
        githubRef = os.environ.get("GITHUB_REF")
        if githubRef is not None:
            if githubRef.startswith("refs/heads/"):
                branch = githubRef[len("refs/heads/") :]
            if githubRef.startswith("refs/pull/"):
                branch = "pull" + githubRef[len("refs/pull/") :].replace(
                    "/", ""
                )
        if not branch:
            branch = subprocess.check_output(
                ["git", "symbolic-ref", "--short", "HEAD"], encoding="UTF-8"
            ).strip()

        if branch == "stable":
            version.tag = ""
        elif branch:
            version.tag = f"-{branch}"
        else:
            raise Exception("Cannot calculate version tag from git ref")

    return version


def updateVersion(version: Version) -> None:
    if os.path.exists("VERSION.FS"):
        updateVersionFs(version)
    if os.path.exists("COMMIT.FS"):
        updateCommitFs(version)
    if os.path.exists("configure.ac"):
        updateConfigureAc(version)
    if os.path.exists("debian/changelog"):
        updateDebianChangelog(version)
    if os.path.exists("PACKAGE.FS"):
        updatePackageFs(version)
    if os.path.exists("pyproject.toml"):
        updatePyProjectToml(version)
    for filename in os.listdir("."):
        if filename.endswith(".spec"):
            updateSpecFile(filename, version)


def update_version_command() -> None:
    # For date/time formatting
    locale.setlocale(locale.LC_TIME, "C")

    # auto_revision = "--auto" in sys.argv
    auto_revision = True
    increment_revision = "--next" in sys.argv
    # include_commit = "--commit" in sys.argv
    include_commit = True
    # if "--auto-next" in sys.argv:
    #     auto_revision = True
    #     increment_revision = True
    version = calculateVersion(
        auto_revision=auto_revision,
        increment_revision=increment_revision,
        include_commit=include_commit,
    )
    for arg in sys.argv:
        if arg.startswith("--build="):
            version.build = int(arg[8:])
        elif arg.startswith("--revision="):
            version.revision = int(arg[11:])
        elif arg.startswith("--last="):
            version.setLast(int(arg[7:]))
        elif arg.startswith("--version="):
            version = Version(arg[10:])
    print(str(version))

    if "--print" in sys.argv:
        # Only print version
        pass
    else:
        updateVersion(version)


def buildDmg():
    if not isMacOS():
        print("Not building DMG on non-macOS platform")
        return
    dmgPath = getDmgPath()
    bundlePath = getBundlePath()
    packageInformation = getPackageInformation()
    print(f"Building {path.basename(dmgPath)}")
    if not path.exists(path.dirname(dmgPath)):
        os.makedirs(path.dirname(dmgPath))
    if os.path.exists(dmgPath):
        os.unlink(dmgPath)
    tool = "dmgbuild"
    if tool == "appdmg":
        bundlePath = getBundlePath(prefix="")
        settingsPath = "build/_build/appdmg.json"
        with open(settingsPath, "w", encoding="UTF-8") as f:
            json.dump(
                {
                    "title": packageInformation.display_name,
                    "contents": [
                        {
                            "x": 192,
                            "y": 344,
                            "type": "file",
                            "path": bundlePath,
                        },
                        {
                            "x": 448,
                            "y": 344,
                            "type": "link",
                            "path": "/Applications",
                        },
                    ],
                },
                f,
            )
        subprocess.check_call(
            [
                "appdmg",
                settingsPath,
                dmgPath,
            ]
        )
    elif tool == "dmgbuild":  # type: ignore
        bundlePath = getBundlePath()
        settingsPath = "build/dmgbuild/settings.py"
        # settingsPath = "build/_build/dmgbuild-settings.py"
        # with open(settingsPath, "w", encoding="UTF-8") as f:
        #     f.write("format = 'UDZO'\n")
        #     f.write("files = [\n")
        #     f.write(f"    '{bundlePath}',\n")
        #     f.write("]\n")
        #     f.write("symlinks = { 'Applications': '/Applications' }\n")
        #     f.write("badge_icon = 'icon/fs-uae-launcher.icns'\n")
        subprocess.check_call(
            [
                "dmgbuild",
                "-s",
                settingsPath,
                "-D",
                f'app="{bundlePath}"',
                "FS-UAE-Launcher",
                dmgPath,
            ]
        )
    else:
        raise Exception("Unknown dmg builder")


def signDmg():
    if not isMacOS():
        print("Not signing DMG on non-macOS platform")
        return
    args = [
        "codesign",
        "--force",
        "--sign",
        getAppleCodesignIdentity(),
        "--digest-algorithm=sha1,sha256",
    ]
    args.append(getDmgPath())
    runCodeSign(args)


def notarizeDmg():
    if not isMacOS():
        print("Not notarizing DMG on non-macOS platform")
        return
    packageInformation = getPackageInformation()
    bundleId = packageInformation.macos_bundle_id
    dmgPath = getDmgPath()
    assert path.exists(dmgPath)
    notarizeApp(dmgPath, bundleId)

    print('xcrun stapler staple "{}"'.format(dmgPath))
    assert os.system('xcrun stapler staple "{}"'.format(dmgPath)) == 0

    print("-" * 80)
    print(f"[BUILD] Notarized {dmgPath}")


def upload_branch_name():
    ref = os.getenv("GITHUB_REF", "")
    try:
        branch = ref.split("refs/heads/", 1)[1]
    except IndexError:
        return None
    if "/" in branch:
        return None
    return branch[0].upper() + branch[1:]
