#!/usr/bin/env python3
# Copyright (c) 2009 Arista Networks, Inc.  All rights reserved.
# Arastra, Inc. Confidential and Proprietary.

# pylint: disable=consider-using-f-string

import Tac, EntityManager, Tracing
import PhyDiagLib
import sys, optparse # pylint: disable=deprecated-module


simulation = False

myTraceFacility = "PhyDiag"

t0 = Tracing.Handle( myTraceFacility ).trace0
t7 = Tracing.Handle( myTraceFacility ).trace7  # status
t9 = Tracing.Handle( myTraceFacility ).trace9  # very verbose

def parseOptions():
   sampleUsage = """%prog [options]

   See the %prog man page for sample usage.
"""

   parser = optparse.OptionParser( usage=sampleUsage )

   parser.add_option( "--debug", dest="debug", action="store_true",
                      default=False, help="Enable debugging" )

   parser.add_option( "--sysname", dest="sysname", action="store",
                      type="string", default="ar",
                      help="Sysname (default=%default)" )

   # General options
   parser.add_option( "--diagsMode", dest="diagsMode", type="choice",
                      choices=[ "normalOperation", "noInit",
                                "basicInit", "xcvrInit" ],
                      help="Choices: 'normalOperation' (turn off diags "
                           "mode), 'noInit' (only take chip out of reset), "
                           "'basicInit' (do basic init), 'xcvrInit' "
                           "(detect and intialize xcvr)" )

   parser.add_option( "--phy", dest="phy", action="append", type="string",
                      help="Front-panel Phy number to manipulate" )

   parser.add_option( "--phys", dest="phys", action="store", type="string",
                      help="Front-panel Phys to use. E.g. --phys=1,2,3,4 " )

   parser.add_option( "--cleanup", dest="cleanup", action="store_true",
                      default=False,
                      help="Take the PHY out of diags mode at the end of the test." )

   # Specific tests
   parser.add_option( "--checkpresent", dest="checkPresent", action="store_true",
                      default=False,
                      help="Verify the presence of the PHY" )

   parser.add_option( "--checkreset", dest="checkReset", action="store_true",
                      default=False,
                      help="Verify that the PHY is not in reset" )
   
   parser.add_option( "-l", "--loopback", dest="loopback", type="choice",
                      choices=[ "shallowSystem", "ss", "deepSystem", "ds",
                                "shallowLine", "sl", "deepLine", "dl", "none" ],
                      help="Loopback port either towards the link partner "
                           "('line' loopback) or towards the system ('system' "
                           "loopback). Choices: shallowSystem (ss), "
                           "deepSystem (ds), shallowLine (sl), deepLine (dl), "
                           "none" )

   options, args = parser.parse_args()
   if args:
      parser.error( "unexpected arguments: " + ' '.join( args ) )
   return options, args


def TestFailure( errorMsg ):
   print( "FAIL:", errorMsg, file=sys.stderr )
   sys.exit( 1 )

# ----------------------------------------------------------------------------
#
# Functions to put PHYs into various diags modes
#
# ----------------------------------------------------------------------------
def diagsModeStatusReflectsConfig( phys ):
   for p in phys:
      if not p.diagsModeStatusReflectsConfig():
         return False
   return True


def diagsModeIs( phys, diagsMode ):
   """
   Sets the diags mode for the given phy to the given value, and
   waits until the phys enter that diags mode.
   """

   actualDiagsMode = None
   for p in phys:
      # If there is no xcvr present, but the user specified "xcvrInit"
      # or "normalOperation", the following Tac.waitFor() will hang
      # forever. Instead of failing the operation, we try to go as far
      # as possible given the constraints.
      #
      # One more thing: the user may not have specified a diagsMode
      # (i.e., it is None). In that case, go as far as possible given
      # the constraints, i.e., "xcvrInit" if there is an xcvr present,
      # or "basicInit" if not.
      if p.status_.xcvrStatus.presence == "xcvrPresent":
         if diagsMode is None:
            actualDiagsMode = "xcvrInit"
         else:
            actualDiagsMode = diagsMode
      else:
         if { "noInit" : "ok", "basicInit" : "ok" }.get( diagsMode ):
            actualDiagsMode = diagsMode
         else:
            # XXX_LWR: if this results in something unexpected, we
            #          could fail here with a helpful error message.
            actualDiagsMode = "basicInit"

      t0( "Putting %s into diags mode %s" % ( p.name, actualDiagsMode ) )
      p.diagsModeIs( actualDiagsMode )

   Tac.waitFor( lambda: diagsModeStatusReflectsConfig( phys ),
                description = "Phys to go into appropriate diags mode" )


# ----------------------------------------------------------------------------
#
# "checkPresent"
#
# ----------------------------------------------------------------------------
def checkPresent( phys ):
   """
   Checks for the presence of the PHY (does a device respond?).
   Details depend on whether this is a Clause 22 or Clause 45 PHY.
   """

   t0( "Running checkPresent for %s" % [ p.name for p in phys ] )
   error = ''
   for p in phys:
      if not p.present():
         error += 'Device present failure for Phy [%s]\n' % p.name
   if error:
      TestFailure( error )
   else:
      print( 'PASS: check present for', [ p.name for p in phys ] )


# ----------------------------------------------------------------------------
#
# "checkReset"
#
# ----------------------------------------------------------------------------
def checkReset( phys ):
   """
   Verify that the PHY is not in reset by reading the reset bit.
   Details depend on whether this is a Clause 22 or Clause 45 PHY.
   """

   t0( "Running checkReset for %s" % [ p.name for p in phys ] )
   error = ""
   for p in phys:
      if p.reset():
         error += "%s is in reset\n" % p.name
   if error:
      TestFailure( error )
   else:
      print( "PASS: check reset for", [ p.name for p in phys ] )



# ----------------------------------------------------------------------------
#
# "doLoopback"
#
# ----------------------------------------------------------------------------
def doLoopback( phys, loopbackMode ):
   lbd = { 'ss' : 'shallowSystem',
           'ds' : 'deepSystem',
           'sl' : 'shallowLine',
           'dl' : 'deepLine' }

   lbm = lbd.get( loopbackMode, loopbackMode )

   def _allPortsInLoopback( phyList ):
      for p in phyList:
         if not p.loopbackApplied():
            return False
      return True

   for p in phys:
      p.loopbackIs( lbm )

   msg = "all ports to go into loopback mode %s" % lbm
   Tac.waitFor( lambda: _allPortsInLoopback( phys ),
                timeout=60, description=msg )


# ----------------------------------------------------------------------------
#
# main
#
# ----------------------------------------------------------------------------
def exitIfMdioAgentNotRunning():
   # Some, but not all, operations require that the MDIO agent is
   # running.
   import AgentDirectory # pylint: disable=import-outside-toplevel
   try:
      _ = AgentDirectory.address( Tac.sysname(), 'Mdio', 'localhost' )
   except KeyError:
      print( "Error: Unable to contact MDIO agent" )
      sys.exit( 1 )

def main():
   options, _ = parseOptions()

   sysname = options.sysname
   Tac.sysnameIs( sysname )

   if options.debug:
      levels = list( Tracing.levelToEnumName_.values() )
      facilityMan = Tracing.traceFacilityManager
      for l in levels:
         facilityMan.traceFacility[ myTraceFacility ].levelEnabled[ l ] = True

   em = EntityManager.Sysdb( sysname )
   (config, status) = PhyDiagLib.mountEntities( em )

   # Unless otherwise specified, assume we're operating on all PHYs
   phyNames = sorted( config.config.keys() )
   if options.phy:
      t0( "Phy list:", options.phy )
      phyNames = options.phy
   elif options.phys:
      t0( "Phy list:", options.phys )
      phyNames = [ 'Phy' + p for p in options.phys.split( ',' ) ]

   phys = []
   for p in phyNames:
      phys.append( PhyDiagLib.newPhy( config.config[ p ],
                                      status.status[ p ] ) )

   cleanup = options.cleanup

   try:
      #-----------------------------------------------------------------------
      #
      # First things first: put the PHYs into diags mode, if necessary
      #
      #-----------------------------------------------------------------------
      if options.diagsMode:
         t0( "Putting PHYs into diags mode '%s': %s" % \
             ( options.diagsMode, [ p.name for p in phys ] ) )
         diagsModeIs( phys, options.diagsMode )
      else:
         print( "WARNING: *not* putting any chips into diags mode, but "
                "running requested tests anyway. Are you sure this is "
                "what you want?" )
         t0( "Skipping diags mode on PHYs %s:" % \
             ( [ p.name for p in phys ] ) )


      #-----------------------------------------------------------------------
      #
      # Figure out which test was specified, and run it.
      #
      #-----------------------------------------------------------------------

      # Trivial ping test to the PHYs across MDIO
      if options.checkPresent:
         exitIfMdioAgentNotRunning()
         checkPresent( phys )
         return

      # Trivial ping test to the PHYs across MDIO
      if options.checkReset:
         exitIfMdioAgentNotRunning()
         checkReset( phys )
         return

      # Put the port into specified loopback mode, if any.
      if options.loopback:
         doLoopback( phys, options.loopback )
         return

   finally:
      if cleanup:
         # TODO: take PHYs back out of loopback

         # Take PHYs out of diags mode.
         t0( "Cleaning up..." )
         diagsModeIs( phys, 'normalOperation' )


if __name__ == '__main__':
   t0( "phydiag starting..." )
   try:
      main()
   except KeyboardInterrupt:
      pass
   t0( "phydiag done..." )
   sys.exit( 0 )
