def treeWalker():
    global startTrail,prevTrail, driver
    def getNodes():
        return getElems('#psbox3 li a' if prevTrail else '#psbox2 li a')
    nodes=getNodes()
    numNodes=len(nodes)

    if startTrail:
        startIdx=startTrail.pop(0)
    else:
        startIdx=0

    ulog('startTrail=%s'%startTrail)
    ulog('prevTrail=%s'%prevTrail)
    ulog('startIdx=%d'%startIdx)

    nodeTxts=[getElemText(_) for _ in getNodes()]
    ulog('nodes=%s (len=%d)'%([(i,_) for i,_ in enumerate(nodeTxts)],numNodes))
    for idx in range(startIdx, numNodes):
        try:
            crumbs=waitText('#psa_crumbs').replace('\n',' > ')
            ulog('crumbs=%s'%crumbs)
            nodeTxt = getElemText(nodes[idx])
            nodeId = nodes[idx].get_attribute('id').strip()
            ulog('nodeId="%s"'%nodeId)
            if not nodeId: # isLeaf
                ulog('Click Leaf "%s"'%nodeTxt)
                clickElem(nodes[idx])
                with suppress(StaleElementReferenceException):
                    nodes[idx].click()
                prevTrail+=[idx]
                # if not waitUntil(lambda: not driver.find_elements_by_css_selector('#psbox3')):
                #     nodes[idx].click()
                selectSoftwareType()
                prevTrail.pop()
                assert getCurDepthAtTreeWalker()==len(prevTrail)
                nodes=getNodes()
                continue
            with UntilTextChanged('#psbox3'):
                ulog('Click branch "%s"'%nodeTxt)
                clickElem(nodes[idx])
            prevTrail+=[idx]
            treeWalker()
            prevTrail.pop()
            nodes=getNodes()
        except Exception as ex:
            ipdb.set_trace()
            print(ex); traceback.print_exc()
            driver.save_screenshot('cisco_treeWalker.png')
    # end for
    crumbs=getElems('#psa_crumbs a')
    ulog('back to "%s"'%getElemText(crumbs[-1]))
    ulog('prevTrail=%s'%prevTrail)
    clickElem(crumbs[-1])
def selectSoftwareType():
    """ This page would be jumped to versionWalker() 
       or either jumped back to treeWalker
       forward: may auto jump
       backward: not auto jump
    """
    global startTrail,prevTrail,driver
    try:
        waitText('.csProductSelectorBreadcrumb', 5, 1)
        waitUntilStable('.csProductSelectorBreadcrumb', 1, 0.3)
        depth = getDepth()
        jumpedLevels =depth - len(prevTrail)
        ulog('jumpedLevels=%d'%jumpedLevels)
        assert jumpedLevels>=0
        ulog('depth=%d, but prevTrail=%s'%(depth, prevTrail))

        startIdxFromStartTrail=False
        def getStartIdx()->int:
            if startTrail:
                nonlocal startIdxFromStartTrail
                startIdxFromStartTrail=True
                return startTrail.pop(0)
            else:
                return 0

        if jumpedLevels>0:
            while depth>len(prevTrail):
                startIdx=getStartIdx()
                prevTrail+=[startIdx]
        else:
            startIdx=getStartIdx()

        assert depth==len(prevTrail)
        ulog('startTrail=%s'%startTrail)
        ulog('prevTrail=%s'%prevTrail)
        ulog('startIdx=%d'%startIdx)

        ulog('url=%s'%driver.current_url)
        crumbs = waitText('.csProductSelectorBreadcrumb')
        uprint('crumbs=%s'%(crumbs.replace('\n',' > ')))

        if not hasElem('table#imageTableContainer', 1.5,0.4):
            if jumpedLevels>0:
                startIdx=getStartIdx()
                if depth > len(prevTrail):
                    prevTrail+=[startIdx]
            sdpBannerTitle=waitText('td.SDPBannerTitle').strip()
            ulog('SDBBannerTitle="%s"'%sdpBannerTitle)
            assert sdpBannerTitle.lower().startswith('select ')
            waitUntil(lambda: getNumElem('div.csWrapper li a') > 0)
            swtypes = getElems('div.csWrapper li a')
            ulog('%s'%[(i,getElemText(_)) for i,_ in enumerate(swtypes)])
            numSwTypes=len(swtypes)
            assert numSwTypes > 0
            for idx in range(startIdx, numSwTypes):
                ulog('goto Trail=%s'%(prevTrail+[idx]))
                swtypes = getElems('div.csWrapper li a')
                ulog('Click "%s"'% getElemText(swtypes[idx]))
                clickElem(swtypes[idx])
                prevTrail+=[idx]
                selectSoftwareType()
                prevTrail.pop()
            # Select a Product -> Select a Software type -> Select a Platform
            # https://software.cisco.com/download/type.html?mdfid=277873153&flowid=170&softwareid=283724313
            # Downloads Home >Products >Cisco Interfaces and Modules >WAN Interface Cards >1700/2600/3600/3700 Series 2-Port Analog Modem WAN Interface Card >Analog Firmware Loader >Windows 2000-v6780 
            # not auto back to treeWalker
            # go back manually
            crumbs = getElems('.csProductSelectorBreadcrumb a')
            ulog('manually backto "%s"'%getElemText(crumbs[-1]))
            ulog('prevTail=%s'%prevTrail)
            clickElem(crumbs[-1])
            # do I need to pop prevTrail?
            # prevTrail.pop()
        else:
            ulog('auto forward to versionWalker')
            if startIdxFromStartTrail:
                startTrail.insert(0, startIdx)
            for i in range(jumpedLevels):
                if not startTrail:
                    break
                startTrail.pop(0)
            versionWalker()
        for i in range(jumpedLevels):
            crumbs = getElems('.csProductSelectorBreadcrumb a')
            ulog('manually backto "%s"'%getElemText(crumbs[-1]))
            ulog('prevTail=%s'%prevTrail)
            clickElem(crumbs[-1])
            prevTrail.pop()
    except Exception as ex:
        ipdb.set_trace()
        print(ex); traceback.print_exc()
        driver.save_screenshot('cisco_selectSoftwareType.png')
def versionWalker():
    global startTrail,prevTrail,driver
    try:
        waitClickable('.treeLinks > a:nth-child(1)')
        ulog('current_url=%s'%driver.current_url)
        crumbs=waitText('.csProductSelectorBreadcrumb').replace('\n', ' > ')
        ulog('crumbs=%s'%crumbs)

        # click Expand All
        numNodes = len(driver.find_elements_by_css_selector('.tree a'))
        ulog('number of versions=%d'%numNodes)
        try:
            with UntilTextChanged('.tree'):
                clickElem(waitClickable('.treeLinks > a:nth-child(1)'))
        except TimeoutException:
            pass
        treeText=waitText('.tree')
        ulog('treeText="%s"'%treeText)

        if startTrail:
            startIdx=startTrail.pop(0)
        else:
            startIdx=1
        ulog('startTrail=%s'%startTrail)
        ulog('prevTrail=%s'%prevTrail)
        ulog('startIdx=%d'%startIdx)
        assert startIdx >= 1

        try:
            prevFwVer=waitText('.tree a.nodeSel', 5)
        except TimeoutException:
            uprint("css='.tree a.nodeSel' not found")
            prevFwVer=None
        ulog('prevFwVer="%s"'%prevFwVer)
        for idx in range(startIdx, numNodes):
            nodes = driver.find_elements_by_css_selector('.tree a')
            isLeaf = (nodes[idx-1].text != '')
            ulog('goto Trail=%s'%(prevTrail+[idx]))
            if isLeaf:
                if not nodes[idx].text.strip():
                    continue
                fwVer=nodes[idx].text.strip()
                nodeClass=nodes[idx].get_attribute('class')
                ulog('fwVer="%s", nodeClass="%s"'%(fwVer,nodeClass))
                if 'nodeSel' not in nodeClass:
                    noWait= (prevFwVer==fwVer) if prevFwVer else False
                    try:
                        with UntilTextChanged('table#imageTableContainer',10,1,noWait):
                            ulog('Click "%s"'%fwVer)
                            clickElem(nodes[idx])
                    except TimeoutException:
                        with UntilTextChanged('table#imageTableContainer',10,1,noWait):
                            ulog('Click "%s" twice'%fwVer)
                            clickElem(nodes[idx])

                        
                prevTrail+=[idx]
                tableRowWalker(fwVer)
                prevTrail.pop()
                prevFwVer=fwVer
        # go back page
        crumbs=getElems('.csProductSelectorBreadcrumb a')
        ulog('backto "%s"'%getElemText(crumbs[-1]))
        ulog('prevTail=%s'%prevTrail)
        if prevTrail==[2, 1, 0, 1, 2, 0]:
            ipdb.set_trace()
        clickElem(crumbs[-1])
    except Exception as ex:
        ipdb.set_trace()
        print(ex); traceback.print_exc()
        driver.save_screenshot('cisco_versionWalker.png')