# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import abc
import subprocess
import jinja2
import tempfile
import datetime
import logging
import typing
import os
import pyautogui
from AppKit import NSBundle  # used to suppress macOS dock icon pop up/bounce

import utils
import browsers


def GetTemplateFileForBrowser(
    browser_driver: browsers.BrowserDriver, template_file: str
) -> str:
    if browser_driver.name == "safari":
        return f"safari_{template_file}"
    else:
        return template_file


class ScenarioOSADriver(abc.ABC):
    """Base Class encapsulating OSA script driving a scenario, with setup
    and tear down.
    """

    def __init__(self, scenario_name, duration: datetime.timedelta):
        self.name = scenario_name
        self.script_process = None
        self.osa_script = None
        self.duration = duration
        self.tag = ""

    def Launch(self):
        """Starts the driver script."""
        app_info = NSBundle.mainBundle().infoDictionary()
        # Suppress macOS dock icon pop up/bounce.
        app_info["LSBackgroundOnly"] = "1"

        # Disable aborting the sequence of movements by moving to the corner of
        # the screen. This is fine because it is a single move, not a sequence.
        pyautogui.FAILSAFE = False

        # Move the cursor in the middle of the screen so it's always in the same
        # spot. This cannot be 0,0 as this keeps the task bar visible in
        # full-screen scenarios.
        width, height = pyautogui.size()
        pyautogui.moveTo(width / 2, height / 2)

        assert self.osa_script is not None
        logging.debug(f"Starting scenario {self.name}")
        self.script_process = subprocess.Popen(
            ['osascript', self.osa_script.name]
        )

    def Wait(self):
        """Waits for the script to complete."""
        assert self.script_process is not None, "Driver wasn't launched."
        logging.debug(f"Waiting for scenario {self.name}")
        self.script_process.wait()

    def TearDown(self):
        """Terminates the script if currently running and ensures processes
        are cleaned up.
        """
        logging.debug(f"Tearing down scenario {self.name}")
        if self.script_process:
            utils.TerminateProcess(self.script_process)
        self.osa_script.close()

    @abc.abstractmethod
    def Summary(self):
        """Returns a dictionary describing the scenarios parameters."""
        pass

    def CycleDuration(self) -> datetime.timedelta:
        """Returns the duration of a cycle in the scenario. Not all
        scenario have a notion of cycles. For those which don't a
        cycle is defined as the duration of the whole scenario"""
        return self.duration

    def IsRunning(self) -> bool:
        """Returns true if the script is currently running."""
        return self.script_process.poll() is None

    def _CompileTemplate(self, template_file: str, extra_args: typing.Dict):
        """Compiles script `template_file`, feeding `extra_args` to a temp
        file.
        """
        loader = jinja2.FileSystemLoader(
            os.path.join(os.path.dirname(__file__), "driver_scripts_templates")
        )
        env = jinja2.Environment(loader=loader)
        template = env.get_template(template_file)
        self.osa_script = tempfile.NamedTemporaryFile('w+t')
        self.osa_script.write(template.render(**extra_args))
        self.osa_script.flush()
        self._args = extra_args

    def Summary(self):
        """Returns a dictionary describing the scenarios parameters."""
        return {'name': self.name, 'tag': self.tag, **self._args}


class ScenarioWithBrowserOSADriver(ScenarioOSADriver):
    """Specialisation for OSA script that runs with a browser."""

    def __init__(
        self,
        scenario_name,
        browser_driver: browsers.BrowserDriver,
        duration: datetime.timedelta,
    ):
        super().__init__(f"{browser_driver.name}_{scenario_name}", duration)
        self.browser = browser_driver

    def Launch(self):
        self.browser.Launch()
        super().Launch()

    def TearDown(self):
        super().TearDown()
        self.browser.TearDown()

    def Summary(self):
        """Returns a dictionary describing the scenarios parameters."""
        return {**super().Summary(), 'browser': self.browser.Summary()}

    def _CompileTemplate(self, template_file, extra_args: typing.Dict):
        return super()._CompileTemplate(
            template_file, {"browser": self.browser.process_name, **extra_args}
        )


class IdleScenario(ScenarioOSADriver):
    """Scenario that lets the system idle."""

    def __init__(self, duration: datetime.timedelta, scenario_name="idle"):
        super().__init__(scenario_name, duration)
        self._CompileTemplate(
            "idle",
            {
                "delay": duration.total_seconds(),
            },
        )


class IdleOnSiteScenario(ScenarioWithBrowserOSADriver):
    """Scenario that lets a browser idle on a web page."""

    def __init__(
        self,
        browser_driver: browsers.BrowserDriver,
        duration: datetime.timedelta,
        site_url: str,
        scenario_name,
        send_full_screen_key,
    ):
        super().__init__(scenario_name, browser_driver, duration)
        self._CompileTemplate(
            GetTemplateFileForBrowser(browser_driver, "idle_on_site"),
            {
                "idle_site": site_url,
                "delay": duration.total_seconds(),
                "send_full_screen_key": send_full_screen_key,
            },
        )

    @staticmethod
    def Wiki(
        browser_driver: browsers.BrowserDriver, duration: datetime.timedelta
    ):
        return IdleOnSiteScenario(
            browser_driver,
            duration,
            "http://www.wikipedia.com/wiki/Alessandro_Volta",
            "idle_on_wiki",
            send_full_screen_key=False,
        )

    @staticmethod
    def Youtube(
        browser_driver: browsers.BrowserDriver, duration: datetime.timedelta
    ):
        return IdleOnSiteScenario(
            browser_driver,
            duration,
            # A nature video confirmed to use AV1 and that lasts long enough.
            # Set to always start at time 1, no matter the progress previously.
            "https://www.youtube.com/watch?v=rV_ERKtNyNA?t=1",
            "idle_on_youtube",
            send_full_screen_key=True,
        )

    @staticmethod
    def Netflix(
        browser_driver: browsers.BrowserDriver, duration: datetime.timedelta
    ):
        return IdleOnSiteScenario(
            browser_driver,
            duration,
            # A movie that lasts long enough. Set to always restart at time 0.
            "https://www.netflix.com/watch/81198930?t=0",
            "idle_on_netflix",
            send_full_screen_key=True,
        )


class ZeroWindowScenario(ScenarioWithBrowserOSADriver):
    """Scenario that lets a browser idle with no window."""

    def __init__(
        self,
        browser_driver: browsers.BrowserDriver,
        duration: datetime.timedelta,
        scenario_name="zero_window",
    ):
        super().__init__(scenario_name, browser_driver, duration)
        self._CompileTemplate(
            GetTemplateFileForBrowser(browser_driver, "zero_window"),
            {
                "delay": duration.total_seconds(),
            },
        )


class NavigationScenario(ScenarioWithBrowserOSADriver):
    """Scenario that has a browser navigating on web pages in a loop."""

    def __init__(
        self,
        browser_driver: browsers.BrowserDriver,
        navigation_duration: datetime.timedelta,
        navigation_cycles: int,
        sites,
        scenario_name,
    ):
        super().__init__(
            scenario_name,
            browser_driver,
            navigation_duration * navigation_cycles * len(sites),
        )
        self._CompileTemplate(
            GetTemplateFileForBrowser(browser_driver, "navigation"),
            {
                "per_navigation_delay": navigation_duration.total_seconds(),
                "navigation_cycles": navigation_cycles,
                "sites": ",".join([f'"{site}"' for site in sites]),
            },
        )

        self.cycle_duration = len(sites) * navigation_duration

    def CycleDuration(self) -> datetime.timedelta:
        return self.cycle_duration


class MeetScenario(ScenarioWithBrowserOSADriver):
    """Scenario that has the browser join a Google Meet room."""

    def __init__(
        self,
        browser_driver: browsers.BrowserDriver,
        duration: datetime.timedelta,
        meeting_id: int,
        scenario_name="meet",
    ):
        super().__init__(scenario_name, browser_driver, duration)
        self._CompileTemplate(
            GetTemplateFileForBrowser(browser_driver, "meet"),
            {"delay": duration.total_seconds(), "meeting_id": meeting_id},
        )


def MakeScenarioDriver(
    scenario_name, browser_driver: browsers.BrowserDriver, meet_meeting_id=None
) -> ScenarioOSADriver:
    """Creates scenario driver by name.

    Args:
      scenario_name: Identifier for the scenario to create. Supported scenarios
        are: meet, idle_on_wiki, idle_on_youtube, idle_on_netflix,
        navigation_top_sites,navigation_heavy_sites, zero_window and idle.
      browser_driver: Browser the scenario is created with.
      meet_meeting_id: Optional meeting id used for meet scenario.
    """

    if "idle" == scenario_name:
        return IdleScenario(datetime.timedelta(minutes=60))
    if not browser_driver:
        return None
    if "prep" == scenario_name:
        return IdleOnSiteScenario.Wiki(
            browser_driver, datetime.timedelta(minutes=1)
        )
    if "meet" == scenario_name:
        return MeetScenario(
            browser_driver,
            datetime.timedelta(minutes=60),
            meeting_id=meet_meeting_id,
        )
    if "idle_on_wiki" == scenario_name:
        return IdleOnSiteScenario.Wiki(
            browser_driver, datetime.timedelta(minutes=60)
        )
    if "idle_on_youtube" == scenario_name:
        return IdleOnSiteScenario.Youtube(
            browser_driver, datetime.timedelta(minutes=60)
        )
    if "idle_on_netflix" == scenario_name:
        return IdleOnSiteScenario.Netflix(
            browser_driver, datetime.timedelta(minutes=60)
        )

    if scenario_name.startswith("navigation"):
        if "navigation_top_sites" == scenario_name:
            NAVIGATED_SITES = [
                "https://amazon.com",
                "https://www.amazon.com/s?k=computer&ref=nb_sb_noss_2",
                "https://google.com",
                "https://www.google.com/search?q=computers",
                "https://www.youtube.com",
                "https://www.youtube.com/results?search_query=computers",
                "https://docs.google.com/document/d/1Ll-8Nvo6JlhzKEttst8GHWCc7_A8Hluy2fX99cy4Sfg/edit?usp=sharing",
            ]
        elif "navigation_heavy_sites" == scenario_name:
            NAVIGATED_SITES = [
                "https://www.chase.com/",
                "https://www.nytimes.com/ca/section/technology",
                "https://www.macys.com/",
                "https://fr.airbnb.ca/s/Montr%C3%A9al/homes?place_id=ChIJDbdkHFQayUwR7-8fITgxTmU&query=Montr%C3%A9al&refinement_paths%5B%5D=%2Fhomes&tab_id=home_tab",
                "https://www.homedepot.ca/search?q=computer#!q=computer",
                "https://polygon.com",
            ]
        else:
            # For navigation scenarios that are not predefined a list is needed.
            if os.path.exists("sites.txt"):
                NAVIGATED_SITES = []
                with open("sites.txt") as sites_file:
                    for site in sites_file:
                        NAVIGATED_SITES.append(site.replace("\n", ""))
            else:
                raise ValueError(
                    "Use predefined navigation scenarios or create sites.txt"
                )

        # Aim for a benchmark that lasts up to 1 hour.
        navigation_duration_in_seconds = 15
        navigation_cycles = (
            (6 * 60) / len(NAVIGATED_SITES) / navigation_duration_in_seconds
        )

        return NavigationScenario(
            browser_driver,
            navigation_duration=datetime.timedelta(
                seconds=navigation_duration_in_seconds
            ),
            navigation_cycles=navigation_cycles,
            sites=NAVIGATED_SITES,
            scenario_name=scenario_name,
        )
    if "zero_window" == scenario_name:
        return ZeroWindowScenario(
            browser_driver, datetime.timedelta(minutes=60)
        )
    return None
