########################################################################
# Copyright (C) 2019-2020 VMware, Inc.                                 #
# All Rights Reserved                                                  #
########################################################################
#

'''
This module defines the data structure of manifest and implements the
functionalities such as construction, serialization to json format,
and deserialization from json.
'''

from .Addon import Addon
from .Errors import ManifestValidationError
from .ReleaseUnit import (ATTR_REL_ID, ReleaseUnit, deepcopy)

import json

try:
   from .Utils.JsonSchema import ValidateManifest
   HAVE_VALIDATE_MANIFEST = True
except Exception:
   HAVE_VALIDATE_MANIFEST = False

NAME_HSI = 'hardwareSupportInfo'
# Prefix for firmware-only manifests that are generated on host.
# Presence of the prefix allows the manifest to contain no component.
FIRMWARE_ONLY_PREFIX = 'FW-'

replaceSpace = lambda x: '_'.join(x.split())

class HardwareSupportManager(object):
   '''Class for hardware support manager.
   '''
   def __init__(self, name):
      self.name = name

   def __eq__(self, other):
      return (isinstance(other, HardwareSupportManager) and
              self.name == other.name)

class HardwareSupportPackage(object):
   '''Class for hardware support  package.
    '''
   def __init__(self, name, version):
      self.name = name
      self.version = version

   def __eq__(self, other):
      return (isinstance(other, HardwareSupportPackage) and
              self.name == other.name and self.version == other.version)

class HardwareSupportInfo(object):
   '''Class for hardware support info. It has a hardware support manager and
      a hardware support package.
   '''
   def __init__(self, manager, package):
      self.manager = manager
      self.package = package

   def __eq__(self, other):
      return (isinstance(other, HardwareSupportInfo) and
              self.manager == other.manager and self.package == other.package)

def _DictToHSI(hsiDict):
   '''Convert dict to HardwareSupportInfo.
   '''
   manager = HardwareSupportManager(hsiDict['manager']['name'])
   packageDict = hsiDict['package']
   package = HardwareSupportPackage(packageDict['name'], packageDict['version'])
   return HardwareSupportInfo(manager, package)

class Manifest(Addon):
   ''' A hardware manifest is an addon that has extra members for hardware
       support information.
   '''

   extraAttributes = [NAME_HSI] + Addon.extraAttributes
   extraDefault = [None] + Addon.extraDefault
   extraMap = dict(zip(extraAttributes, extraDefault))

   mandatoryAttr = list(Addon.mandatoryAttr)
   mandatoryAttr.extend([NAME_HSI])

   # The schema version.
   SCHEMA_VERSION = "1.0"

   # Valid schema veriosn map to release version. Need to populate when bump
   # schema version.
   SCHEMA_VERSION_MAP = {"1.0": '7.0.0'}

   # The common software spec type for all instances of this class.
   releaseType = 'manifest'

   ReleaseUnit.typeConverters[NAME_HSI] = _DictToHSI

   @property
   def isFirmwareOnly(self):
      """Returns if this manifest is a firmware-only one.
      """
      return self.nameSpec.name.startswith(FIRMWARE_ONLY_PREFIX)

   def Validate(self, components=None, manifestVibs=None):
      """Validates the manifest. Manifest should have at least one component and
         there should be no conflict/obsolete problems within the components.
         With a firmware-only Hardware Support Package, a manifest object is
         created on the host, which has no component and removed component name.

         Parameters:
            * components - ComponentCollection object having all manifest
                           components.
            * manifestVibs  - VibCollection object with VIBs that correspond to
                              all components in manifest.
      """
      if (not self.isFirmwareOnly and not self.components and
          not self.removedComponents):
         raise ManifestValidationError(self.releaseID,
            'Manifest should have at least one component or at least remove '
            'one component.')
      if components and manifestVibs:
         compProblems = self._getCompProblemMsgs(components, manifestVibs)
         if compProblems:
            raise ManifestValidationError(self.releaseID,
               'Failed to validate components in manifest %s: %s'
               % (self.nameSpec.name, ', '.join(compProblems)))

   @classmethod
   def FromJSON(cls, jsonString, validation=False):
      # Schema Validation
      if validation and HAVE_VALIDATE_MANIFEST:
         valid, errMsg = ValidateManifest(jsonString)
         if not valid:
            try:
               manifest = json.loads(jsonString)
            except Exception:
               # failed to load manifest from jsonstring
               # hence return empty releaseID
               raise ManifestValidationError('', errMsg)
            releaseId = manifest[ATTR_REL_ID] if ATTR_REL_ID in manifest else ''
            raise ManifestValidationError(releaseId, errMsg)

      manifest = Manifest(spec=jsonString)
      if validation:
         manifest.Validate()
      return manifest

   def ToJSON(self):
      self.Validate()
      # calling ReleaseUnit class's ToJSON() method to skip the validation
      # logic implemented in Addon class
      jsonString = ReleaseUnit.ToJSON(self)

      # Schema Validation
      if HAVE_VALIDATE_MANIFEST:
         valid, errMsg = ValidateManifest(jsonString)
         if not valid:
            raise ManifestValidationError(self.releaseID, errMsg)

      return jsonString

   def Copy(self):
      manifest = Manifest()
      manifestDict = deepcopy(self.ToJSONDict())
      manifest.FromJSONDict(manifestDict)
      return manifest

   def SetHardwareSupportInfo(self, hsi):
      if not isinstance(hsi, HardwareSupportInfo):
         msg = 'Argument value is not an instance of HardwareSupportInfo'
         raise ValueError(msg)
      self._hardwareSupportInfo = hsi

   hardwareSupportInfo = property(lambda self: self._hardwareSupportInfo,
                                  SetHardwareSupportInfo)
