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

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

import optparse, sys # pylint: disable=deprecated-module
import Tac, SmbusUtil, BusUtilCommon

AhamConstants = Tac.Type( "Hardware::AhamConstants" )

def dataStrToCharList( s ):
   result = []
   length = len( s )
   if (length==0) or ((length % 2) != 0):
      parser.error( "Data string of wrong length" )
   start = 0
   if( s[1]=='x' or s[1]=='X' ): start = 2 # pylint: disable=multiple-statements
   for n in range( start, length, 2 ):
      try:
         i = int( s[n : n+2], 16 )
      except ValueError:
         parser.error( "Wrong data string" )
      result.append( bytes( ( i, ) ) )
   return result

def binary( value, bytes=4 ): # pylint: disable=redefined-builtin
   """
String rep of an integer in binary, similar to 'hex'
   Turns 0xc7 into 1100 0111
The optional bytes parameter indicates how many bytes wide the value is."""
   return "".join( ( ( value>>n &1 ) and "1" or "0" ) +
         ( ( n % 4 == 0 ) and " " or "" )
                    for n in range( bytes*8-1,-1,-1 ) )

usage = """
smbus [<options>] {read8|read16|read32} <devicepath> address [count]
smbus [<options>] {write8|write16|write32} <devicepath> address data
smbus [<options>] reads <devicepath> address size
smbus [<options>] writes <devicepath> address data
smbus [<options>] processCall <devicepath> address writeData readSize
smbus [<options>] send <devicepath> address
smbus [<options>] reset /scd/<accelId>
  <devicepath> is in this form:
     /scd/<accelId>/<busId>/<deviceId>[L[L[L]]]
     /sb/<busId>/<deviceId>
  e.g.,
     /scd/3/2/80L is the path to the supervisor seeprom
  If the deviceId is suffixed with "l" or "L" then 16-bit addresses are
  used when accessing the device.
  If the deviceId is suffixed with "ll" or "LL" then 32-bit addresses are
  used when accessing the device.
  If the deviceId is suffixed with "lll" or "LLL" then 24-bit addresses are
  used when accessing the device.

  accelId and busId are zero-based, so the legal values are 0-3 and 0-15.

  A 'bulk' read can be done by specifying a non-zero count for the
  read.

  A single read prints its result in hex, binary,and ascii

  All numeric arguments can be specified in decimal, hex (prefixed by
  '0x'), or binary (prefixed by '0b').

  The reset command is only applicable to scd based accelerators. Similarly,
  the --agent, --raw, --timeout, and --delay options only affect scd reads and
  do not have an effect on the smbuses accessed through the South Bridge (sb).

  --string
      For 'writes' argument:
         By default, input data is treated as a string of 2-character hex numbers.
         The corresponding ascii characters are what is actually written to the
         device.

         If -s is specified with 'writes' argument, input data is considered as a
         string of ASCII characters and is written as such.

      For 'reads' argument:
         By default, each byte of data read from the device is displayed as a
         2-character HEX number.

         if -s is specified, the data read from device is treated as ASCII
         numbers and the corresponding ASCII characters are printed

  --binary
      For read[n] arguments, output is written in raw binary. The intent is
      this is redirected to a file or program that can handle binary.

  --input <filename> can be used with 'writes' option. If '-' is specified as
      filename then stdin is used. This is an alternative to specifying the
      data in the args. In this case, the data is read from the specified file.

  --maxChunkSize
      This must be used with 'writes'. When specified, if the given data size is
      greater then the maxChunkSize, it is split into multiple chunks and
      written to the specified device - one chunk at a time

  --smbusAddrSpacing
      By default, smbus accelerators are assumed to have base addresses that
      are 0x100 apart.  To change the spacing between accelerator address
      spaces, specify this argument.

      An argument of 'schooner' for this option specifies the discontiguous address
      mapping used by Schooner (accelerators 8-15 have addresses starting at
      0x18000; otherwise spacing is 0x80 apart).

  --smbusBaseAddr <address> can be used to specify smbus base address.
      Default is 0x8000

  --pciAddr <address> can be used with raw mode to specify the PCI address of
      the SCD on platforms where there are multiple Scds.

  --be
      Treat data for read{16|32} and write{16|32} as bigendian
      (MSB at lowest address).
  
  --noack 
      Ignore checking acknowledgements for write operations

  --value
      Print only the decimal value of a read result

  --stress
      Run the command repeatedly to stress the bus

  --busSpeed
      Set the bus clock speed (`/scd` controllers only)

  Examples:
   1) To read IDEEPROM on the Mezzanine on Silverado:
         smbus -s reads /scd/0/6/0x50 0x0 0xff
   2) To write prefdl to IDEEPROM on the Mezzanine on Silverado:
         /usr/bin/smbus -i /tmp/prefdl -s -m 16 writes /scd/0/6/0x50 0x0
               or
         cat /tmp/prefdl | /tmp/smbus -i - -s -m 16 writes /scd/0/6/0x50 0x0
      Here, /tmp/prefdl has the encoded prefdl for Mezzanine
   3) To read the first AOM EEPROM in a ThreeBrothers in slot Linecard9:
         smbus read8 /scd/0/0/0x53l 0x0 0xff -s -l 9
"""

timeoutDefault = 10
delayMs = {
   '0': 'delay0',
   '1': 'delay1ms',
   '10': 'delay10ms',
   '50': 'delay50ms'
}
busTimeouts = {
   '0': 'busTimeout100us',
   '35': 'busTimeout35ms',
   '100': 'busTimeout100ms',
   '250': 'busTimeout250ms',
   '500': 'busTimeout500ms',
   '1000': 'busTimeout1000ms'
}
busSpeeds = {
   '100khz': 'speed100kHz',
   '400khz': 'speed400kHz',
   '1mhz': 'speed1MHz',
   '3.4mhz': 'speed3point4MHz',
   'unspecified': 'unspecifiedSpeed',
}
parser = optparse.OptionParser(usage=usage)
parser.add_option( "-v", "--verbose", action="store_true",
                   help="enable verbose output" )
parser.add_option( "-a", "--agent", action="store_true",
                   help="force access to hardware via Smbus agent",
                   dest="agent", default=False )
parser.add_option( "-r", "--raw", action="store_true",
                   help="force direct access to hardware",
                   dest="raw", default=False )
parser.add_option( "-o", "--smbusAddrSpacing", action="store",
                   default='0x100', dest="smbusAddrSpacing",
                   help="Specify accelerator address offsets." )
parser.add_option( "", "--smbusBaseAddr", action="store",
                   default='0x8000', dest="smbusBaseAddr",
                   help="Specify smbus base address." )
parser.add_option( "-s", "--string", action="store_true",
                   help="Output result as a string to stdout" )
parser.add_option( "-t", "--timeout", action="store",
                   help="max seconds to wait for the agent to respond " \
                        "(default=%d)" %( timeoutDefault ),
                   default=timeoutDefault )
parser.add_option( "-w", "--width", action="store", type="int",
                   default=16,
                   help="Bytes to show per line in bulk mode" )
parser.add_option( "", "--delay", action="store",
                   default='0', choices=list( delayMs.keys() ),
                   help="Ms to delay between transactions (default 0)" )
parser.add_option( "-b", "--busTimeout", action="store",
                   default='1000', choices=list( busTimeouts.keys() ),
                   help="Smbus transaction timeout in ms (default 1000)" )
parser.add_option( "-c", "--writeNoStopReadCurrent", action="store_true",
                   default=False,
                   help="enable write no stop read current" )
parser.add_option( "-i", "--inputFile", action="store",
                   default=False,
                   help="Specify input file for data to write." )
parser.add_option( "", "--binary", action="store_true",
                   default=False,
                   help="Read data is written in binary." )
parser.add_option( "-m", "--maxChunkSize", action="store",
                   default=False,
                   help="Specify maxChunkSize for each write." )
parser.add_option( "-l", "--linecard", action="store",
                   type="int", default=None,
                   help="Specify the linecard on which the device exists." )
parser.add_option( "", "--pciAddr", action="store",
                   default=None,
                   help="Specify the PCI address of the Scd device." )
parser.add_option( "-p", "--pec", action="store_true",
                   help="Send PEC on write or check PEC on read." )
parser.add_option( "", "--be", action="store_true",
                   help="Use big endian for read{16|32} and write{16|32} data "
                        "(MSB at lowest address)" )
parser.add_option( "--noack", action="store_false", default=True,
                  help="Don't check for acknowledgements from the responder device" )
parser.add_option( "-d", "--value", action="store_true",
                  help="Output the decimal value of the result to stdout" )
parser.add_option( "", "--stress", action="store_true",
                   help="Run the command repeatedly to stress the bus" )
parser.add_option( "--busSpeed", action="store",
                   help="Set the bus clock speed (`/scd` controllers only)",
                   choices=list( busSpeeds.keys() ), default="unspecified" )

( options, args ) = parser.parse_args()
if len( args ) < 2:
   parser.error( "Too few arguments" )

if not args[1].startswith( "/sb" ):
   agentPresent = BusUtilCommon.agentAddr( 'Smbus' )
   if( agentPresent and options.raw ):
      parser.error( "Kill Smbus agent before asking for raw" )
   if( (not agentPresent) and options.agent ):
      parser.error( "Smbus agent is not running" )

options.timeout = int( options.timeout )

op = args[0]
if op not in ( "read8", "write8", "read16", "write16", "read32", "write32",
               "reads", "writes", "processCall", "send", "reset" ):
   parser.error( "Unknown argument:" + args[0] )

count = 1
data = None
if op.startswith( "write" ):
   if len( args ) < 3:
      parser.error( "Wrong # of arguments for write" )
   op = args[ 0 ]
   path = args[ 1 ]
   addr = args[ 2 ]
   if options.inputFile:
      assert op == "writes"
      if options.inputFile == "-":
         # For stdin, assume we want no whitespace before/after, e.g. the user is
         # entering a string. Note that this makes `cat some_file | smbus` behave
         # oddly.
         data = sys.stdin.read().strip()
      else:
         # When a filename is specified, assume we want a byte-for-byte write to the
         # SMBUs device. Don't tinker with the data at all.
         with open( options.inputFile, "rb" ) as fp:
            data = fp.read()
   else:
      # -i not specified. 4th argument must be data
      if len( args ) != 4:
         parser.error( "Wrong # of arguments for write" )
      data = args[ 3 ]
   if op == "writes":
      # Data as a list of bytes
      if not isinstance( data, bytes ):
         if options.string:
            data = list( data )
         else:
            data = dataStrToCharList( data )
   else:
      # Data as a single word
      data = SmbusUtil.strToInt( data )
elif op.startswith( "read" ) or op == "send":
   (op,path,addr) = args[0:3]
   if(len(args) > 3)  or (op == "reads"):
      if len( args ) == 3:
         # Block read
         count = AhamConstants.blockReadCount
      elif len( args ) == 4:
         count = SmbusUtil.strToInt(args[3])
      else:
         parser.error( "Wrong number of arguments for read" )
elif op == "processCall":
   if len( args ) != 5:
      parser.error( "Wrong # of arguments for processCall" )
   path, addr = args[1:3]
   if options.string:
      data = list( args[3] )
   else:
      data = dataStrToCharList( args[3] )
   count = SmbusUtil.strToInt( args[4] )
else: # reset
   if not args[1].startswith( '/scd' ):
      parser.error( "Cannot use reset with non-scd buses" )
   if len(args) != 2:
      parser.error( "Too many arguments for reset" )
   (op,path) = args
   addr = 0

try:
   controller, accelId, busId, deviceId, addrSize = \
      SmbusUtil.parseSmbusPath( path, op )
except SmbusUtil.SmbusPathError as e:
   parser.error( str( e ) )

silent = options.string
verbose = options.verbose and not silent

addr = SmbusUtil.strToInt(addr)
width = options.width

def ascii_( value, size ): # pylint: disable=inconsistent-return-statements
   if size > 0:
      output = ""
      while size > 0:
         output = chr( value & 0xff ) + output
         size = size - 1
         value = ( value >> 8 )
      
      return output

def doRead( func, addr, count, size, pec=False, bigendian=False ):
   if count == 1:
      result = func( addr, pec=pec )
      if bigendian:
         val = 0
         for i in range( size ):
            val = ( ( val << 8 ) + ( ( result >> ( 8 * i ) ) & 0xff ) )
         result = val
      if options.string:
         print( ascii_( result, size ) )
      elif options.value:
         print( result )
      elif options.binary:
         print( chr( result ) )
      else:
         print( hex( result ), binary( result, size ), ascii_( result, size ) )
   else:
      if options.binary:
         result = helper.read( addr, count, accessSize=size, raw=True, pec=pec )
         sys.stdout.buffer.write( bytes( result ) )
      else:
         result = helper.read( addr, count, accessSize=size, pec=pec )
         # NOTE - result is a list of bytes
         if options.string:
            resultStr = "".join( [ascii_(i,1) for i in result] )
            print( resultStr )
         else:
            from Hexdump import hexdump # pylint: disable=import-outside-toplevel
            print( hexdump( bytearray(result ), firstAddr=addr, rowLen=width ) )

def doReadByteString( addr, size, stringOp, pec=False ):
   if size == AhamConstants.blockReadCount:
      accessSize = 1
   else:
      accessSize = size
   result = helper.readByteList( addr, size, accessSize, pec=pec )
   if stringOp:
      for n in result:
         # This does not print the space/newline after each character
         sys.stdout.write( chr( n ) )
   else:
      for n in result:
         print( "%02x" %n, end=' ' )
   print( "\n" )

# Once again, mux on scd and sb
if controller == 'scd':
   factory = SmbusUtil.Factory()
   delay = delayMs[ options.delay ]
   # Modular systems need to specify which smbus agent to talk to,
   # which is identified by linecard number. Fixed systems should
   # pass the default None.
   helper = factory.device( accelId, busId, deviceId, addrSize,
                            readDelayMs=delay, writeDelayMs=delay,
                            busTimeout=busTimeouts[ options.busTimeout ],
                            writeNoStopReadCurrent=options.writeNoStopReadCurrent,
                            smbusAddrSpacing=options.smbusAddrSpacing,
                            smbusAgentId=options.linecard,
                            pciAddress=options.pciAddr,
                            smbusBaseAddr=options.smbusBaseAddr,
                            verbose=verbose, verifyAck=options.noack,
                            requestedSpeed=busSpeeds[ options.busSpeed ] )
elif controller == 'sb':
   factory = SmbusUtil.Factory( factoryType='sb' )
   helper = factory.device( busId, deviceId, # pylint: disable=no-value-for-parameter
           verbose=verbose, verifyAck=options.noack )
else:
   assert False

if op == "reset":
   if agentPresent:
      parser.error( "reset operation is supported in raw mode only" )
   helper.reset()
   sys.exit( 0 )

while True:
   try:
      if op == "read8":
         doRead( helper.read8, addr, count, 1, pec=options.pec )
      elif op == "read16":
         doRead( helper.read16, addr, count, 2, pec=options.pec,
                 bigendian=options.be )
      elif op == "read32":
         doRead( helper.read32, addr, count, 4, pec=options.pec,
                 bigendian=options.be )
      elif op == "reads":
         doReadByteString( addr, count, options.string, pec=options.pec )
      elif op == "write8":
         helper.write8( addr, data, pec=options.pec )
         print( "wrote", data, "at", addr )
      elif op in [ "write16", "write32" ]:
         d = 0
         dSize = 4
         if op == "write16":
            dSize = 2
         if options.be:
            # The helper always writes little endian (for write16 LSB first, then
            # MSB). Pre-swap the bytes so they come out right. This option is used on
            # SFP-10G-T modules so test changes there. See AID/10131 for details.
            for i in range(dSize):
               d = ( ( d << 8 ) + ( data & 0xff ) )
               data = ( data >> 8 )
         else:
            d = data
         if op == "write16":
            helper.write16( addr, d, pec=options.pec )
         if op == "write32":
            helper.write32( addr, d, pec=options.pec )
         print( "wrote", d, "at", addr )
      elif op == "writes":
         if options.maxChunkSize:
            length = len( data )
            maxChunkSize = int( options.maxChunkSize )
            while length > 0:
               writeLength = min( length, maxChunkSize )
               helper.write( addr, data[ :writeLength ], writeLength,
                             pec=options.pec )
               print( "Wrote", writeLength,
                      "bytes: ", data[ :writeLength ], "at", addr )
               length = length - writeLength
               addr = addr + writeLength
               data = data[ writeLength: ]
         else:
            helper.write( addr, data, len( data ), pec=options.pec )
      elif op == "send":
         helper.sendByte( addr, pec=options.pec )
      elif op == "processCall":
         result = helper.processCall( addr, data, count )
         print( f"process call at {addr}: wrote {data}; read following data" )
         if options.string:
            for n in result:
               sys.stdout.write( chr( n ) )
         else:
            for n in result:
               print( "%02x" % n, end=' ' )
         print( "\n" )

   except SmbusUtil.SmbusFailure as e:
      smbusStr = f"{path} 0x{addr:x}"
      if data is not None:
         if isinstance( data, int ):
            smbusStr += f" 0x{data:x}"
         elif options.string:
            smbusStr += f" '{''.join( data )}'"
         else:
            smbusStr += f" 0x{b''.join( data ).hex()}"
      print( f"Smbus transaction failed ({smbusStr}), exception = {str( e )}" )
      if not options.stress:
         sys.exit(1)

   if not options.stress:
      break
