# 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 os

# ruff linter deprecates Dict required for Python 3.8 compatibility
from typing import Any, Dict, Optional  # noqa UP035

import yaml

DictAny = Dict[str, Any]
DictStr = Dict[str, str]
OptTestSettings = Optional[DictAny]

# From https://developer.android.com/tools/releases/platforms
# https://apilevels.com/
# https://en.wikipedia.org/wiki/Android_version_history
# testing/mozbase/mozdevice/mozdevice/version_codes.py
# MUST be a 1-1 map
android_os_to_api_map = {
    "7.0": "24",
    "7.1": "25",
    "8.0": "26",
    "8.1": "27",
    "9.0": "28",
    "10.0": "29",
    "11.0": "30",
    "12.0": "31",
    "12.1": "32",
    "13": "33",
    "14": "34",
    "15": "35",
    "16": "36",
}


def android_os_to_api_version(os_version: str):
    api_version = android_os_to_api_map.get(os_version)
    if api_version is None:
        raise Exception(
            f"Unknown Android OS version '{os_version}'. Supported versions are {(android_os_to_api_map.keys())}."
        )
    return api_version


def android_api_to_os_version(api_version: str):
    if not isinstance(api_version, str):
        api_version = str(api_version)
    os_version = None
    for k in android_os_to_api_map.keys():
        if android_os_to_api_map[k] == api_version:
            os_version = k
            break
    if os_version is None:
        raise Exception(
            f"Unknown Android API version '{api_version}'. Supported versions are {android_os_to_api_map.values()}."
        )
    return os_version


class PlatformInfo:

    variant_data = {}

    buildmap = {
        "debug-isolated-process": "isolated_process",
    }

    def __init__(self, test_settings: OptTestSettings = None) -> None:
        if not test_settings:
            return

        self._platform: DictAny = test_settings["platform"]
        self._platform_os: DictStr = self._platform["os"]
        self._build: DictStr = test_settings["build"]
        self._runtime: DictStr = test_settings.get("runtime", {})

        self.build = self._platform_os.get("build")
        self.display = self._platform.get("display")
        self.os = self._clean_os()
        self.os_version = self._clean_os_version()
        self.arch = self._clean_arch()
        self.bits = self._get_bits()
        self.build_type = self._clean_build_type()
        self.opt = self._build["type"] == "opt"
        self.debug = self._build["type"] == "debug"
        self.test_variant = self._clean_test_variant()

    def _clean_os(self) -> str:
        name = self._platform_os["name"]
        if name is None:
            raise Exception("Could not find platform name")

        pretty = name
        if pretty == "windows":
            pretty = "win"
        elif pretty == "macosx":
            pretty = "mac"

        supported_os = ("win", "mac", "linux", "android")
        if pretty not in supported_os:
            raise ValueError(
                f"Unknown os name {pretty}. Supported os are {supported_os}"
            )

        return pretty

    def _clean_os_version(self) -> str:
        version = self._platform_os["version"]
        if version is None:
            raise Exception("Could not find platform version")

        if self.os in ["mac", "linux"]:
            # Hack for macosx 11.20 reported as 11.00
            if self.os == "mac" and version == "1100":
                return "11.20"
            if len(version) == 5 and version[2] == ".":
                return version  # already has a dot
            return version[0:2] + "." + version[2:4]
        if self.os == "android":
            if version not in android_os_to_api_map and version.endswith(".0"):
                version = version[:-2]
            android_version = android_os_to_api_map.get(version)
            if android_version is None:
                raise Exception(
                    f"Unknown android OS version {version}. Supported versions are {list(android_os_to_api_map.keys())}."
                )

        build = self.build
        if build is not None and self.os == "win":
            if build == "24h2":
                version += ".26100"
            else:
                version += "." + build
        return version

    def _clean_arch(self) -> str:
        arch = self._platform["arch"]
        if arch is None:
            raise Exception("Could not find platform architecture")

        if arch == "x86" or arch.find("32") >= 0:
            return "x86"
        elif arch not in ("aarch64", "ppc", "arm7"):
            return "x86_64"
        return arch

    def _get_bits(self) -> str:
        cleaned_arch = self.arch
        if cleaned_arch == "x86":
            return "32"
        return "64"

    def _clean_build_type(self):
        build_type = self._build["type"]
        keys = self._build.keys()
        if len(keys) > 1:
            filtered_types = [x for x in keys if x not in ["type", "shippable"]]
            if len(filtered_types) > 0:
                build_type = filtered_types[0]

        build_type = self.buildmap.get(build_type, build_type)

        # TODO: this is a hack, but these don't apply:
        if build_type in ["devedition", "mingwclang"]:  # only on beta, no mozinfo
            build_type = "opt"
        if self.os == "mac" and build_type == "ccov":  # not scheduled
            build_type = "opt"
        if (
            self.os == "android" and build_type == "lite"
        ):  # no specific way to skip this, treat as normal android
            build_type = "opt"
        return build_type

    def get_variant_data(self):
        if PlatformInfo.variant_data:
            return PlatformInfo.variant_data

        # if running locally via `./mach ...`, assuming running from root of repo
        filename = (
            os.environ.get("GECKO_PATH", ".") + "/taskcluster/test_configs/variants.yml"
        )
        with open(filename) as f:
            PlatformInfo.variant_data = yaml.safe_load(f.read())

        return PlatformInfo.variant_data

    def get_variant_condition(self, test_variant: str) -> str:
        variant_data = self.get_variant_data()
        if test_variant not in variant_data.keys():
            return ""

        mozinfo = variant_data[test_variant].get("mozinfo", "")

        # This is a hack as we have no-fission and fission variants
        # sharing a common mozinfo variable.
        # TODO: what other hacks like this exist?
        if test_variant in ["no-fission", "1proc"]:
            mozinfo = "!" + mozinfo
        return mozinfo

    def _clean_test_variant(self) -> str:
        # TODO: consider adding display here
        runtimes = list(self._runtime.keys())
        test_variant = "+".join(
            [v for v in [self.get_variant_condition(x) for x in runtimes] if v]
        )
        if not runtimes or not test_variant:
            test_variant = "no_variant"
        return test_variant

    # Used for test data
    def from_dict(self, data: DictAny):
        self.os = data["os"]
        self.os_version = data["os_version"]
        self.arch = data["arch"]
        self.bits = data["bits"]
        self.build = data.get("build")
        self.display = data.get("display")
        self.build_type = data["build_type"]
        self.opt = data["opt"]
        self.debug = data["debug"]
        self.test_variant = data["runtime"]

    def to_dict(self):
        return {
            "arch": self.arch,
            "bits": self.bits,
            "build": self.build,
            "build_type": self.build_type,
            "debug": self.debug,
            "display": self.display,
            "opt": self.opt,
            "os": self.os,
            "os_version": self.os_version,
            "runtime": self.test_variant,
        }
