# 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 pathlib
import platform
import re
import subprocess

import mozversion

from mozperftest.utils import (
    CHROME_DESKTOP_APPS,
    CHROME_MOBILE_APPS,
    FIREFOX_DESKTOP_APPS,
    FIREFOX_MOBILE_APPS,
    ON_TRY,
    get_adb_device_or_emu,
)


class MultipleApplicationSetups(Exception):
    """Raised when there are multiple setup classes for an application."""

    pass


app_setups = {}


def binary_setup(klass):
    for app in klass.apps:
        if app in app_setups:
            raise MultipleApplicationSetups(f"{app} already has a setup class")
        app_setups[app] = klass
    return klass


def get_app_setup(app):
    app_setup = app_setups.get(app, None)
    return app_setup


class BaseVersionProducer:
    def __init__(self, application, logger):
        self.application = application
        self.logger = logger

    def get_binary_version(self, binary, **kwargs):
        # Try to get the version info through mozversion, and if that
        # fails then let the Desktop, or Mobile handle it
        application_info = mozversion.get_version(binary=binary)
        return application_info.get("application_version")


class DesktopVersionProducer(BaseVersionProducer):
    def get_binary_version(self, binary, **kwargs):
        try:
            return super().get_binary_version(binary)
        except Exception:
            pass

        version = None
        try:
            if (
                "mac" in platform.system().lower()
                or "darwin" in platform.system().lower()
            ):
                import plistlib

                for plist_file in ("version.plist", "Info.plist"):
                    try:
                        binary_path = pathlib.Path(binary)
                        plist_path = binary_path.parent.parent.joinpath(plist_file)
                        with plist_path.open("rb") as plist_file_content:
                            plist = plistlib.load(plist_file_content)
                    except FileNotFoundError:
                        pass
                version = plist.get("CFBundleShortVersionString")
            elif "linux" in platform.system().lower():
                command = [binary, "--version"]
                proc = subprocess.run(
                    command, check=True, timeout=10, capture_output=True, text=True
                )

                bmeta = proc.stdout.split("\n")
                meta_re = re.compile(r"([A-z\s]+)\s+([\w.]*)")
                if len(bmeta) != 0:
                    match = meta_re.match(bmeta[0])
                    if match:
                        version = match.group(2)
            else:
                # Define the PowerShell command. We use this method on Windows since WMIC will
                # soon be deprecated.
                binary_path = pathlib.Path(binary)
                command = (
                    rf'(Get-ItemProperty -Path "{binary_path}").VersionInfo.FileVersion'
                )
                self.logger.info(
                    "Attempting to get browser application version with powershell..."
                )
                bmeta = subprocess.check_output(
                    ["powershell", "-Command", command],
                    text=True,
                )
                if not bmeta:
                    self.logger.warning("Unable to acquire browser version")
                else:
                    version = bmeta.strip()
                    self.logger.info(
                        f"Successfully acquired browser version: {version}"
                    )
        except Exception as e:
            self.logger.warning(
                f"Failed to get browser meta data through fallback method: {e.__class__.__name__}-{e}"
            )
            raise e

        return version


class MobileVersionProducer(BaseVersionProducer):
    def get_binary_version(self, binary, apk_path=None, **kwargs):
        try:
            return super().get_binary_version(apk_path or binary)
        except Exception:
            pass

        device = get_adb_device_or_emu()
        pkg_info = device.shell_output(f"dumpsys package {binary}")
        version_matcher = re.compile(r".*versionName=([\d.]+)")
        for line in pkg_info.split("\n"):
            match = version_matcher.match(line)
            if match:
                return match.group(1)

        return None


class BaseSetup:
    version_producer = DesktopVersionProducer

    def __init__(self, application, layer_obj):
        self.application = application
        self.logger = layer_obj
        self.get_binary_path = layer_obj.mach_cmd.get_binary_path

    def get_binary_version(self, binary, **kwargs):
        return self.__class__.version_producer(
            self.application, self.logger
        ).get_binary_version(binary, **kwargs)


@binary_setup
class FirefoxSetup(BaseSetup):
    apps = FIREFOX_DESKTOP_APPS

    def setup_binary(self):
        if ON_TRY:
            self.logger.warning(
                "Cannot setup Firefox binary automatically in CI. Provide the path "
                "with --binary."
            )
            return None
        return self.get_binary_path()


@binary_setup
class ChromeSetup(BaseSetup):
    apps = CHROME_DESKTOP_APPS

    def setup_binary(self):
        if ON_TRY:
            self.logger.warning(
                "Cannot setup Chrome binary automatically in CI. Provide the path "
                "with --binary."
            )
            return None
        if "win" in platform.system().lower():
            self.logger.warning(
                "Getting the chrome binary on windows may not work. "
                "Use --binary to provide a path to it."
            )
            cmd = "where"
            executable = "chrome.exe"
        else:
            cmd = "which"
            executable = "google-chrome"
        return subprocess.check_output([cmd, executable]).decode("utf-8").strip()


@binary_setup
class ChromeMobileSetup(BaseSetup):
    apps = CHROME_MOBILE_APPS
    version_producer = MobileVersionProducer

    def setup_binary(self):
        return "com.android.chrome"


@binary_setup
class FirefoxMobileSetup(BaseSetup):
    apps = FIREFOX_MOBILE_APPS
    version_producer = MobileVersionProducer

    def setup_binary(self):
        return {
            "focus": "org.mozilla.focus",
            "geckoview": "org.mozilla.geckoview_example",
            "fenix": "org.mozilla.fenix",
        }[self.application]
