#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import glob
import os
import re
import subprocess
import sys

# load modules from parent dir
sys.path.insert(1, os.path.dirname(sys.path[0]))

# import the guts
import mozharness
from mozharness.base.log import DEBUG, ERROR, FATAL
from mozharness.base.transfer import TransferMixin
from mozharness.base.vcs.vcsbase import VCSScript
from mozharness.mozilla.tooltool import TooltoolMixin

external_tools_path = os.path.join(
    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
    "external_tools",
)


class OpenH264Build(TransferMixin, VCSScript, TooltoolMixin):
    all_actions = [
        "clobber",
        "get-tooltool",
        "checkout-sources",
        "build",
        "test",
        "package",
        "dump-symbols",
    ]

    default_actions = [
        "get-tooltool",
        "checkout-sources",
        "build",
        "package",
        "dump-symbols",
    ]

    config_options = [
        [
            ["--repo"],
            {
                "dest": "repo",
                "help": "OpenH264 repository to use",
                "default": "https://github.com/dminor/openh264.git",
            },
        ],
        [
            ["--rev"],
            {"dest": "revision", "help": "revision to checkout", "default": "master"},
        ],
        [
            ["--debug"],
            {
                "dest": "debug_build",
                "action": "store_true",
                "help": "Do a debug build",
            },
        ],
        [
            ["--arch"],
            {
                "dest": "arch",
                "help": "Arch type to use (x64, x86, arm, or aarch64)",
            },
        ],
        [
            ["--os"],
            {
                "dest": "operating_system",
                "help": "Specify the operating system to build for",
            },
        ],
        [
            ["--branch"],
            {
                "dest": "branch",
                "help": "dummy option",
            },
        ],
    ]

    def __init__(
        self,
        require_config_file=False,
        config={},
        all_actions=all_actions,
        default_actions=default_actions,
    ):
        # Default configuration
        default_config = {
            "debug_build": False,
            "upload_ssh_key": "~/.ssh/ffxbld_rsa",
            "upload_ssh_user": "ffxbld",
            "upload_ssh_host": "upload.ffxbld.productdelivery.prod.mozaws.net",
            "upload_path_base": "/tmp/openh264",
        }
        default_config.update(config)

        VCSScript.__init__(
            self,
            config_options=self.config_options,
            require_config_file=require_config_file,
            config=default_config,
            all_actions=all_actions,
            default_actions=default_actions,
        )

    def query_abs_dirs(self):
        if self.abs_dirs:
            return self.abs_dirs
        dirs = super(OpenH264Build, self).query_abs_dirs()
        dirs["abs_upload_dir"] = os.path.join(dirs["abs_work_dir"], "upload")
        self.abs_dirs = dirs
        return self.abs_dirs

    def get_tooltool(self):
        c = self.config
        if not c.get("tooltool_manifest_file"):
            self.info("Skipping tooltool fetching since no tooltool manifest")
            return
        dirs = self.query_abs_dirs()
        self.mkdir_p(dirs["abs_work_dir"])
        manifest = os.path.join(
            dirs["abs_src_dir"],
            "testing",
            "mozharness",
            "configs",
            "openh264",
            "tooltool-manifests",
            c["tooltool_manifest_file"],
        )
        self.info("Getting tooltool files from manifest (%s)" % manifest)
        try:
            self.tooltool_fetch(
                manifest=manifest,
                output_dir=os.path.join(dirs["abs_work_dir"]),
                cache=c.get("tooltool_cache"),
            )
        except KeyError:
            self.error("missing a required key.")

    def query_package_name(self):
        if self.config["arch"] in ("x64", "aarch64"):
            bits = "64"
        else:
            bits = "32"
        version = self.config["revision"]

        if sys.platform in ("linux2", "linux"):
            if self.config.get("operating_system") == "android":
                return "openh264-android-{arch}-{version}.zip".format(
                    version=version, arch=self.config["arch"]
                )
            elif self.config.get("operating_system") == "darwin":
                suffix = ""
                if self.config["arch"] != "x64":
                    suffix = "-" + self.config["arch"]
                return f"openh264-macosx{bits}{suffix}-{version}.zip"
            elif self.config["arch"] == "aarch64":
                return f"openh264-linux64-aarch64-{version}.zip"
            else:
                return f"openh264-linux{bits}-{version}.zip"
        elif sys.platform == "win32":
            if self.config["arch"] == "aarch64":
                return f"openh264-win64-aarch64-{version}.zip"
            else:
                return f"openh264-win{bits}-{version}.zip"
        self.fatal("can't determine platform")

    def query_make_params(self):
        retval = []
        if self.config["debug_build"]:
            retval.append("BUILDTYPE=Debug")

        if self.config["arch"] in ("x64", "aarch64"):
            retval.append("ENABLE64BIT=Yes")
        else:
            retval.append("ENABLE64BIT=No")

        if self.config["arch"] == "x86":
            retval.append("ARCH=x86")
        elif self.config["arch"] == "x64":
            retval.append("ARCH=x86_64")
        elif self.config["arch"] == "aarch64":
            retval.append("ARCH=arm64")
        else:
            self.fatal("Unknown arch: {}".format(self.config["arch"]))

        if "operating_system" in self.config:
            retval.append("OS=%s" % self.config["operating_system"])
            if self.config["operating_system"] == "android":
                retval.append("TARGET=invalid")
                retval.append("NDKLEVEL=%s" % self.config["min_sdk"])
                retval.append("NDKROOT=%s/android-ndk" % os.environ["MOZ_FETCHES_DIR"])
                retval.append("NDK_TOOLCHAIN_VERSION=clang")
            if self.config["operating_system"] == "darwin":
                retval.append("OS=darwin")

        if self._is_windows():
            retval.append("OS=msvc")
            retval.append("CC=clang-cl")
            retval.append("CXX=clang-cl")
            if self.config["arch"] == "aarch64":
                retval.append("CXX_LINK_O=-nologo --target=aarch64-windows-msvc -Fe$@")
        else:
            retval.append("CC=clang")
            retval.append("CXX=clang++")

        return retval

    def query_upload_ssh_key(self):
        return self.config["upload_ssh_key"]

    def query_upload_ssh_host(self):
        return self.config["upload_ssh_host"]

    def query_upload_ssh_user(self):
        return self.config["upload_ssh_user"]

    def query_upload_ssh_path(self):
        return "%s/%s" % (self.config["upload_path_base"], self.config["revision"])

    def run_make(self, target, capture_output=False):
        make = (
            f"{os.environ['MOZ_FETCHES_DIR']}/mozmake/mozmake"
            if sys.platform == "win32"
            else "make"
        )
        cmd = [make, target] + self.query_make_params()
        dirs = self.query_abs_dirs()
        repo_dir = os.path.join(dirs["abs_work_dir"], "openh264")
        env = None
        if self.config.get("partial_env"):
            env = self.query_env(self.config["partial_env"])
        kwargs = dict(cwd=repo_dir, env=env)
        if capture_output:
            return self.get_output_from_command(cmd, **kwargs)
        else:
            return self.run_command(cmd, **kwargs)

    def _git_checkout(self, repo, repo_dir, rev):
        try:
            subprocess.run(["git", "clone", "-q", "--no-checkout", repo, repo_dir])
            subprocess.run(["git", "checkout", "-q", "-f", f"{rev}^0"], cwd=repo_dir)
        except Exception:
            self.rmtree(repo_dir)
            raise
        return True

    def checkout_sources(self):
        repo = self.config["repo"]
        rev = self.config["revision"]

        dirs = self.query_abs_dirs()
        repo_dir = os.path.join(dirs["abs_work_dir"], "openh264")

        if self._is_windows():
            # We don't have git on our windows builders, so download a zip
            # package instead.
            path = repo.replace(".git", "/archive/") + rev + ".zip"
            self.download_file(path)
            self.unzip(rev + ".zip", dirs["abs_work_dir"])
            self.move(
                os.path.join(dirs["abs_work_dir"], "openh264-" + rev),
                os.path.join(dirs["abs_work_dir"], "openh264"),
            )

            # Retrieve in-tree version of gmp-api
            self.copytree(
                os.path.join(dirs["abs_src_dir"], "dom", "media", "gmp", "gmp-api"),
                os.path.join(repo_dir, "gmp-api"),
            )

            # We need gas-preprocessor.pl for arm64 builds
            if self.config["arch"] == "aarch64":
                openh264_dir = os.path.join(dirs["abs_work_dir"], "openh264")
                self.download_file(
                    (
                        "https://raw.githubusercontent.com/libav/"
                        "gas-preprocessor/c2bc63c96678d9739509e58"
                        "7aa30c94bdc0e636d/gas-preprocessor.pl"
                    ),
                    parent_dir=openh264_dir,
                )
                self.chmod(os.path.join(openh264_dir, "gas-preprocessor.pl"), 744)

                # gas-preprocessor.pl expects cpp to exist
                # os.symlink is not available on Windows until we switch to
                # Python 3.
                os.system(
                    "ln -s %s %s"
                    % (
                        os.path.join(
                            os.environ["MOZ_FETCHES_DIR"], "clang", "bin", "clang.exe"
                        ),
                        os.path.join(openh264_dir, "cpp"),
                    )
                )
            return 0

        self.retry(
            self._git_checkout,
            error_level=FATAL,
            error_message="Automation Error: couldn't clone repo",
            args=(repo, repo_dir, rev),
        )

        # Checkout gmp-api
        # TODO: Nothing here updates it yet, or enforces versions!
        if not os.path.exists(os.path.join(repo_dir, "gmp-api")):
            retval = self.run_make("gmp-bootstrap")
            if retval != 0:
                self.fatal("couldn't bootstrap gmp")
        else:
            self.info("skipping gmp bootstrap - we have it locally")

        # Checkout gtest
        # TODO: Requires svn!
        if not os.path.exists(os.path.join(repo_dir, "gtest")):
            retval = self.run_make("gtest-bootstrap")
            if retval != 0:
                self.fatal("couldn't bootstrap gtest")
        else:
            self.info("skipping gtest bootstrap - we have it locally")

        return retval

    def build(self):
        retval = self.run_make("plugin")
        if retval != 0:
            self.fatal("couldn't build plugin")

    def package(self):
        dirs = self.query_abs_dirs()
        srcdir = os.path.join(dirs["abs_work_dir"], "openh264")
        package_name = self.query_package_name()
        package_file = os.path.join(dirs["abs_work_dir"], package_name)
        if os.path.exists(package_file):
            os.unlink(package_file)
        to_package = []
        for f in glob.glob(os.path.join(srcdir, "*gmpopenh264*")):
            if not re.search(
                r"(?:lib)?gmpopenh264(?!\.\d)\.(?:dylib|so|dll|info)(?!\.\d)", f
            ):
                # Don't package unnecessary zip bloat
                # Blocks things like libgmpopenh264.2.dylib and libgmpopenh264.so.1
                self.log(f"Skipping packaging of {f}")
                continue
            to_package.append(os.path.basename(f))
        self.log("Packaging files %s" % to_package)
        cmd = ["zip", package_file] + to_package
        retval = self.run_command(cmd, cwd=srcdir)
        if retval != 0:
            self.fatal("couldn't make package")
        self.copy_to_upload_dir(
            package_file, dest=os.path.join(srcdir, "artifacts", package_name)
        )

        # Taskcluster expects this path to exist, but we don't use it
        # because our builds are private.
        path = os.path.join(
            self.query_abs_dirs()["abs_work_dir"], "..", "public", "build"
        )
        self.mkdir_p(path)

    def dump_symbols(self):
        dirs = self.query_abs_dirs()
        c = self.config
        srcdir = os.path.join(dirs["abs_work_dir"], "openh264")
        package_name = self.run_make("echo-plugin-name", capture_output=True)
        if not package_name:
            self.fatal("failure running make")
        zip_package_name = self.query_package_name()
        if not zip_package_name[-4:] == ".zip":
            self.fatal("Unexpected zip_package_name")
        symbol_package_name = f"{zip_package_name[:-4]}.symbols.zip"
        symbol_zip_path = os.path.join(srcdir, "artifacts", symbol_package_name)
        repo_dir = os.path.join(dirs["abs_work_dir"], "openh264")
        env = None
        if self.config.get("partial_env"):
            env = self.query_env(self.config["partial_env"])
        kwargs = dict(cwd=repo_dir, env=env)
        dump_syms = os.path.join(dirs["abs_work_dir"], c["dump_syms_binary"])
        self.chmod(dump_syms, 0o755)
        python = self.query_exe("python3")
        cmd = [
            python,
            os.path.join(external_tools_path, "packagesymbols.py"),
            "--symbol-zip",
            symbol_zip_path,
            dump_syms,
            os.path.join(srcdir, package_name),
        ]
        self.run_command(cmd, **kwargs)

    def test(self):
        retval = self.run_make("test")
        if retval != 0:
            self.fatal("test failures")

    def copy_to_upload_dir(
        self,
        target,
        dest=None,
        log_level=DEBUG,
        error_level=ERROR,
        compress=False,
        upload_dir=None,
    ):
        """Copy target file to upload_dir/dest.

        Potentially update a manifest in the future if we go that route.

        Currently only copies a single file; would be nice to allow for
        recursive copying; that would probably done by creating a helper
        _copy_file_to_upload_dir().
        """
        dest_filename_given = dest is not None
        if upload_dir is None:
            upload_dir = self.query_abs_dirs()["abs_upload_dir"]
        if dest is None:
            dest = os.path.basename(target)
        if dest.endswith("/"):
            dest_file = os.path.basename(target)
            dest_dir = os.path.join(upload_dir, dest)
            dest_filename_given = False
        else:
            dest_file = os.path.basename(dest)
            dest_dir = os.path.join(upload_dir, os.path.dirname(dest))
        if compress and not dest_filename_given:
            dest_file += ".gz"
        dest = os.path.join(dest_dir, dest_file)
        if not os.path.exists(target):
            self.log("%s doesn't exist!" % target, level=error_level)
            return None
        self.mkdir_p(dest_dir)
        self.copyfile(target, dest, log_level=log_level, compress=compress)
        if os.path.exists(dest):
            return dest
        else:
            self.log("%s doesn't exist after copy!" % dest, level=error_level)
            return None


# main {{{1
if __name__ == "__main__":
    myScript = OpenH264Build()
    myScript.run_and_exit()
