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

import os

from telemetry.internal import forwarders
from telemetry.internal.util import webpagereplay
from telemetry.util import wpr_modes

class ArchiveDoesNotExistError(Exception):
  """Raised when the archive path does not exist for replay mode."""
  pass


class ReplayAndBrowserPortsError(Exception):
  """Raised an existing browser would get different remote replay ports."""
  pass


class NetworkControllerBackend(object):
  """Control network settings and servers to simulate the Web.

  Network changes include forwarding device ports to host platform ports.
  Web Page Replay is used to record and replay HTTP/HTTPS responses.
  """

  def __init__(self, platform_backend):
    self._platform_backend = platform_backend
    self._browser_backend = None
    self._active_replay_args = {}
    self._pending_replay_args = {}
    self._forwarder = None
    self._wpr_server = None

  def SetReplayArgs(self, archive_path, wpr_mode, netsim, extra_wpr_args,
                    make_javascript_deterministic=False):
    """Save the arguments needed for replay.

    To make the settings effective, this call must be followed by a call
    to UpdateReplay.

    Args:
      archive_path: a path to a specific WPR archive.
      wpr_mode: one of wpr_modes.WPR_OFF, wpr_modes.WPR_APPEND,
          wpr_modes.WPR_REPLAY, or wpr_modes.WPR_RECORD.
      netsim: a net_config string ('dialup', '3g', 'dsl', 'cable', or 'fios').
      extra_wpr_args: a list of additional replay args (or an empty list).
      make_javascript_deterministic: True if replay should inject a script
          to make JavaScript behave deterministically (e.g., override Date()).
    """
    if not archive_path:
      # TODO(slamm, tonyg): Ideally, replay mode should be stopped when there is
      # no archive path. However, if the replay server already started, and
      # a file URL is tested with the
      # telemetry.core.local_server.LocalServerController, then the
      # replay server forwards requests to it. (Chrome is configured to use
      # fixed ports fo all HTTP/HTTPS requests.)
      return
    self._pending_replay_args = dict(
        archive_path=archive_path,
        wpr_mode=wpr_mode,
        netsim=netsim,
        extra_wpr_args=extra_wpr_args,
        make_javascript_deterministic=make_javascript_deterministic)
    # TODO(slamm): Update replay here when the browser_backend dependencies
    # are moved to the platform. https://crbug.com/423962
    # |self._pending_replay_args| can be removed at that time.

  def UpdateReplay(self, browser_backend=None):
    """Start or reuse Web Page Replay.

    UpdateReplay must be called after every call to SetReplayArgs.

    TODO(slamm): Update replay in SetReplayArgs once the browser_backend
        dependencies move to platform. https://crbug.com/423962
        browser_backend properties used:
          - Input: wpr_port_pairs
          - Output: wpr_port_pairs (browser uses for --testing-fixed-* flags).
    Args:
      browser_backend: instance of telemetry.core.backends.browser_backend
    """
    if not self._pending_replay_args:
      # In some cases (e.g., unit tests), the browser is used without replay.
      return
    if self._pending_replay_args == self._active_replay_args:
      return

    pending_archive_path = self._pending_replay_args['archive_path']



    pending_wpr_mode = self._pending_replay_args['wpr_mode']
    if pending_wpr_mode == wpr_modes.WPR_OFF:
      return
    if (pending_wpr_mode == wpr_modes.WPR_REPLAY and
        not os.path.exists(pending_archive_path)):
      raise ArchiveDoesNotExistError(
          'Archive path does not exist: %s' % pending_archive_path)
    if browser_backend:
      self._browser_backend = browser_backend
      self.StopReplay()  # stop any forwarder too
      wpr_port_pairs = self._browser_backend.wpr_port_pairs
    else:
      # If no browser_backend, then this is an update for an existing browser.
      assert self._browser_backend
      self._StopReplayOnly()  # leave existing forwarder in place
      wpr_port_pairs = self._forwarder.port_pairs
    wpr_http_port = wpr_port_pairs.http.local_port
    wpr_https_port = wpr_port_pairs.https.local_port
    wpr_dns_port = (wpr_port_pairs.dns.local_port
                    if wpr_port_pairs.dns else None)

    archive_path = self._pending_replay_args['archive_path']
    wpr_mode = self._pending_replay_args['wpr_mode']
    netsim = self._pending_replay_args['netsim']
    extra_wpr_args = self._pending_replay_args['extra_wpr_args']
    make_javascript_deterministic = self._pending_replay_args[
        'make_javascript_deterministic']

    if wpr_mode == wpr_modes.WPR_OFF:
      return
    if (wpr_mode == wpr_modes.WPR_REPLAY and
        not os.path.exists(archive_path)):
      raise ArchiveDoesNotExistError(
          'Archive path does not exist: %s' % archive_path)

    wpr_args = _ReplayCommandLineArgs(
        wpr_mode, netsim, extra_wpr_args, make_javascript_deterministic,
        self._platform_backend.wpr_ca_cert_path)
    self._wpr_server = self._ReplayServer(
        archive_path, self._platform_backend.forwarder_factory.host_ip,
        wpr_http_port, wpr_https_port, wpr_dns_port, wpr_args)
    started_ports = self._wpr_server.StartServer()

    if not self._forwarder:
      self._forwarder = self._platform_backend.forwarder_factory.Create(
          _ForwarderPortPairs(started_ports, wpr_port_pairs))

    self._active_replay_args = self._pending_replay_args
    self._pending_replay_args = None

  def _ReplayServer(
      self, archive_path, host_ip, http_port, https_port, dns_port, wpr_args):
    return webpagereplay.ReplayServer(
        archive_path, host_ip, http_port, https_port, dns_port, wpr_args)

  def StopReplay(self):
    if self._forwarder:
      self._forwarder.Close()
      self._forwarder = None
    self._StopReplayOnly()

  def _StopReplayOnly(self):
    if self._wpr_server:
      self._wpr_server.StopServer()
      self._wpr_server = None
      self._active_replay_args = {}

  @property
  def wpr_http_device_port(self):
    if not self._forwarder or not self._forwarder.port_pairs.http:
      return None
    return self._forwarder.port_pairs.http.remote_port

  @property
  def wpr_https_device_port(self):
    if not self._forwarder or not self._forwarder.port_pairs.https:
      return None
    return self._forwarder.port_pairs.https.remote_port


def _ReplayCommandLineArgs(wpr_mode, netsim, extra_wpr_args,
                           make_javascript_deterministic, wpr_ca_cert_path):
  wpr_args = list(extra_wpr_args)
  if netsim:
    wpr_args.append('--net=%s' % netsim)
  if wpr_mode == wpr_modes.WPR_APPEND:
    wpr_args.append('--append')
  elif wpr_mode == wpr_modes.WPR_RECORD:
    wpr_args.append('--record')
  if not make_javascript_deterministic:
    wpr_args.append('--inject_scripts=')
  if wpr_ca_cert_path:
    wpr_args.extend([
        '--should_generate_certs',
        '--https_root_ca_cert_path=%s' % wpr_ca_cert_path,
        ])
  return wpr_args


def _ForwarderPortPairs(started_ports, wpr_port_pairs):
  """Return PortPairs with started local ports and requested remote ports.

  The local host is where Telemetry is run. The remote is host where
  the target application is run. The local and remote hosts may be
  the same (e.g., testing a desktop browser) or different (e.g., testing
  an android browser).

  The remote ports may be zero. In that case, the forwarder determines
  the remote ports.

  Args:
    started_ports: a tuple of of integer ports from which to forward:
        (HTTP_PORT, HTTPS_PORT, DNS_PORT)  # DNS_PORT may be None
    wpr_port_pairs: a forwarders.PortPairs instance where the remote ports,
        if set, are used.
  Returns:
    a forwarders.PortPairs instance used to create the forwarder.
  """
  local_http_port, local_https_port, local_dns_port = started_ports
  return forwarders.PortPairs(
      forwarders.PortPair(local_http_port, wpr_port_pairs.http.remote_port),
      forwarders.PortPair(local_https_port, wpr_port_pairs.https.remote_port),
      (forwarders.PortPair(local_dns_port, wpr_port_pairs.dns.remote_port)
       if wpr_port_pairs.dns is not None else None))
