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

# pylint: disable=redefined-outer-name
# pylint: disable=consider-using-f-string

import Tac, MdioUtil, BusUtilCommon, EntityManager, Cell
import Pci, ScdRegisters, PhyStatusLib
import argparse, sys

progDesc = """
  MDIO accelerator utility.

  Concurrent use of an MDIO accelerator yields unpredictable results, thus,
  inhibit other agents that may be using the MDIO accelerator.

  For details on each command, pass -h or --help after that command."""

mdioTransactionDesc = """
  <devicePath> = indicates the MDIO accelerator and the MDIO device.
     /pci/<pciAddress>/<accelId>/<busId>/<portAddr>,
     /scd[/<linecard>]/<accelId>/<busId>/<portAddr> or
     /phy[/<linecard>]/<phyId>
     Note: /scd/... and /phy/... accelerator paths rely on Sysdb.

  <linecard> = card where the accel is on (e.g., Linecard3, Linecard5).
  <accelId> = zero-based decimal.  It spans accross SCDs when using "/scd"
              but it is limited to the pci device when using "/pci".
  <busId> = zero-based decimal, legal values are 0-2.
  <portAddr> = must fit in 5-bits (a.k.a. phyAddr).
  <pciAddress> = DDDD:BB:SS.F D=Domain, B=BusNumber, S=Slot, F=Function.
  <phyId> = one-based decimal (a.k.a port number)

  <devAddr>, <register> and <data> can be decimal, binary (prefixed with 0b)
  or hex (prefixed with 0x)."""

mdioReadDesc = """  Read register(s) from MDIO device.
   """ + mdioTransactionDesc

mdioWriteDesc = """  Write to register on MDIO device.
   """ + mdioTransactionDesc

mdioResetDesc = """
  This resets the MDIO accelerator only.  The MDIO device is unaware of
  this operation.

  <accelPath> = indicates the MDIO accelerator:
     /pci/<pciAddress>/<accelId>,
     /scd[/<linecard>]/<accelId> or
     /phy[/<linecard>]/<phyId>
     Note: /scd/... and /phy/... accelerator paths rely on Sysdb.

  <linecard> = card where the accel is on (e.g., Linecard3, Linecard5).
  <accelId> = zero-based decimal.  It spans accross SCDs when using "/scd"
              but it is limited to the pci device when using "/pci".
  <pciAddress> = DDDD:BB:SS.F D=Domain, B=BusNumber, S=Slot, F=Function.
  <phyId> = one-based decimal (a.k.a port number)"""

helpRegister = """
   register address [<devAddr>.]<register>.
   <devAddr> is a 5-bit value which infers Clause 45 MDIO.
   <register> is 5-bit value for Clause 22 or 16-bit value for Clause 45."""

max5bitValue  = 0x1F
max16bitValue = 0xFFFF
currParser = None

def error( msg ):
   if currParser:
      currParser.error ( msg )
   else:
      print( sys.argv[ 0 ] + ": error: " + msg + "." )
   sys.exit( 1 )

def warn( msg ):
   print( sys.argv[ 0 ] + ": warning: " + msg + "." )


def prefixedInt( s ):
   try:
      i = int( s )
   except ValueError:
      if s.startswith( "0b") or s.startswith( "0B" ):
         i = int( s[ 2: ], 2 )
      elif s.startswith( "0x") or s.startswith( "0X" ):
         i = int( s[ 2: ], 16 )
      else:
         raise argparse.ArgumentTypeError(  # pylint: disable=raise-missing-from
            "%s is not int, hex(0x) or binary(0b)" % s )
   return i

def strToInt( s, arg ):
   try:
      val = prefixedInt( s )
   except argparse.ArgumentTypeError:
      error( "argument %s: %s is not int, hex(0x) or binary(0b)" % ( arg, s ) )
   return val


def ascii( number ): # pylint: disable=redefined-builtin
   def convert( character ):
      if 0x20 <= character <= 0x7e:
         return chr( character )
      else:
         return "."
   return ( convert( ( number >> 8 ) & 0xff ) +
            convert( number & 0xff ) )


def format( addr, data, width=8 ): # pylint: disable=redefined-builtin
   length = len( data )

   # FIRST ROW
   strbuf = ""
   # leading spaces
   if length <= width:
      col = 0
      output = "%04x:" % addr
   else:
      col = addr % width
      idx = addr - col  # round down addr
      output = "%04x:" % idx
      for x in range( col ):
         output += ( " " * 5 )
         strbuf += "  "
   # data
   rowlength = min( length , width - col )
   for x in range( rowlength ):
      output += " %04x" % data[ x ]
      strbuf += ascii( data[ x ] )
   output += "  " + strbuf + "\n"
   if length <= width:
      return output

   idx = idx + width
   # SUBSEQUENT ROWS
   while idx < addr + length:
      remainder = addr + length - idx
      strbuf = ""
      output += "%04x:" % idx
      rowlength = min( width, remainder )
      for x in range( rowlength ):
         output += " %04x" % data[ x + idx - addr ]
         strbuf += ascii( data[ x + idx - addr ] )

      # pad out the rest of the line (only for the last line)
      for x in range( width - rowlength ):
         output += ( " " * 5 )

      output += "  " + strbuf + "\n"
      idx = idx + width
   return output


def parseRegisterAddress( regAddr ):
   regAddr = regAddr.split( '.' )
   if len( regAddr ) == 1:
      clause = 'clause22'
      devAddr = 0
      register = strToInt( regAddr[ 0 ], "register" )
      if register > max5bitValue:
         error( "<register> exceeds 5-bits for Clause 22" )
   elif len( regAddr ) == 2:
      clause = 'clause45'
      devAddr = strToInt( regAddr[ 0 ], "devAddr" )
      register = strToInt( regAddr[ 1 ], "register" )
      if devAddr > max5bitValue:
         error( "<devaddr> exceeds 5-bits for Clause 45" )
      if register > max16bitValue:
         error( "<register> exceeds 16-bits for Clause 45" )
   else:
      error( "register format should be <devAddr>.<register> " +
                   "for Clause 45 or <register> for Clause 22" )
   return ( clause, devAddr, register )


def parsePath( pathType, path ):
   pathParser = argparse.ArgumentParser( prog=pathType, usage="", add_help=False )
   pathSubparsers = pathParser.add_subparsers( dest="devType",
                                               help="device Type" )

   phyUsage = "/phy[/<linecard>]/<phyId>"
   scdUsage = "/scd[/<linecard>]/<accelId>"
   pciUsage = "/pci/<pciAddress>/<accelId>"
   if pathType == 'devicePath':
      scdUsage = scdUsage + "/<busId>/<portAddr>"
      pciUsage = pciUsage + "/<busId>/<portAddr>"

   pciPathParser = pathSubparsers.add_parser( "pci", usage=pciUsage )
   pciPathParser.add_argument( "pciAddress", help="PCI Address" )
   pciPathParser.add_argument( "accelId", type=prefixedInt,
                               help="MDIO accelerator ID" )
   if pathType == 'devicePath':
      pciPathParser.add_argument( "busId", type=prefixedInt, help="MDIO bus" )
      pciPathParser.add_argument( "portAddr", type=prefixedInt,
                                 help="a.k.a. phyAddr in MDIO" )

   scdPathParser = pathSubparsers.add_parser( "scd", usage=scdUsage )
   scdPathParser.add_argument( "sliceId", nargs="?", default=None,
                               help="sliceId" )
   scdPathParser.add_argument( "accelId", type=prefixedInt,
                               help="MDIO accelerator ID" )
   if pathType == 'devicePath':
      scdPathParser.add_argument( "busId", type=prefixedInt, help="MDIO bus" )
      scdPathParser.add_argument( "portAddr", type=prefixedInt,
                                   help="a.k.a. phyAddr in MDIO" )

   phyPathParser = pathSubparsers.add_parser( "phy", usage=phyUsage )
   phyPathParser.add_argument( "sliceId", nargs="?", default=None )
   phyPathParser.add_argument( "phyId", type=prefixedInt )

   if not path.startswith( '/' ):
      error( "<%s> must begin with a /" % pathType )

   if path.endswith( '/' ):
      error( "<%s> must not end with a /" % pathType )

   path = path[ 1: ]
   path = path.split( '/' )

   return pathParser.parse_args( path )


def mountState( sysname, sliceId ):
   em = EntityManager.Sysdb( sysname=sysname )
   mg = em.mountGroup()
   mg.mount( 'hardware/phy/topology/allPhys',
             'Hardware::PhyTopology::AllPhyIntfStatuses', 'r' )
   mg.mount( 'hardware/phy/config', 'Tac::Dir', 'ri' )
   mg.mount( 'hardware/phy/status', 'Tac::Dir', 'ri' )
   mg.mount( "hardware/mdio/", "Tac::Dir", "ri" )
   mg.mount( "hardware/managedMdio/", "Tac::Dir", "ri" )
   # hamImpl points to pciDeviceStatusDir
   mg.mount( 'cell/%d/hardware/pciDeviceStatusDir' % Cell.cellId(),
             'Hardware::PciDeviceStatusDir', 'r' )
   mg.close( blocking=True )
   return em


# search for accelerator in Sysdb and find its PCI address and register
def accelPciDetails( em, sliceId, accelId ):
   mdioAccel = None
   mdioDirs = []
   subdir = 'slice' if sliceId else 'cell'
   # pylint: disable-next=redefined-builtin
   for dir in [ 'hardware/mdio', 'hardware/managedMdio' ]:
      if subdir in em.root().entity[ dir ]:
         mdioDirs.append( em.root().entity[ dir ][ subdir ] )

   if sliceId:
      if not mdioDirs:
         error( "'slice' subdirectory absent under 'mdio' and 'managedMdio" )

      sliceDirs = []
      for dir in mdioDirs:
         if sliceId in dir:
            sliceDirs.append( dir[ sliceId ] )

      if not sliceDirs:
         error( "%s is not an mdio slice in Sysdb" % sliceId )

      for dir in sliceDirs:
         if accelId in dir.accel:
            mdioAccel = dir.accel[ accelId ]

   else:
      if not mdioDirs:
         error( "'cell' subdirectory absent under 'mdio' and 'managedMdio" )

      for dir in mdioDirs:
         accelCollection = dir[ str( Cell.cellId() ) ].accel
         if accelId in accelCollection:
            mdioAccel = accelCollection[ accelId ]

   if not mdioAccel:
      error( "<accelId> %d is not in Sysdb" % accelId )

   hamPciAddr = mdioAccel.ham.hamImpl.address
   pciAddress = "%04x:%02x:%02x.%x" % ( hamPciAddr.domain, hamPciAddr.bus,
                                        hamPciAddr.slot, hamPciAddr.function )
   return ( pciAddress, mdioAccel.offset )


# search for accelerator details in Sysdb for this scd
def scdAccelDetails( sysname, sliceId, accelId ):
   entityManager = mountState( sysname, sliceId )
   ( pciAddress,
     accelRegBase ) = accelPciDetails( entityManager, sliceId, accelId )
   return ( pciAddress, accelRegBase )


# search for device details in Sysdb for this phy
def phyAccelDetails( sysname, sliceId, phyNum ):
   if not sliceId:
      phyName = "Ethernet%d" % phyNum
   elif sliceId.startswith( 'Linecard' ):
      phyName = "Ethernet%s/%d" % ( sliceId[ 8: ], phyNum, )
   else:
      phyName = "Ethernet%s/%d" % ( sliceId, phyNum, )

   em = mountState( sysname, sliceId )
   phyTopology = em.root().entity[ 'hardware/phy/topology/allPhys' ]
   phyIntfData = PhyStatusLib.phyConfigStatusAtPos(
      em.root(), phyTopology, phyName, Tac.Type( 'PhyEee::PhyPosition' ).asicPhy,
      firstLaneOnly=True )
   assert len( phyIntfData ) == 1

   ad = phyIntfData[ 0 ][ 0 ].ahamDesc
   accelId = ad.arbusAccelId
   busId = ad.busId
   portAddr = ad.portAddr

   ( pciAddress, accelRegBase ) = accelPciDetails( em, sliceId, accelId )
   return ( pciAddress, accelRegBase, accelId, busId, portAddr )


def pciAccelRegister( accelRegBase0, accelRegDelta, accelId ):
   # accelId is within the scope of the PCI device
   # accelRegBase0 = first register of accelerator #0.
   # accelRegDelta = register-address delta between two consecutive accelerators.
   accelRegBase = accelRegBase0 + accelRegDelta * accelId
   return ( accelRegBase ) # pylint: disable=superfluous-parens


#
# top command parser
#
parser = argparse.ArgumentParser(
            formatter_class=argparse.RawDescriptionHelpFormatter,
            description=progDesc )
parser.add_argument( "-v", "--verbose", action="store_true",
                     help="enable verbose output" )
agentGroup = parser.add_argument_group( "Mdio agent specific options" )
agentGroup.add_argument( "-a", "--agent", action="store_true", default=False,
                     help="use Mdio agent to perform mdio operations" )
agentGroup.add_argument( "-t", "--timeout", action="store", type=int, default=10,
                     help="max seconds to wait for the agent to respond " +
                        "(default: %(default)s)" )
pciGroup = parser.add_argument_group( "devicePath /pci/... options" )
pciGroup.add_argument( "--regBase", action="store", type=prefixedInt,
                     default="0x9000", dest="accel0RegBase",
                     help="first register address of first accelerator " +
                          "(default: %(default)s)" )
pciGroup.add_argument( "--regSpace", action="store", type=prefixedInt,
                     default="0x100", dest="accelRegSpace",
                     help="offset between the first-register addresses of " +
                          "two consecutive accelerators (default: %(default)s)" )
sysdbGroup = parser.add_argument_group( "devicePath /scd/... and "
                                        "/phy/... options" )
sysdbGroup.add_argument( "--sysname", action="store", default="ar",
                     help="sysname (default: %(default)s)" )
subparsers = parser.add_subparsers( dest="cmd", help="MDIO accelerator commands" )

#
# "read" command subparser
#
parserRead = subparsers.add_parser( "read",
                help="read register(s) from MDIO device",
                formatter_class=argparse.RawDescriptionHelpFormatter,
                description=mdioReadDesc )
parserRead.add_argument( "devicePath",
                         help="see above description for details" )
parserRead.add_argument( "register", metavar="[devAddr.]register",
                         help=helpRegister )
parserRead.add_argument( "count", type=prefixedInt, nargs="?", default=1,
                         help="consecutive registers to read "
                         "(default: %(default)s)" )
parserRead.add_argument( "-w", "--width", action="store", type=int, default=8,
                         help="register data to show per line "
                              "(default: %(default)s)" )

#
# "write" command subparser
#
parserWrite = subparsers.add_parser( "write",
                 help="write to a register on MDIO device",
                 formatter_class=argparse.RawDescriptionHelpFormatter,
                 description=mdioWriteDesc )
parserWrite.add_argument( "devicePath",
                          help="see above description for details" )
parserWrite.add_argument( "register", metavar="[devAddr.]register",
                          help=helpRegister )
parserWrite.add_argument( "regData", type=prefixedInt, metavar="data",
                          help="16-bit register-data to write" )

#
# "reset" command subparser
#
parserReset = subparsers.add_parser( "reset", help="reset mdio accelerator",
                 formatter_class=argparse.RawDescriptionHelpFormatter,
                 description=mdioResetDesc )
parserReset.add_argument( "accelPath", help="see above description for details" )

args = parser.parse_args()
if args.cmd == "read":
   currParser = parserRead
   path = parsePath( 'devicePath', args.devicePath )

   ( clause, devAddr, register ) = parseRegisterAddress( args.register )

   if args.agent and args.count > 1 and clause == 'clause22':
      warn( "Mdio agent doesn't reliably read multiple registers " +
            "with clause 22" )
      args.count = 1

   maxRegAddrSpace = max16bitValue if clause == 'clause45' else max5bitValue
   if ( register + args.count ) > ( maxRegAddrSpace + 1 ):
      parserRead.error( "<register> + <count> exceeds register address space")

elif args.cmd == "write":
   currParser = parserWrite
   path = parsePath( 'devicePath', args.devicePath )

   ( clause, devAddr, register ) = parseRegisterAddress( args.register )
   if args.regData > max16bitValue:
      parserWrite.error( "<data> exceeds 16-bit value")

elif args.cmd == "reset":
   currParser = parserReset
   clause = None
   devAddr = None
   register = None
   if args.agent:
      parserReset.error( "reset operation not supported with --agent" )

   path = parsePath( 'accelPath', args.accelPath )
else:
   parser.error( "Unknown mdio command: " + args.cmd  )

if path.devType == 'scd':
   sliceId = path.sliceId
   accelId = path.accelId
   ( pciAddress,
     accelRegBase ) = scdAccelDetails( args.sysname, sliceId, accelId )
   if args.cmd != 'reset':
      busId = path.busId
      portAddr = path.portAddr
elif path.devType == 'phy':
   sliceId = path.sliceId
   ( pciAddress,
     accelRegBase,
     accelId,
     busId,
     portAddr ) = phyAccelDetails( args.sysname, sliceId, path.phyId )
elif path.devType == 'pci':
   if args.agent:
      # In this path, <accelId> is per SCD but the mdio agent expects
      # <accelId> at the module level.  Also, the agent needs an
      # sliceId in modular systems.
      error( "/pci/... not supported with --agent" )
   sliceId = None
   pciAddress = path.pciAddress
   accelRegBase = pciAccelRegister( args.accel0RegBase, args.accelRegSpace,
                                    path.accelId )
   accelId = path.accelId
   if args.cmd != 'reset':
      busId = path.busId
      portAddr = path.portAddr
else:
   parser.error( "unknown device type: %s" % path.devType )

if args.verbose:
   if args.cmd == 'reset':
      print( "pci %s, regBase 0x%x, accel %d :: %s " % ( pciAddress,
            accelRegBase, accelId, args.cmd ) )
   else:
      print( ( "pci %s, regBase 0x%x, accel %d, bus 0x%x, port 0x%x :: " +
              "%s 0x%x.0x%x" ) % ( pciAddress, accelRegBase, accelId,
              busId, portAddr, args.cmd, devAddr, register ) )

Tac.sysnameIs( args.sysname )  # Used in BusUtilCommon.py
# pylint: disable-next=superfluous-parens
if ( Pci.Device( pciAddress ).id() != ScdRegisters.PciId ):
   error( "device %s is not a scd" % pciAddress )

if args.agent:
   agentName = 'Mdio' + ( '-' + sliceId if sliceId else '' )
   if not BusUtilCommon.agentAddr( agentName ):
      error( "Mdio agent (%s) is not running" % agentName )

   if args.verbose:
      print( "Making %s request to Mdio agent" % ( args.cmd ) )

   helper = MdioUtil.mdioAgentClient( accelId, busId, portAddr, clause,
                                      sliceId=sliceId )

   timeout = args.timeout
   if clause == 'clause22':
      # MdioUtil expects the register address in devAddr for 'clause22'.
      devAddress = register
      regAddress = devAddr
   else:
      devAddress = devAddr
      regAddress = register

   if args.cmd == "read":
      result = helper.read16( devAddress, regAddress, args.count,
                              timeout=timeout )
      print( format( register, result, args.width ) )
   else: # args.cmd == write
      regData = args.regData
      helper.write16( devAddress, regAddress, [ regData ], timeout=timeout )
      print( "wrote 0x%x at 0x%x.0x%x" % ( regData,  devAddr,  register ) )

else:
   if BusUtilCommon.agentAddr( 'Mdio' ):
      error( "Mdio agent is running" )

   if args.cmd == "reset":
      busId = 0
      portAddr = 0

   try:
      hal = MdioUtil.halMdioAccel( pciAddress, accelId, accelRegBase )
      helper = MdioUtil.MdioHelper( hal, busId, portAddr, clause )

      if clause == 'clause22':
         # MdioUtil expects the register address in devAddr for 'clause22'.
         devAddress = register
         regAddress = devAddr
      else:
         devAddress = devAddr
         regAddress = register

      if args.cmd == "read":
         result = helper.read( devAddress, regAddress, args.count )
         print( format( register, result, args.width ) )
      elif args.cmd == "write":
         regData = args.regData
         helper.write( devAddress, regAddress, regData )
         print( "wrote 0x%x at 0x%x.0x%x" % ( regData,  devAddr,  register ) )
      else: # args.cmd == "reset"
         helper.reset()

   except MdioUtil.MdioError as e:
      error( "Exception occurred while performing mdio operation: %s" % e )
