# Copyright 2017 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 logging

from decorators import cached_property


class DependencyAnalyzer(object):

  def __init__(self, platform, regression_version, regression_range,
               dep_fetcher):
    """Data about the dependencies relating to a crash/regression.

    Properties:
      _platform (str): The platform name; e.g., 'win', 'mac', 'linux',
        'android', 'ios', etc.
      _regression_version (str): The version of project in which the
        crash/regression occurred.
      _regression_range (pair or None): a pair of the last-good and first-bad
        versions.
      _dependencies (dict): A dict from dependency paths to
        ``Dependency`` objects. The keys are all those deps which are
        used by both the ``regression_version`` of the code, and at least
        one frame in the relevant ``stacktrace`` stacks.
      _dependency_rolls (dict) A dict from dependency
        paths to ``DependencyRoll`` objects. The keys are all those
        dependencies which (1) occur in the regression range for the
        ``platform`` where the crash occurred, (2) neither add nor delete
        a dependency, and (3) are also keys of ``dependencies``.
      _dep_fetcher (ChromeDependencyFetcher): Dependency fetcher that can fetch
        all dependencies related to regression_version.
    """
    self._platform = platform
    self._regression_version = regression_version
    self._regression_range = regression_range
    self._dep_fetcher = dep_fetcher

  def GetDependencies(self, stacks_list):
    """Get all dependencies that are in the given stacks."""
    if not stacks_list:
      logging.warning('Cannot get dependencies without stacktrace.')
      return {}

    return {
        frame.dep_path: self.regression_version_deps[frame.dep_path]
        for stack in stacks_list
        for frame in stack.frames
        if frame.dep_path and frame.dep_path in self.regression_version_deps
    }

  def GetDependencyRolls(self, stacks_list):
    """Gets all dependency rolls of ``dependencies`` in regression range."""
    # Short-circuit when we know the deprolls must be empty.
    if not self._regression_range or not stacks_list:
      logging.warning('Cannot get deps and dep rolls for report without '
                      'regression range or stacktrace.')
      return {}

    # Get ``DependencyRoll` objects for all dependencies in the regression
    # range (for this particular platform).
    regression_range_dep_rolls = self._dep_fetcher.GetDependencyRollsDict(
        self._regression_range[0], self._regression_range[1], self._platform)
    # Filter out the ones which add or delete a dependency, because we
    # can't really be sure whether to blame them or not. This rarely
    # happens, so our inability to decide shouldn't be too much of a problem.
    def HasBothRevisions(dep_path, dep_roll):
      has_both_revisions = bool(dep_roll.old_revision) and bool(
          dep_roll.new_revision)
      if not has_both_revisions:
        logging.info(
            'Skip %s dependency %s',
            'added' if dep_roll.new_revision else 'deleted',
            dep_path)
      return has_both_revisions

    # Apply the above filter, and also filter to only retain those
    # which occur in ``GetDependencies``.
    return {
        dep_path: dep_roll
        for dep_path, dep_roll in regression_range_dep_rolls.iteritems()
        if HasBothRevisions(dep_path, dep_roll) and dep_path
        in self.GetDependencies(stacks_list)
    }

  @cached_property
  def regression_version_deps(self):
    """Gets all dependencies related to regression_version.

    N.B. All dependencies will be returned, no matter whether they appeared in
    stacktrace or are related to the crash/regression or not.
    """
    return self._dep_fetcher.GetDependency(
        self._regression_version, self._platform) if self._dep_fetcher else {}

