#!/usr/bin/env python

##############################################################################
# Copyright 2017-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the license found in the
# LICENSE file in the root directory of this source tree.
##############################################################################

import json
import os
import re
import shlex
import shutil
import time

from degrade.degrade_base import DegradeBase, getDegrade
from platforms.platform_base import PlatformBase
from profilers.perfetto.perfetto import Perfetto
from profilers.profilers import getProfilerByUsage
from six import string_types
from utils.custom_logger import getLogger
from utils.utilities import getRunStatus, setRunStatus


class AndroidPlatform(PlatformBase):
    def __init__(self, tempdir, adb, args, usb_controller=None):
        super(AndroidPlatform, self).__init__(
            tempdir,
            args.android_dir,
            adb,
            args.hash_platform_mapping,
            args.device_name_mapping,
        )
        self.args = args
        self.rel_version = adb.getprop("ro.build.version.release")
        self.build_version = adb.getprop("ro.build.version.sdk")
        platform = (
            adb.getprop("ro.product.model")
            + "-"
            + self.rel_version
            + "-"
            + self.build_version
        )
        self.platform_abi = adb.getprop("ro.product.cpu.abi")
        self.os_version = "{}-{}".format(self.rel_version, self.build_version)
        self.type = "android"
        self.setPlatform(platform)
        self.setPlatformHash(adb.device)
        self.usb_controller = usb_controller
        self._setLogCatSize()
        self.app = None
        self.degrade_constraints = None
        self.degrade: DegradeBase = getDegrade(self.type)
        if self.args.set_freq:
            self.util.setFrequency(self.args.set_freq)

    def getKind(self):
        if self.platform_model and self.platform_os_version:
            return "{}-{}".format(self.platform_model, self.platform_os_version)
        return self.platform

    def getOS(self):
        return "Android {} sdk {}".format(self.rel_version, self.build_version)

    def _setLogCatSize(self):
        repeat = True
        size = 131072
        while repeat and size > 256:
            repeat = False
            # We know this command may fail. Avoid propogating this
            # failure to the upstream
            success = getRunStatus()
            ret = self.util.logcat("-G", str(size) + "K")
            setRunStatus(success, overwrite=True)
            if len(ret) > 0 and ret[0].find("failed to") >= 0:
                repeat = True
                size = int(size / 2)

    def fileExistsOnPlatform(self, files):
        if isinstance(files, string_types):
            exists = self.util.shell(
                "test -e {} && echo True || echo False".format(files).split(" ")
            )
            if "True" not in exists:
                return False
            return True
        elif isinstance(files, list):
            for f in files:
                if not self.fileExistsOnPlatform(f):
                    return False
            return True
        raise TypeError(
            "fileExistsOnPlatform takes either a string or list of strings."
        )

    def preprocess(self, *args, **kwargs):
        assert "programs" in kwargs, "Must have programs specified"

        programs = kwargs["programs"]
        benchmark = kwargs["benchmark"]

        # find the first zipped app file
        assert "program" in programs, "program is not specified"

        if "platform" in benchmark["model"] and benchmark["model"][
            "platform"
        ].startswith("android"):
            if "app" in benchmark["model"]:
                self.app = benchmark["model"]["app"]

        self.degrade_constraints = benchmark["tests"][0].get("degrade")

        if not self.app:
            if "intent.txt" in programs:
                # temporary to rename the program with adb suffix
                with open(programs["intent.txt"], "r") as f:
                    self.app = json.load(f)
            else:
                return

        # Uninstall if exist
        package = self.util.shell(["pm", "list", "packages", self.app["package"]])
        if len(package) > 0 and package[0].strip() == "package:" + self.app["package"]:
            self.util.shell(["pm", "uninstall", self.app["package"]])
        # temporary fix to allow install apk files
        if not programs["program"].endswith(".apk"):
            new_name = programs["program"] + ".apk"
            shutil.copyfile(programs["program"], new_name)
            programs["program"] = new_name
        self.util.run(["install", programs["program"]])

        del programs["program"]

    def root(self):
        return self.util.root()

    def unroot(self):
        return self.util.unroot()

    def user_is_root(self):
        return self.util.user_is_root()

    def rebootDevice(self):
        self.util.reboot()
        self.waitForDevice(180)

        # Need to wait a bit more after the device is rebooted
        time.sleep(20)
        # may need to set log size again after reboot
        self._setLogCatSize()
        if self.args.set_freq:
            self.util.setFrequency(self.args.set_freq)

    def runBenchmark(self, cmd, *args, **kwargs):
        if not isinstance(cmd, list):
            cmd = shlex.split(cmd)

        # meta is used to store any data about the benchmark run
        # that is not the output of the command
        meta = {}

        with self.degrade(self.util, self.degrade_constraints):
            # We know this command may fail. Avoid propogating this
            # failure to the upstream
            success = getRunStatus()
            self.util.logcat("-b", "all", "-c")
            setRunStatus(success, overwrite=True)
            if self.app:
                log, meta = self.runAppBenchmark(cmd, *args, **kwargs)
            else:
                log, meta = self.runBinaryBenchmark(cmd, *args, **kwargs)
            return log, meta

    def runAppBenchmark(self, cmd, *args, **kwargs):
        arguments = self.getPairedArguments(cmd)
        argument_filename = os.path.join(self.tempdir, "benchmark.json")
        arguments_json = json.dumps(arguments, indent=2, sort_keys=True)
        with open(argument_filename, "w") as f:
            f.write(arguments_json)
        tgt_argument_filename = os.path.join(self.tgt_dir, "benchmark.json")
        activity = os.path.join(self.app["package"], self.app["activity"])
        self.util.push(argument_filename, tgt_argument_filename)
        platform_args = {}
        if "platform_args" in kwargs:
            platform_args = kwargs["platform_args"]
            if "power" in platform_args and platform_args["power"]:
                platform_args["non_blocking"] = True
                self.util.shell(["am", "start", "-S", activity])
                return []
            if platform_args.get("enable_profiling", False):
                getLogger().warn("Profiling for app benchmarks is not implemented.")

        patterns = []
        pattern = re.compile(
            r".*{}.*{}.*BENCHMARK_DONE".format(
                self.app["package"], self.app["activity"]
            )
        )
        patterns.append(pattern)
        pattern = re.compile(
            r".*ActivityManager: Killing .*{}".format(self.app["package"])
        )
        patterns.append(pattern)
        platform_args["patterns"] = patterns
        self.util.shell(["am", "start", "-S", "-W", activity])
        log_logcat = self.util.run(["logcat"], **platform_args)
        self.util.shell(["am", "force-stop", self.app["package"]])
        return log_logcat

    def runBinaryBenchmark(self, cmd, *args, **kwargs):
        log_to_screen_only = (
            "log_to_screen_only" in kwargs and kwargs["log_to_screen_only"]
        )
        platform_args = {}
        if "platform_args" in kwargs:
            platform_args = kwargs["platform_args"]
            if "taskset" in platform_args:
                taskset = platform_args["taskset"]
                cmd = ["taskset", taskset] + cmd
                del platform_args["taskset"]
            if "sleep_before_run" in platform_args:
                sleep_before_run = str(platform_args["sleep_before_run"])
                cmd = ["sleep", sleep_before_run, "&&"] + cmd
                del platform_args["sleep_before_run"]
            if "power" in platform_args and platform_args["power"]:
                # launch settings page to prevent the phone
                # to go into sleep mode
                self.util.shell(["am", "start", "-a", "android.settings.SETTINGS"])
                time.sleep(1)
                cmd = (
                    ["nohup"]
                    + ["sh", "-c", "'" + " ".join(cmd) + "'"]
                    + [">", "/dev/null", "2>&1"]
                )
                platform_args["non_blocking"] = True
                del platform_args["power"]
            enable_profiling = platform_args.get("profiling_args", {}).get(
                "enabled", False
            )
            if enable_profiling:
                profiler = platform_args["profiling_args"]["profiler"]
                profiling_types = platform_args["profiling_args"]["types"]
                if profiler == "simpleperf":
                    assert profiling_types == [
                        "cpu"
                    ], "Only cpu profiling is supported for SimplePerf"
                    try:
                        # attempt to run with cpu profiling, else fallback to standard run
                        return self._runBenchmarkWithSimpleperf(
                            cmd, log_to_screen_only, **platform_args
                        )
                    except Exception:
                        # if this has not succeeded for some reason reset run status and run without profiling.
                        getLogger().critical(
                            f"An error has occurred when running Simpleperf profiler on device {self.platform} {self.platform_hash}.",
                            exc_info=True,
                        )
                elif profiler == "perfetto":
                    assert (
                        "cpu" not in profiling_types
                    ), "cpu profiling is not yet implemented for Perfetto"
                    try:
                        # attempt Perfetto profiling
                        return self._runBenchmarkWithPerfetto(
                            cmd, log_to_screen_only, **platform_args
                        )
                    except Exception:
                        # if this has not succeeded for some reason reset run status and run without profiling.
                        getLogger().critical(
                            f"An error has occurred when running Perfetto profiler on device {self.platform} {self.platform_hash}.",
                            exc_info=True,
                        )
                else:
                    getLogger().error(
                        f"Ignoring unsupported profiler setting: {profiler}: {profiling_types}.",
                    )

        # Run without profiling
        return self._runBinaryBenchmark(cmd, log_to_screen_only, **platform_args)

    def _runBinaryBenchmark(self, cmd, log_to_screen_only: bool, **platform_args):
        log_screen = self.util.shell(cmd, **platform_args)
        log_logcat = []
        if not log_to_screen_only:
            log_logcat = self.util.logcat("-d")
        return log_screen + log_logcat, {}

    def _runBenchmarkWithSimpleperf(
        self, cmd, log_to_screen_only: bool, **platform_args
    ):
        simpleperf = getProfilerByUsage(
            "android",
            None,
            platform=self,
            model_name=platform_args.get("model_name", None),
            cmd=cmd,
        )
        if simpleperf:
            f = simpleperf.start()
            output, meta = f.result()
            if not output or not meta:
                raise RuntimeError("No data returned from Simpleperf profiler.")
            log_logcat = []
            if not log_to_screen_only:
                log_logcat = self.util.logcat("-d")
            return output + log_logcat, meta

    def _runBenchmarkWithPerfetto(self, cmd, log_to_screen_only: bool, **platform_args):
        # attempt Perfetto profiling
        if not self.util.isRootedDevice(silent=True):
            raise RuntimeError(
                "Attempted to perform Perfetto profiling on unrooted device {self.util.device}."
            )

        with Perfetto(
            platform=self,
            types=platform_args["profiling_args"]["types"],
            options=platform_args["profiling_args"]["options"],
        ) as perfetto:
            getLogger().info("Invoked with Perfetto.")
            log_screen = self.util.shell(cmd, **platform_args)
            log_logcat = []
            if not log_to_screen_only:
                log_logcat = self.util.logcat("-d")
            meta = perfetto.getResults()
            return log_screen + log_logcat, meta

    def collectMetaData(self, info):
        meta = super(AndroidPlatform, self).collectMetaData(info)
        meta["platform_hash"] = self.platform_hash
        return meta

    def killProgram(self, program):
        basename = os.path.basename(program)
        # if the program doesn't exist, the grep may fail
        # do not update status code
        success = getRunStatus()
        res = self.util.shell(["ps", "|", "grep", basename])
        setRunStatus(success, overwrite=True)
        if len(res) == 0:
            return
        results = res[0].split("\n")
        pattern = re.compile(r"^shell\s+(\d+)\s+")
        for result in results:
            match = pattern.match(result)
            if match:
                pid = match.group(1)
                self.util.shell(["kill", pid])

    def waitForDevice(self, timeout):
        period = int(timeout / 20) + 1
        num = int(timeout / period)
        count = 0
        ls = []
        while len(ls) == 0 and count < num:
            ls = self.util.shell(["ls", self.tgt_dir])
            time.sleep(period)
        if len(ls) == 0:
            getLogger().critical(
                f"Cannot reach device {self.platform} {self.platform_hash} after {timeout}s."
            )

    def currentPower(self):
        try:
            result = self.util.shell(["dumpsys", "battery"], retry=10, retry_sleep=2)
            for line in result:
                if "Charge counter" in line:
                    result_line = line
            return int(result_line.split(": ")[-1])
        except Exception:
            getLogger().critical(
                f"Could not read battery level for device {self.platform}  {self.platform_hash}",
                exc_info=True,
            )
            return -1

    @property
    def powerInfo(self):
        return {"unit": "mAh", "metric": "batteryLevel"}
