#!/usr/bin/python
########################################################################
# Copyright (C) 2010-2021 VMware, Inc.                                 #
# All Rights Reserved                                                  #
########################################################################

import logging

from .Version import VibVersion
from .Vib import VibProvide, VibRelation
from .Utils.Misc import isString

log = logging.getLogger(__name__)

#
# This is the version of the esximage library.
# It is injected into the dependency graph as a provides with the
# special name "installer:esximage".
# This name may be used in a <depends> if a VIB needs to use certain
# features of a particular version of the esximage library.
#
ESXIMAGE_LIB_VERSION = "1.0"
ESXIMAGE_PROVIDE = "installer:esximage"


class ScanResult(object):
   """Holds information about component relationships, where a component
      may be a VIB or something else.
         Attributes:
            * id               - Component ID, unique amongst all components
            * comptype         - Component type
            * depends          - A dictionary. Each key is a VibRelation ID.
                                 Each value is a set of VIB IDs that provide
                                 the dependency.
            * dependedOnBy     - A set of VIB IDs which depend on this VIB.
            * replaces         - A set of VIB IDs which this VIB replaces.
            * replacedBy       - A set of VIB IDs which replace this VIB.
            * conflicts        - A set of VIB IDs with which this VIB conflicts.
                                 (Note that conflicts are reflexive;
                                 if a->conflicts->b; then b->conflicts->a.)
   """
   TYPE_VIB    = "vib"
   TYPE_ESXIMGLIB = "esximagelib"     # The esximage library version
   TYPE_IMPLICITREQ = "implicitreq"   # Implicit depend from installer:esximage
   TYPE_COMPONENT = "component"
   ALL_TYPES = (TYPE_VIB, TYPE_ESXIMGLIB, TYPE_IMPLICITREQ, TYPE_COMPONENT)

   def __init__(self, compid, comptype=TYPE_VIB):
      self.id = compid
      self.comptype = comptype
      self.depends = dict()
      self.dependedOnBy = set()
      self.replaces = set()
      self.replacedBy = set()
      self.conflicts = set()

class VibScanner(object):
   """Provides a method for establishing relationships between VIBs, and holds
      a mapping of VIB IDs to ScanResult objects.
         Attributes:
            * results - A dictionary containing ScanResults objects.  Each key
                        is a component ID, and each value is a ScanResult object.
            * vibs - The subset of results that are actually VIBs.
            * extraprovides - A list of tuples for injection of extra provides
                        into the scanning system.  Useful for external deps,
                        the esximage library version, and other special situations.
                        Each tuple consists of (provideobj, compids, comptype),
                        where provideobj is an instance of Vib.VibProvide;
                        compids is a list or set of unique component IDs for the
                        results dict that provides the thing in provideobj, and
                        comptype is one of ScanResult.TYPES_*.
   """
   def __init__(self):
      self.results = dict()
      self.vibs = dict()
      self.extraprovides = list()

      # Inject the esximage library version in as a provide.  Note that
      # we also need to inject a special ID just so that any depends
      # on the esximage library can get a nonzero result.
      esximgid = ESXIMAGE_PROVIDE
      esximgprov = VibProvide(esximgid,
                              VibVersion.fromstring(ESXIMAGE_LIB_VERSION))
      self.AddExtraProvide(esximgprov, (esximgid,), ScanResult.TYPE_ESXIMGLIB)

   def AddExtraProvide(self, provideobj, compids, comptype):
      """Inject an extra provide into the components being scanned.  Useful for
         external dependencies, the esximage library version, etc.
         Parameters:
            * provideobj  - An instance of Vib.VibProvide
            * compids     - A list or set of each unique component ID being provided.
                            Unless this list is nonempty, matches for the extra
                            provide will not be able to return any providing comp IDs.
            * comptype    - One of the ScanResult.TYPE_* types.
      """
      if comptype not in ScanResult.ALL_TYPES:
         raise ValueError("'%s' is not a valid component type" % (comptype))
      self.extraprovides.append((provideobj, compids, comptype))

   def GetResultsByType(self, comptype):
      """Returns a dict containing a subset of the scan results by type.
         Parameters:
            * comptype - The component type to get results for
         Returns:
            A dictionary. Each key is a component ID, and each value is a
            ScanResult object.
      """
      return dict((r.id, r) for r in self.results.values() if r.comptype == comptype)

   def GetNewestSet(self):
      """Returns the newest VIBs amongst the scanned VIBs.  The newest VIBs are
         those whose scan results have an empty replacedBy.
         Returns:
            A set of VIB IDs corresponding to the newest VIBs.
      """
      return set(vid for vid in self.vibs if len(self.vibs[vid].replacedBy) == 0)

   def GetUpdatesSet(self, vibs):
      """Returns all the VIBs that update some set of VIBs.  No results will be
         returned for VIBs that are not part of the scan results.
         Parameters:
            * vibs - An iterable of either VIB IDs or Vib instances.
                     A VibCollection should work.  These are the VIBs to find
                     updates for.
         Returns:
            A set of VIB IDs for the VIBs that update the parameter vibs.
      """
      vibids = set()
      for vib in vibs:
         if isString(vib):
            vibids.add(vib)
         else:
            vibids.add(vib.id)

      remaining = set(self.vibs.keys()) - vibids
      return set(vid for vid in remaining \
                 if len(self.vibs[vid].replaces & vibids))

   def GetDowngradesSet(self, vibs):
      """Returns all the VIBs that are older than some set of VIBs.  No results
         will be returned for VIBs that are not part of the scan results.
         Parameters:
            * vibs - An iterable of either VIB IDs or Vib instances.
                     A VibCollection should work.  These are the VIBs to find
                     downgrades or older VIBs for.
         Returns:
            A set of VIB IDs for the VIBs that downgrade the parameter vibs.
      """
      vibids = set()
      for vib in vibs:
         if isString(vib):
            vibids.add(vib)
         else:
            vibids.add(vib.id)

      remaining = set(self.vibs.keys()) - vibids
      return set(vid for vid in remaining \
                 if len(self.vibs[vid].replacedBy & vibids))

   def Scan(self, vibs):
      """Populates relationships between VIBs.
            Parameters:
               * vibs - A VibCollection instance.
            Note: None of the child objects in the vibs parameter are modified.
      """
      # XXX: Workarounds of VIB blanket replace relations, a dict of:
      # VIB_name -> replaced_name -> (new_relation_direction,
      #    new_replaced_version_bound)
      # if new_replaced_version_bound is None VIB version is used.
      BLANKET_REPLACE_WORKAROUNDS = {
         # PR 2817860 / 2863931:
         # 1) i40enu's blanket replace of i40en VIB causes newer i40en VIB to
         # be skipped during an upgrade; to workaround this: within the scope
         # of VIB scanning, the replace relation will be modified to only
         # replace i40en with lower version than i40enu.
         'i40enu': {
            'i40en': ('<<', None),
         },
         # 2) intel-nvme-vmd's blanket replace of iavmd VIB causes newer iavmd
         # VIB to be skipped during an upgrade. Within VIB scanning, limit the
         # replace relation to only iavmd VIBs with versions lower than 2.7.0.
         # 2.0.0/2.6.x iavmd VIBs will not take over intel-nvme-vmd.
         'intel-nvme-vmd': {
            'iavmd': ('<<', '2.7.0'),
         },
      }

      # vibbyid is a complex dict. Keys are vib IDs, and values are lists.
      # Each list itself contains three dictionaries. Dictionary 0 maps depends
      # IDs to a set of matching VibProvide IDs; dictionary 1 maps replaces IDs
      # to a set of matching VibProvide IDs; dictionary 2 maps conflicts IDs to
      # a set of matching VibProvide IDs.
      # vibbyid[vibid] = [{dependsid: set((provideids,)),
      #                    replacesid: set((provideids,)),
      #                    conflictsid: set(provideids,))}]
      vibbyid = dict()
      # provbyid maps VibProvide IDs to VibProvide objects and the VIBs that
      # provide them. provbyid[provideid] = [provideobj, set((providingvibs,))]
      provbyid = dict()
      # relbyname is a complex dict. Each key is a VibRelation name. Each value
      # is then itself a dictionary, where the keys are VibRelation IDs, and
      # values are a list. The first item in each list is a VibRelation object,
      # and the second value is a set of matching provide IDs.
      # relbyname[relationname] = {relationid: [relationobj,
      #                                         set((provideids,))]}
      relbyname = dict()

      def cacherel(rel):
         relname = rel.name
         relid = rel.id
         if relname in relbyname:
            namecache = relbyname[relname]
            if relid in namecache:
               provids = namecache[relid][1]
            else:
               provids = set()
               namecache[relid] = [rel, provids]
         else:
            provids = set()
            relbyname[relname] = {relid: [rel, provids]}
         return relid, provids

      # Add in extra provides to the scanned components
      for provideobj, compids, comptype in self.extraprovides:
         provbyid[provideobj.id] = [provideobj, set(compids)]
         for compid in compids:
            self.results[compid] = ScanResult(compid, comptype)

      for vibid, vib in vibs.items():
         # We don't do anything with ScanResult objects in this loop. We
         # just create the objects here so that we don't have to worry about
         # whether they exist or not later in this function.
         self.results[vibid] = ScanResult(vibid)
         depends = dict()
         replaces = dict()
         conflicts = dict()
         vibbyid[vibid] = [depends, replaces, conflicts]
         # important: the values that we push into the depends, replaces and
         #            conflicts dictionaries are the same set() instances that
         #            are populated into the relbyname cache.

         for dep in vib.depends:
            depid, provids = cacherel(dep)
            depends[depid] = provids

            # Add an entry to delegate the requirement, which will be linked to
            # the VIBs actually providing the ESXIMAGE_PROVIDE requirement.
            # This will help VUM to know which VIB to install first.
            if dep.name == ESXIMAGE_PROVIDE:
               newname = dep.name.split(':')[1]
               newdep = VibRelation(newname, dep.relation, dep.version, True)
               self.results[dep.id] = ScanResult(dep.id,
                     ScanResult.TYPE_IMPLICITREQ)
               newdepid, newprovids = cacherel(newdep)
               vibbyid[dep.id] = [{newdepid : newprovids}, dict(), dict()]

         for rep in vib.replaces:
            if (vib.name in BLANKET_REPLACE_WORKAROUNDS and
                rep.name in BLANKET_REPLACE_WORKAROUNDS[vib.name] and
                rep.relation == None):
               newRel, newVer = BLANKET_REPLACE_WORKAROUNDS[vib.name][rep.name]
               if newVer:
                  rep = VibRelation(rep.name, newRel,
                                    VibVersion.fromstring(newVer))
                  log.debug('Limiting the blanket %s replace relation in VIB '
                            '%s to version lower than %s', rep.name, vib.id,
                            newVer)
               else:
                  rep = VibRelation(rep.name, newRel, vib.version)
                  log.debug('Limiting the blanket %s replace relation in VIB '
                            '%s to lower version than the VIB', rep.name,
                            vib.id)

            repid, provids = cacherel(rep)
            replaces[repid] = provids
         for con in vib.conflicts:
            conid, provids = cacherel(con)
            conflicts[conid] = provids
         for prov in vib.provides:
            provid = prov.id
            if provid in provbyid:
               provbyid[provid][1].add(vibid)
            else:
               provbyid[provid] = [prov, set((vibid,))]

      for provid, (prov, vibset) in provbyid.items():
         name = prov.name
         if name in relbyname:
            # The provids set here will be the same instances recorded in the
            # vibbyid dict, so that adding an item to provids here also adds it
            # to the inner dictionaries in vibbyid.
            for relid, (rel, provids) in relbyname[name].items():
               if rel.matchesprovide(prov):
                  provids.add(provid)

      for vibid, (depends, replaces, conflicts) in vibbyid.items():
         vibsr = self.results[vibid]
         for depid, provids in depends.items():
            providingvibs = set()
            for provid in provids:
               for providingvibid in provbyid[provid][1]:
                  providingvibs.add(providingvibid)
                  self.results[providingvibid].dependedOnBy.add(vibid)
            vibsr.depends[depid] = providingvibs
         for repid, provids in replaces.items():
            replacedvibs = vibsr.replaces
            for provid in provids:
               for providingvibid in provbyid[provid][1]:
                  replacedvibs.add(providingvibid)
                  self.results[providingvibid].replacedBy.add(vibid)
         for conid, provids in conflicts.items():
            conflictingvibs = vibsr.conflicts
            for provid in provids:
               for providingvibid in provbyid[provid][1]:
                  conflictingvibs.add(providingvibid)
                  self.results[providingvibid].conflicts.add(vibid)

      # Don't forget to update the vibs-(only) attribute
      self.vibs = self.GetResultsByType(ScanResult.TYPE_VIB)
