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

try:
  from py_utils import slots_metaclass
  SlotsMetaclass = slots_metaclass.SlotsMetaclass # pylint: disable=invalid-name
except ImportError:
  # TODO(benjhayden): Figure out why py_utils doesn't work in dev_appserver.py
  SlotsMetaclass = None # pylint: disable=invalid-name

from tracing.value.diagnostics import all_diagnostics


class Diagnostic(object):
  __slots__ = '_guid',

  # Ensure that new subclasses remember to specify __slots__ in order to prevent
  # regressing memory consumption:
  if SlotsMetaclass:
    __metaclass__ = SlotsMetaclass

  def __init__(self):
    self._guid = None

  def __ne__(self, other):
    return not self == other

  @property
  def guid(self):
    if self._guid is None:
      self._guid = str(uuid.uuid4())
    return self._guid

  @guid.setter
  def guid(self, g):
    assert self._guid is None
    self._guid = g

  @property
  def has_guid(self):
    return self._guid is not None

  def AsDictOrReference(self):
    if self._guid:
      return self._guid
    return self.AsDict()

  def AsProtoOrReference(self):
    if self._guid:
      return self._guid
    return self.AsProto()

  def AsDict(self):
    dct = {'type': self.__class__.__name__}
    if self._guid:
      dct['guid'] = self._guid
    self._AsDictInto(dct)
    return dct

  def AsProto(self):
    return self._AsProto()

  def _AsDictInto(self, unused_dct):
    raise NotImplementedError()

  def _AsProto(self):
    raise NotImplementedError()

  @staticmethod
  def FromDict(dct):
    cls = all_diagnostics.GetDiagnosticClassForName(dct['type'])
    if not cls:
      raise ValueError('Unrecognized diagnostic type: ' + dct['type'])
    diagnostic = cls.FromDict(dct)
    if 'guid' in dct:
      diagnostic.guid = dct['guid']
    return diagnostic

  @staticmethod
  def FromProto(d):
    # Here we figure out which field is set and downcast to the right diagnostic
    # type. The diagnostic names in the proto must be the same as the class
    # names in the python code, for instance Breakdown.
    attr_name = d.WhichOneof('diagnostic_oneof')
    assert attr_name, 'The diagnostic oneof cannot be empty.'

    d = getattr(d, attr_name)
    assert type(d).__name__ in all_diagnostics.GetDiagnosticTypenames(), (
        'Unrecognized diagnostic type ' + type(d).__name__)

    diag_type = type(d).__name__
    cls = all_diagnostics.GetDiagnosticClassForName(diag_type)

    return cls.FromProto(d)

  def ResetGuid(self, guid=None):
    if guid:
      self._guid = guid
    else:
      self._guid = str(uuid.uuid4())

  def Inline(self):
    """Inlines a shared diagnostic.

    Any diagnostic that has a guid will be serialized as a reference, because it
    is assumed that diagnostics with guids are shared. This method removes the
    guid so that the diagnostic will be serialized by value.

    Inling is used for example in the dashboard, where certain types of shared
    diagnostics that vary on a per-upload basis are inlined for efficiency
    reasons.
    """
    self._guid = None

  def CanAddDiagnostic(self, unused_other_diagnostic):
    return False

  def AddDiagnostic(self, unused_other_diagnostic):
    raise Exception('Abstract virtual method: subclasses must override '
                    'this method if they override canAddDiagnostic')


def Deserialize(type_name, data, deserializer):
  cls = all_diagnostics.GetDiagnosticClassForName(type_name)
  if not cls:
    raise ValueError('Unrecognized diagnostic type: ' + type_name)
  return cls.Deserialize(data, deserializer)
