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

from __future__ import absolute_import
import collections
from telemetry.core import exceptions


def DebuggerUrlToId(debugger_url):
  return debugger_url.split('/')[-1]


class InspectorBackendList(collections.Sequence):
  """A dynamic sequence of active InspectorBackends."""

  def __init__(self, browser_backend):
    """Constructor.

    Args:
      browser_backend: The BrowserBackend instance to query for
          InspectorBackends.
    """
    self._browser_backend = browser_backend
    self._devtools_context_map_backend = None
    # A list of filtered contexts.
    self._filtered_context_ids = []
    # A cache of inspector backends, by context ID.
    self._wrapper_dict = {}

  @property
  def _devtools_client(self):
    return self._browser_backend.devtools_client

  @property
  def app(self):
    return self._browser_backend.app

  def GetContextInfo(self, context_id):
    return self._devtools_context_map_backend.GetContextInfo(context_id)

  def ShouldIncludeContext(self, _):
    """Override this method to control which contexts are included."""
    return True

  def CreateWrapper(self, inspector_backend_instance):
    """Override to return the wrapper API over InspectorBackend.

    The wrapper API is the public interface for InspectorBackend. It
    may expose whatever methods are desired on top of that backend.
    """
    raise NotImplementedError

  # TODO(crbug.com/398467): Remove this method and turn inspector_backend_list
  # API to dictionary-like API.
  def __getitem__(self, index):
    self._Update()
    if index >= len(self._filtered_context_ids):
      raise exceptions.DevtoolsTargetCrashException(
          self.app,
          'Web content with index %s may have crashed. '
          'filtered_context_ids = %s' % (
              index, repr(self._filtered_context_ids)))
    context_id = self._filtered_context_ids[index]
    return self.GetBackendFromContextId(context_id)

  def GetTabById(self, identifier):
    self._Update()
    return self.GetBackendFromContextId(identifier)

  def GetBackendFromContextId(self, context_id):
    self._Update()
    if context_id not in self._wrapper_dict:
      try:
        backend = self._devtools_context_map_backend.GetInspectorBackend(
            context_id)
      except exceptions.Error as e:
        self._HandleDevToolsConnectionError(e)
        raise  # Re-raise exception with original stacktrace.
      # Propagate KeyError from GetInspectorBackend call.
      wrapper = self.CreateWrapper(backend)
      self._wrapper_dict[context_id] = wrapper
    return self._wrapper_dict[context_id]

  def IterContextIds(self):
    self._Update()
    return iter(self._filtered_context_ids)

  def __iter__(self):
    self._Update()
    for context_id in self._filtered_context_ids:
      yield self.GetTabById(context_id)

  def __len__(self):
    self._Update()
    return len(self._filtered_context_ids)

  def _Update(self):
    backends_map = self._devtools_client.GetUpdatedInspectableContexts()
    self._devtools_context_map_backend = backends_map

    # Clear context ids that do not appear in the inspectable contexts.
    context_ids = [context['id'] for context in backends_map.contexts]
    self._filtered_context_ids = [context_id
                                  for context_id in self._filtered_context_ids
                                  if context_id in context_ids]
    # Add new context ids.
    for context in backends_map.contexts:
      if (context['id'] not in self._filtered_context_ids and
          self.ShouldIncludeContext(context)):
        self._filtered_context_ids.append(context['id'])

    # Clean up any backends for contexts that have gone away.
    for context_id in self._wrapper_dict.keys():
      if context_id not in self._filtered_context_ids:
        del self._wrapper_dict[context_id]

  def _HandleDevToolsConnectionError(self, error):
    """Called when handling errors in connecting to the DevTools websocket.

    This can be overwritten by sub-classes to add more debugging information to
    errors.
    """
    pass
