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

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


import copy
import optparse # pylint: disable=deprecated-module
import os
import re
import sys
import time

import ArPyUtils
import Tac
import Tracing
from PyClient import PyClient

__defaultTraceHandle__ = Tracing.Handle( "LedDiags" )

PlutoStatus = Tac.Type( "Led::PlutoStatus" )

def ManufacturingTest( args, options ):
   """ manufacturing led diag

       There are either 2 or 3 tests (chosen at random), with each test
       comprising a yellow test and a green test.  In each test, between
       0 and 5 LEDs are turned off, and the technician is asked to verify
       the number of LEDs that are off.

       If any of the answers are incorrect, then a final test is made with
       all the LEDs (of that color) on.  If either of these final tests is
       run and fails then the entire test fails.

       Prior to the first phase, the list of LEDs is randomly permuted.
       During the tests, a different set of LEDs is turned off.  In the
       first test, the first 0-5 LEDs are off.  In the second test,
       the next 0-5 LEDs after the first set are off.  This ensures
       that the same LED is not turned off more than once, and since
       multiple tests execute, every LED is on for at least one test.
   """

   def ledMfgTest( ledsOn, ledsOff, options, checkOff = True ):
      """ Turn on a list of LEDs, let the user count how many are off """
      [ illuminare(led, options) for led in ledsOn ]
      options.red = False
      options.green = False
      options.yellow = False
      options.blue = False
      [ illuminare(led, options) for led in ledsOff ]
      try:
         inputValue = int( input( "How many LEDs are %s? " %
                                         ( checkOff and 'off' or 'on' ) ) )
         if( inputValue != len( ledsOff ) and checkOff ) or \
                ( inputValue != len( ledsOn ) and not checkOff ):
            print( "Expected %d" % ( checkOff and len( ledsOff ) or len( ledsOn ) ) )
      except ValueError:
         print( "Unexpected input" )
         inputValue = -1 # definitely wrong!
      if checkOff:
         return inputValue != len(ledsOff)
      return inputValue != len(ledsOn)

   import random # pylint: disable=import-outside-toplevel
   random.seed()
   beaconList = []

   for led in ledList:
      if led.lower().startswith('beacon'):
         ledList.remove(led)
         beaconList.append(led)

   # Test beacon
   options.green = True
   options.blue = True
   # Beacons should come on
   ledMfgTest( beaconList, ledList, options, checkOff = False)
   # Beacons should be off
   ledMfgTest( [], ledList + beaconList, options, checkOff = False )

   # Test portgroup
   portgroupList = []
   for led in ledList:
      if led.lower().startswith( 'portgroup' ):
         portgroupList.append(led)

   # Newer supervisors don't support red color on active LED
   activeList = []
   procCmdline = Tac.run( [ 'cat', '/proc/cmdline' ], stdout=Tac.CAPTURE )
   platform = re.search( r'platform=(\w+)', procCmdline ).group( 1 )

   if platform not in [ 'eaglepeak',
                        'oldfaithful',
                        'greatfountain',
                        'oak' ]:
      if options.debug:
         print( "%s's Active LED does not support red color" % platform )
      for led in ledList:
         if led.lower().startswith( 'active' ):
            activeList.append( led )

   ledListG = copy.copy(ledList)
   # Portgroup LEDs do no support yellow/red. Exclude them in the Yellow list
   ledListY = list( set( ledList ) - set( portgroupList ) - set( activeList ) )
   random.shuffle( ledListY )
   random.shuffle( ledListG )

   # Now run the tests
   indexY = 0
   indexG = 0
   errorY = False
   errorG = False
   options.red = False
   options.yellow = False
   options.blue = False
   options.green = False
   def _numOff():
      return random.randint( 0, min( len(ledList)-1, 5 ) )
   
   for x in range(random.randint(0,1) + 2): # pylint: disable=unused-variable
      # red/yellow test. Note that we use red instead of yellow as all leds
      # support red, not all support yellow
      #
      # NOTE - michaelchin: this comment is no longer true -- front-panel port
      # LEDs haven't supported red for a while.
      numOff = _numOff()
      options.red = True
      options.green = False
      if options.debug:
         print( "DEBUG numOff:", numOff, "errorY:", errorY )
      # Portgroup LEDs will stay off in this test because they do not support
      # yellow/red. Add them to the led 'off' list
      errorY = ( ledMfgTest( ledListY[0:indexY] + ledListY[indexY+numOff:],
                             ledListY[indexY:indexY+numOff] + portgroupList 
                             + activeList, 
                             options )
                 or errorY )
      indexY = indexY + numOff

      # green test
      numOff = _numOff()
      options.red = False
      options.green = True
      if options.debug:
         print( "DEBUG numOff:", numOff, "errorG:", errorG )
      errorG = ( ledMfgTest( ledListG[0:indexG] + ledListG[indexG+numOff:],
                             ledListG[indexG:indexG+numOff], options ) 
                 or errorG )
      indexG = indexG + numOff

   # one last test if there were any errors
   if options.debug:
      print( "DEBUG errorY:", errorY, "errorG:", errorG )
   returnCode = 0
   if errorY:
      options.red = True
      options.green = False
      if ledMfgTest( ledListY, [], options ):
         returnCode = 1
   if errorG:
      options.red = False
      options.green = True
      if ledMfgTest( ledListG, [], options ):
         returnCode = 1

   # if either final test failed, then return failure
   if returnCode == 0:
      print( "LED manufacturing: PASS" )
   else:
      print( "LED manufacturing: FAIL" )
   return returnCode
   
def BasicTest( args, options ):
   """ the simplest led test... """

   def led( options, type ): # pylint: disable=redefined-builtin
      """ change all the leds for a device type """
      for led in ledList:
         if led.startswith(type):
            illuminare(led, options)

   # solid red/yellow
   options.red = True
   [ led(options, t) for t in ledTypes ]
   time.sleep(3)

   # flash red/yellow
   options.flash = 1
   [ led(options, type) for dev in ledTypes ]
   time.sleep(3)

   # solid green
   options.red = False
   options.green = True
   options.flash = 0
   [ led(options, type) for dev in ledTypes ]
   time.sleep(3)

   # flash green
   options.flash = 1
   [ led(options, type) for dev in ledTypes ]
   time.sleep(3)

   # off
   options.green = False
   [ led(options, type) for dev in ledTypes ]

def WalkTest( args, options ):
   """ silly silly silly """

   off = copy.copy(options)
   forward = copy.copy(options)
   backward = copy.copy(options)
   off.green = False
   off.red = False
   off.yellow = False
   # pylint: disable-next=singleton-comparison
   if options.red == False and options.green == False and options.yellow == False:
      forward.green = True
      backward.red = True
   numLeds = 7
   midLed = 3
   brightFactor=0.6
   while True:
      backwardList=[]
      prev=[]
      for i in range(numLeds+1):
         prev.append(None)
      for led in ledList:
         if not led.startswith("Ethernet"):
            continue
         if prev[0]:
            illuminare(prev[0], off, False)
         for i in range(1,numLeds):
            if(prev[i]): # pylint: disable=superfluous-parens
               forward.max = int( ( brightFactor ** abs( midLed - i ) ) * 100 )
               illuminare(prev[i], forward, False)
               prev[i-1] = prev[i]
         prev[numLeds-1] = led
         forward.max = int( ( brightFactor ** abs( midLed - numLeds ) ) * 100 )
         illuminare(led, forward, True)
         time.sleep(0.001)
         backwardList.append(led)
      backwardList.reverse()
      del backwardList[0]
      for led in backwardList:
         if prev[0]:
            illuminare(prev[0], off, False)
         for i in range(1,numLeds):
            if(prev[i]): # pylint: disable=superfluous-parens
               backward.max = int( ( brightFactor ** abs( midLed - i ) ) * 100 )
               illuminare(prev[i], backward, False)
               prev[i-1] = prev[i]
         prev[numLeds-1] = led
         backward.max = int( ( brightFactor ** abs( midLed - numLeds ) ) * 100 )
         illuminare(led, backward, True)
         time.sleep(0.001)

#-----------------------------------------------------

# Initialize mount
def initMount( sysname ):
   sysdbRoot = PyClient( sysname, "Sysdb" ).agentRoot()
   ledConfig = sysdbRoot.entity[ "led/config" ]
   ledHwConfig = sysdbRoot.entity[ "hardware/led/configInit" ]
   return ( ledConfig, ledHwConfig )

def changeLedMode( ledConfig, mode ):
   diagSetName = "diags"
   if mode == "normal":
      if ledConfig.get( diagSetName ):
         ledConfig.deleteEntity( diagSetName )
         Tac.flushEntityLog()
   elif mode == "diagnostic":
      if not ledConfig.get( diagSetName ):
         ledConfig.newEntity( "Led::Set", diagSetName )
         diagSet = ledConfig[ diagSetName ]
         diagSet.priority = 0
   return ledConfig.get( diagSetName )

def buildLedList( fru ):
   # get the list of leds from the led policy manager
   if fru != "":
      ledList = [ led for led in ledHwConfig.leds
                  if ledHwConfig.leds[ led ].fru == fru ]
   else:
      ledList = list( ledHwConfig.leds )

   if not ledList:
      print( "No leds found" )
      sys.exit( 1 )

   # TEMPORARY: On systems with Tri-Color LEDs (Brownsville2, Cedarville2,
   # Quicksilver), only use Ethernet/1, as Ethernet/[>2] do not have dedicated
   # LEDs. This test should be updated once the new LED policy has been finalized.
   # Temporary solution described in BUG883028.
   # Test extension upon completion of LED policy described in BUG883028.

   procCmdline = Tac.run( [ 'cat', '/proc/cmdline' ], stdout=Tac.CAPTURE )
   sid = re.search( r'sid=(\w+)', procCmdline ).group( 1 )
   if sid.startswith( ( "Brownsville2", "Cedarville2", "Quicksilver" ) ):
      ledList = [led for led in ledList if not re.match(r"Ethernet.*\/[2-9]", led)]

   # LED types are the name prefixes of an LED, e.g. type for LED "Fan1"
   # is "Fan", and for "Ethernet12" is "Ethernet"
   ledTypeCounts = { }
   ledList = ArPyUtils.naturalsorted( ledList )
   for led in ledList:
      mg = re.match( r'([a-zA-Z]+)\d+', led )
      if mg:
         ledType = mg.group( 1 )
         if ledType not in ledTypeCounts:
            ledTypeCounts[ledType] = 1
      else:
         Tracing.trace0( "Unknown LED name found: ", led )

   ledTypes = list( ledTypeCounts )
   ledTypes.sort()
   Tracing.trace8( "Found LED types:", ",".join(ledTypes) )
   return ( ledTypes, ledList )

def getPlutoStatus( red, blue, green, yellow ):
   colors = ( red, blue, green, yellow )
   plutoStatusMapping = {
      # None
      ( False, False, False, False ) : PlutoStatus.plutoStatusOff,
      # Green
      ( False, False, True, False )  : PlutoStatus.plutoStatusGood,
      # Red
      ( True, False, False, False )  : PlutoStatus.plutoStatusBad,
      # Yellow
      ( False, False, False, True )  : PlutoStatus.plutoStatusInactive,
      # Blue
      ( False, True, False, False )  : PlutoStatus.plutoStatusBeacon,
   }

   if colors not in plutoStatusMapping:
      print( "warning: color settings unsupported by pluto" )
   return plutoStatusMapping.get( colors, 0 )

def illuminare( led, rawOptions, sync=True ):
   options = copy.copy( rawOptions )
   if led.startswith( "Ethernet" ) and options.red and not options.yellow:
      # michaelchin: because of how this test was written, we have taught
      # our front-panel port LED driving agents that "turn on red" actually
      # means "turn on yellow". For LEDs which are driven by the SCD, this
      # doesn't matter because to the SCD, red==yellow, but for LEDs driven
      # by other phy agents, such code just looks silly. This should fix that.
      options.red = False
      options.yellow = True
   plutoStatus = getPlutoStatus( options.red, options.blue,
                                 options.green, options.yellow )
   lightSetting = Tac.Value( "Led::LightSetting",
            blue=options.blue, green=options.green,
            red=options.red, yellow=options.yellow, flashRate=options.flash,
            rampRate=options.ramp, maxBright=options.max, minBright=options.min,
            plutoStatus=plutoStatus )
   diagSet = changeLedMode( ledConfig, "diagnostic" )
   diagSet.led[led] = lightSetting
   if sync:
      Tac.flushEntityLog()
      
testlist = [ '"' + k[:-4] + '"'
             for k,v in globals().items()
             if k.endswith( "Test" ) ]
if testlist == []:
   testlist = "<no tests presently defined>"
else:
   testlist = "      " + ", ".join( testlist ) + "\n"

# Get sysname and sysdbsockname for mounting
sysname = os.getenv( 'SYSNAME', 'ar' )
sysdbsockname = os.getenv( 'SYSDBSOCKNAME', None )

envDescription = """
Actual sysdb is mounted with sysname=%s and sysdbsockname=%s.
If the program is hanging you may want to change SYSNAME and/or
SYSDBSOCKNAME environment variables.
""" %( sysname, sysdbsockname )

ledConfig, ledHwConfig = initMount( sysname )
   
# Query initial LED types
ledTypes, ledList = buildLedList("")

usage = """
   %prog set [options]
      --device can be one of:
        """+repr(ledTypes)+"""

   %prog mode <normal|diagnostic>
      normal - return LEDs to normal usage
      diagnostic - diag mode

   %prog gettypes
   
   %prog test <test name>
      Run a particular test.  <test name> can be one of:
""" + testlist + envDescription
parser = optparse.OptionParser(usage=usage)
parser.add_option( "", "--fru", action="store", dest="fru", default="",
                   help="The Fru to run the test on, e.g. Linecard3 or Cell1" )
parser.add_option( "", "--device", action="append", dest="devices",
                   choices=ledTypes,
                   help="Device led to manipulate" )
parser.add_option( "", "--index", action="store", default="all",
                   help="Index of the device to use (default=%default)" )
parser.add_option( "-b", "--blue", action="store_true", default=False,
                   help="turn on blue led (if present)" )
parser.add_option( "-g", "--green", action="store_true", default=False,
                   help="turn on green led (if present)" )
parser.add_option( "-r", "--red", action="store_true", default=False,
                   help="turn on red led (if present)" )
parser.add_option( "-y", "--yellow", action="store_true", default=False,
                   help="turn on yellow led (if present)" )
parser.add_option( "-f", "--flash", action="store", default=0, type="int",
                   help="flash rate selector (default=0 (off))" )
parser.add_option( "--ramp", action="store", default=0x6, type="int",
                   help="ramp rate (default=0x6)" )
parser.add_option( "--max", action="store", default=100, type="int",
                   help="maximum brightness (default=100)" )
parser.add_option( "--min", action="store", default=0x00, type="int",
                   help="minimum brightness (default=0)" )
parser.add_option( "--debug", action="store_true", default=False,
                   help="display additional debugging information" )

options,args = parser.parse_args()
if len( args ) == 0:
   if cmd != "gettypes": # pylint: disable=used-before-assignment
      parser.error( """Missing <device> specification.
Which led do you want to turn on or off?""" )

returnCode = 0
cmd = args.pop( 0 )

if options.fru:
   # Rebuild the led types and list based on the user's input
   ledTypes, ledList = buildLedList(options.fru)

if cmd == "gettypes":
   if ledTypes:
      print( "LED types: ", ", ".join( ledTypes ) )
   else:
      print( "Cannot detect LED types" )
elif cmd == "set":
   devices = options.devices
   for device in options.devices:
      index = options.index
      if index == "all":
         leds = [ i for i in ledList if i.startswith( device ) ]
      else:
         leds = [ device+str(int( index )) ]
         
      for led in leds:
         if options.flash < 0 or options.flash > 5:
            parser.error( "Invalid flash rate register. Max is 5" )
         if options.ramp < 0 or options.ramp > 0xff:
            parser.error( "Invalid ramp rate specification." )
         if options.max < 0 or options.max > 100:
            parser.error( "Invalid maximum brightness specification." )
         if options.min < 0 or options.min > 100:
            parser.error( "Invalid minimum brightness specification." )
         illuminare( led, options )   
elif cmd == "test" :
   # additional test name required on command line
   if len( args ) < 1:
      parser.error( "Missing test name specification." )
   testname = args[0]
   
   commandTable = {  k[ :-4 ]:v for k,v in globals().items()
                    if k.endswith( "Test" )  }
   func = commandTable.get( testname )
   if func:
      try:
         returnCode = func( args, options )
      except KeyboardInterrupt:
         print( "\nUser interrupt, test aborted." )
   else:
      parser.error( "Invalid test: %s" % testname )
   # go back into normal mode
   changeLedMode( ledConfig, "normal" )
   
elif cmd == "mode":
   if args[0] in ( 'normal', 'diagnostic' ):
      diagSet = changeLedMode( ledConfig, args[ 0 ] )
      if diagSet:
         for led in ledList:
            ledOff = Tac.Value( "Led::LightSetting" )
            diagSet.led[ led ] = ledOff
         Tac.flushEntityLog()
   else:
      parser.error( "Invalid mode." )   
else:
   parser.error( "Unknown command %s" % cmd )

sys.exit( returnCode )
