import logging
import os
import subprocess
import time

from enum import Enum

from device import Device, DeviceType
from helper import convert_str_to_bool, get_env_value_or_raise, symlink_force
from constants import ENV, UTF8


class Emulator(Device):
    DEVICE = (
        "Nexus 4",
        "Nexus 5",
        "Nexus 7",
        "Nexus One",
        "Nexus S",
        "Samsung Galaxy S6",
        "Samsung Galaxy S7",
        "Samsung Galaxy S7 Edge",
        "Samsung Galaxy S8",
        "Samsung Galaxy S9",
        "Samsung Galaxy S10",
        "Pixel C"
    )

    API_LEVEL = {
        "9.0": "28",
        "10.0": "29",
        "11.0": "30",
        "12.0": "32",
        "13.0": "33",
        "14.0": "34"
    }

    adb_name_id = 5554

    class ReadinessCheck(Enum):
        BOOTED = "booted"
        RUN_STATE = "in running state"
        WELCOME_SCREEN = "in welcome screen"
        POP_UP_WINDOW = "pop up window"

    def __init__(self, name: str, device: str, android_version: str, data_partition: str,
                 additional_args: str, img_type: str, sys_img: str) -> None:
        super().__init__()
        self.logger = logging.getLogger(self.__class__.__name__)
        self.adb_name = f"emulator-{Emulator.adb_name_id}"
        self.device_type = DeviceType.EMULATOR.value
        self.name = name
        if device in self.DEVICE:
            self.device = device
        else:
            raise RuntimeError(f"device '{device}' is not supported!")
        if android_version in self.API_LEVEL.keys():
            self.android_version = android_version
        else:
            raise RuntimeError(f"android version '{android_version}' is not supported!")
        self.api_level = self.API_LEVEL[self.android_version]
        self.data_partition = data_partition
        self.additional_args = additional_args
        self.img_type = img_type
        self.sys_img = sys_img
        workdir = get_env_value_or_raise(ENV.WORK_PATH)
        self.path_device_profile_target = os.path.join(workdir, ".android", "devices.xml")
        self.path_emulator = os.path.join(workdir, "emulator")
        self.path_emulator_config = os.path.join(workdir, "emulator", "config.ini")
        self.path_emulator_profiles = os.path.join(workdir, "docker-android", "mixins",
                                                   "configs", "devices", "profiles")
        self.path_emulator_skins = os.path.join(workdir, "docker-android", "mixins",
                                                "configs", "devices", "skins")
        self.file_name = self.device.replace(" ", "_").lower()
        self.no_skin = convert_str_to_bool(os.getenv(ENV.EMULATOR_NO_SKIN))
        self.interval_after_booting = 15
        Emulator.adb_name_id += 2
        self.form_data.update({
            self.form_field[Device.FORM_SCREEN_RESOLUTION]: f"{os.getenv(ENV.SCREEN_WIDTH)}x"
                                                            f"{os.getenv(ENV.SCREEN_HEIGHT)}x"
                                                            f"{os.getenv(ENV.SCREEN_DEPTH)}",
            self.form_field[Device.FORM_EMU_DEVICE]: self.device,
            self.form_field[Device.FORM_EMU_ANDROID_VERSION]: self.android_version,
            self.form_field[Device.FORM_EMU_NO_SKIN]: self.no_skin,
            self.form_field[Device.FORM_EMU_DATA_PARTITION]: self.data_partition,
            self.form_field[Device.FORM_EMU_ADDITIONAL_ARGS]: self.additional_args
        })

    def is_initialized(self) -> bool:
        import re
        if os.path.exists(self.path_emulator_config):
            self.logger.info("Config file exists")
            with open(self.path_emulator_config, 'r') as f:
                if any(re.match(r'hw\.device\.name ?= ?{}'.format(self.device), line) for line in f):
                    self.logger.info("Selected device is already created")
                    return True
                else:
                    self.logger.info("Selected device is not created")
                    return False

        self.logger.info("Config file does not exist")
        return False

    def _add_profile(self) -> None:
        if "samsung" in self.device.lower():
            path_device_profile_source = os.path.join(self.path_emulator_profiles,
                                                      "{fn}.xml".format(fn=self.file_name))
            symlink_force(path_device_profile_source, self.path_device_profile_target)
            self.logger.info("Samsung device profile is linked")

    def _use_override_config(self) -> None:
        override_confg_path = os.getenv(ENV.EMULATOR_CONFIG_PATH)

        if override_confg_path is None:
            self.logger.info(f"The environment variable 'EMULATOR_CONFIG_PATH' is not set")
            return

        self.logger.info(f"Environment variable 'EMULATOR_CONFIG_PATH' found: {override_confg_path}")

        if not os.path.isfile(override_confg_path):
            self.logger.warning(f"Source file '{override_confg_path}' does not exist.")
            return

        if not os.access(override_confg_path, os.R_OK):
            self.logger.warning(f"Source file '{override_confg_path}' is not readable.")
            return

        try:
            with open(override_confg_path, 'r') as src, open(self.path_emulator_config, 'a') as dst:
                dst.write(src.read())
            self.logger.info(f"Content from '{override_confg_path}' successfully appended to '{self.path_emulator_config}'.")
        except Exception as e:
            self.logger.error(f"An error occurred while copying file content: {e}")

    def _add_skin(self) -> None:
        device_skin_path = os.path.join(
            self.path_emulator_skins, "{fn}".format(fn=self.file_name))
        with open(self.path_emulator_config, "a") as cf:
            cf.write("hw.keyboard=yes\n")
            cf.write("disk.dataPartition.size={dp}\n".format(dp=self.data_partition))
            cf.write("skin.path={sp}\n".format(
                sp="_no_skin" if self.no_skin else device_skin_path))
        self.logger.info(f"Skin is added in: '{self.path_emulator_config}'")

    def create(self) -> None:
        super().create()
        first_run = not self.is_initialized()
        if first_run:
            self.logger.info(f"Creating the {self.device_type}...")
            self._add_profile()
            creation_cmd = "avdmanager create avd -f -n {n} -b {it}/{si} " \
                           "-k 'system-images;android-{al};{it};{si}' " \
                           "-d {d} -p {pe}".format(n=self.name, it=self.img_type, si=self.sys_img,
                                                   al=self.api_level, d=self.device.replace(" ", "\ "),
                                                   pe=self.path_emulator)
            self.logger.info(f"Command to create emulator: '{creation_cmd}'")
            subprocess.check_call(creation_cmd, shell=True)
            self._add_skin()
            self._use_override_config()
            self.logger.info(f"{self.device_type} is created!")

    def change_permission(self) -> None:
        kvm_path = "/dev/kvm"
        if os.path.exists(kvm_path):
            cmds = (f"sudo chown 1300:1301 {kvm_path}",
                    "sudo sed -i '1d' /etc/passwd")
            for c in cmds:
                subprocess.check_call(c, shell=True)
            self.logger.info("KVM permission is granted!")
        else:
            raise RuntimeError("/dev/kvm cannot be found!")

    def deploy(self):
        self.logger.info(f"Deploying the {self.device_type}")

        basic_cmd = "emulator @{n}".format(n=self.name)
        basic_args = "-gpu swiftshader_indirect -accel on -writable-system -verbose"
        wipe_arg = "-wipe-data" if not self.is_initialized() else ""

        start_cmd = f"{basic_cmd} {basic_args} {wipe_arg} {self.additional_args}"
        self.logger.info(f"Command to run {self.device_type}: '{start_cmd}'")
        subprocess.Popen(start_cmd.split())

    def start(self) -> None:
        super().start()
        self.change_permission()
        self.deploy()

    def check_adb_command(self, readiness_check_type: ReadinessCheck, bash_command: str,
                          expected_keyword: str, max_attempts: int, interval_waiting_time: int,
                          adb_action: str = None) -> None:
        success = False
        for _ in range(1, max_attempts):
            if success:
                break
            else:
                try:
                    output = subprocess.check_output(
                        bash_command.split()).decode(UTF8)
                    if expected_keyword in str(output).lower():
                        if readiness_check_type is self.ReadinessCheck.POP_UP_WINDOW:
                            subprocess.check_call(adb_action, shell=True)
                        else:
                            self.logger.info(
                                f"{self.device_type} is {readiness_check_type.value}!")
                            success = True
                    else:
                        self.logger.info(f"[attempt: {_}] {self.device_type} is not {readiness_check_type.value}! "
                                         f"will check again in {interval_waiting_time} seconds")
                        time.sleep(interval_waiting_time)
                except subprocess.CalledProcessError:
                    self.logger.warning("command cannot be executed! will continue...")
                    time.sleep(2)
                    continue
        else:
            if readiness_check_type is self.ReadinessCheck.POP_UP_WINDOW:
                self.logger.info(f"Pop up windows '{expected_keyword}' is not found!")
            else:
                raise RuntimeError(
                    f"{readiness_check_type.value} is checked {_} times!")

    def wait_until_ready(self) -> None:
        super().wait_until_ready()
        booting_cmd = f"adb -s {self.adb_name} wait-for-device shell getprop sys.boot_completed"
        focus_cmd = f"adb -s {self.adb_name} shell dumpsys window | grep -i mCurrentFocus"
        self.check_adb_command(self.ReadinessCheck.BOOTED,
                               booting_cmd, "1", 60, self.interval_waiting)
        time.sleep(self.interval_after_booting)

        interval_pop_up = 0
        max_attempt_pop_up = 3
        pop_up_system_ui = "Not Responding: com.android.systemui"
        system_ui_cmd = f"adb shell su root 'kill $(pidof com.android.systemui)'"
        pop_up_key_enter = {
            "Not Responding: com.google.android.gms",
            "Not Responding: system",
            "ConversationListActivity"
        }
        key_enter_cmd = "adb shell input keyevent KEYCODE_ENTER"
        self.check_adb_command(self.ReadinessCheck.POP_UP_WINDOW, focus_cmd, pop_up_system_ui,
                               max_attempt_pop_up, interval_pop_up, system_ui_cmd)
        for pe in pop_up_key_enter:
            self.check_adb_command(self.ReadinessCheck.POP_UP_WINDOW, focus_cmd, pe, max_attempt_pop_up,
                                   interval_pop_up, key_enter_cmd)

        self.check_adb_command(self.ReadinessCheck.WELCOME_SCREEN,
                               focus_cmd, "launcheractivity", 60, self.interval_waiting)
        self.logger.info(f"{self.device_type} is ready to use")

    def tear_down(self, *args) -> None:
        self.logger.warning("Sigterm is detected! Nothing to do!")

    def __repr__(self) -> str:
        try:
            return "Emulator(name={n}, device={d}, adb_name={an}, android_version={av}, api_level={al}, " \
                   "data_partition={dp}, additional_args={aa}, img_type={it}, sys_img={si}, " \
                   "path_device_profile_target={pdpt}, path_emulator={pe}, path_emulator_config={pec}, " \
                   "file={f})".format(n=self.name, d=self.device, an=self.adb_name, av=self.android_version,
                                      al=self.api_level, dp=self.data_partition, aa=self.additional_args,
                                      it=self.img_type, si=self.sys_img, pdpt=self.path_device_profile_target,
                                      pe=self.path_emulator, pec=self.path_emulator_config, f=self.file_name)
        except AttributeError as ae:
            self.logger.error(ae)
            return ""
