# Copyright 2016 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 collections import defaultdict
from collections import namedtuple
import logging


class _FrozenDict(dict):
  """An immutable ``dict`` (or some approximation thereof).

  The goal of this class is to render a ``dict`` hashable, so
  that it can be used as a key in other ``dict``s, so that in
  turn we can use our ``MemoizedFunction`` on functions taking
  ``CrashReport`` as an argument.

  For now, we simply define the ``__hash__`` method and assume clients
  will not try to mutate instances after they have been stored as keys
  in another ``dict``. In the future it may be worth taking further
  steps to make it more difficult for clients to do such mutation.

  N.B., the ``__init__`` method will clone the ``dict`` argument. There
  doesn't seem to be an easy way around this.
  """
  def __hash__(self):
      return hash(tuple(sorted(self.items())))


class CrashReport(namedtuple(
    'CrashReport', ['crashed_version', 'signature', 'platform', 'stacktrace',
                    'regression_range', 'dependencies', 'dependency_rolls',
                    'root_repo_url', 'root_repo_path'])):
  """A crash report with all information needed for analysis for clients.

  This class comprises the inputs to the Predator library; as distinguished
  from the Culprit class, which comprises the outputs/results of Predator's
  analyses. N.B., the appengine clients conflate input and output into
  a single CrashAnalysis(ndb.Model) class, but that's up to them; in
  the library we keep inputs and outputs entirely distinct.

  Properties:
    crashed_version (str): The version of Chrome in which the crash occurred.
    signature (str): The signature of the crash on the Chrome crash server.
    platform (str): The platform affected by the crash; e.g., 'win',
      'mac', 'linux', 'android', 'ios', etc.
    stacktrace (Stacktrace): The stacktrace of the crash. N.B., this is
      an object generated by parsing the string containing the stack trace;
      we do not store the string itself.
    regression_range (pair of str or None): a pair of ``last_good_version`` and
      ``first_bad_version``. The regression_range can be ``None``.
    dependencies (_FrozenDict): An immutable dict from dependency paths to
      ``Dependency`` objects. The keys are all those deps which are
      used by both the ``crashed_version`` of the code, and at least
      one frame in the ``stacktrace.crash_stack``.
    dependency_rolls (_FrozenDict) An immutable 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``.
  """
  __slots__ = ()

  def __new__(cls, crashed_version, signature, platform, stacktrace,
              regression_range, dependencies, dependency_rolls,
              root_repo_url=None, root_repo_path=None):
    return super(CrashReport, cls).__new__(
        cls, crashed_version, signature, platform, stacktrace,
        tuple(regression_range) if regression_range else None,
        _FrozenDict(dependencies) if dependencies else _FrozenDict({}),
        _FrozenDict(dependency_rolls) if dependency_rolls else _FrozenDict({}),
        root_repo_url, root_repo_path)
