def __init__(self, graphSettings, areaGraph, restrictions, container,
              endDate):
     super(FillerProgSpeed, self).__init__(graphSettings.startAP, areaGraph,
                                           restrictions, container, endDate)
     distanceProp = 'GraphArea' if graphSettings.areaRando else 'Area'
     self.stdStart = GraphUtils.isStandardStart(self.startAP)
     self.progSpeedParams = ProgSpeedParameters(
         self.restrictions, len(container.unusedLocations))
     self.choice = ItemThenLocChoiceProgSpeed(restrictions,
                                              self.progSpeedParams,
                                              distanceProp, self.services)
class FillerProgSpeed(Filler):
    def __init__(self, graphSettings, areaGraph, restrictions, container, endDate):
        super(FillerProgSpeed, self).__init__(graphSettings.startAP, areaGraph, restrictions, container, endDate)
        distanceProp = 'GraphArea' if graphSettings.areaRando else 'Area'
        self.stdStart = GraphUtils.isStandardStart(self.startAP)
        self.progSpeedParams = ProgSpeedParameters(self.restrictions, len(container.unusedLocations))
        self.choice = ItemThenLocChoiceProgSpeed(restrictions, self.progSpeedParams, distanceProp, self.services)

    def initFiller(self):
        super(FillerProgSpeed, self).initFiller()
        self.states = []
        self.progressionItemLocs = []
        self.progressionStatesIndices = []
        self.rollbackItemsTried = {}
        self.lastFallbackStates = []
        self.initState = FillerState(self)

    def determineParameters(self):
        speed = self.settings.progSpeed
        if speed == 'variable':
            speed = self.progSpeedParams.getVariableSpeed()
        self.currentProgSpeed = speed
        self.choice.determineParameters(speed)
        self.minorHelpProb = self.progSpeedParams.getMinorHelpProb(speed)
        self.itemLimit = self.progSpeedParams.getItemLimit(speed) if not self.isEarlyGame() else 0
        self.locLimit = self.progSpeedParams.getLocLimit(speed)
        self.possibleSoftlockProb = self.progSpeedParams.getPossibleSoftlockProb(speed)
        self.progressionItemTypes = self.progSpeedParams.getProgressionItemTypes(speed)
        if self.restrictions.isEarlyMorph() and 'Morph' in self.progressionItemTypes:
            self.progressionItemTypes.remove('Morph')
        collectedAmmo = self.container.getCollectedItems(lambda item: item.Category == 'Ammo')
        collectedAmmoTypes = set([item.Type for item in collectedAmmo])
        ammos = ['Missile', 'Super', 'PowerBomb']
        if 'Super' in collectedAmmoTypes:
            ammos.remove('Missile')
        self.progressionItemTypes += [ammoType for ammoType in ammos if ammoType not in collectedAmmoTypes]

    def chooseItemLoc(self, itemLocDict, possibleProg):
        return self.choice.chooseItemLoc(itemLocDict, possibleProg, self.progressionItemLocs, self.ap, self.container)

    # during random fill at the end put suits first while there's still available locations which don't fall under the suits restriction
    def chooseItemLocNoLogic(self, itemLocDict):
        if self.settings.restrictions['Suits'] == True and self.container.hasItemInPool(lambda item: item.Type in ['Varia', 'Gravity']):
            itemLocDict = {key: value for key, value in itemLocDict.items() if key.Type in ['Varia', 'Gravity']}
        # pure random choice instead of prog-speed specific
        return ItemThenLocChoice.chooseItemLoc(self.choice, itemLocDict, False)

    def currentLocations(self, item=None):
        return self.services.currentLocations(self.ap, self.container, item=item)

    def getComebackCheck(self):
        if self.isEarlyGame() or self.services.can100percent(self.ap, self.container):
            return ComebackCheckType.NoCheck
        if random.random() >= self.possibleSoftlockProb:
            return ComebackCheckType.ComebackWithoutItem
        return ComebackCheckType.JustComeback

    # from current accessible locations and an item pool, generate an item/loc dict.
    # return item/loc, or None if stuck
    def generateItem(self, comeback=ComebackCheckType.Undefined):
        comebackCheck = comeback if comeback != ComebackCheckType.Undefined else self.getComebackCheck()
        itemLocDict, possibleProg = self.services.getPossiblePlacements(self.ap, self.container, comebackCheck)
        if self.isEarlyGame() and possibleProg == True:
            # cheat a little bit if non-standard start: place early
            # progression away from crateria/blue brin if possible
            startAp = getAccessPoint(self.startAP)
            if startAp.GraphArea != "Crateria":
                newItemLocDict = {}
                for w, locs in itemLocDict.items():
                    filtered = [loc for loc in locs if loc.GraphArea != 'Crateria']
                    if len(filtered) > 0:
                        newItemLocDict[w] = filtered
                if len(newItemLocDict) > 0:
                    itemLocDict = newItemLocDict
        itemLoc = self.chooseItemLoc(itemLocDict, possibleProg)
        self.log.debug("generateItem. itemLoc="+"None" if itemLoc is None else getItemLocStr(itemLoc))
        return itemLoc

    def getCurrentState(self):
        return self.states[-1] if len(self.states) > 0 else self.initState

    def appendCurrentState(self):
        curState = FillerState(self)
        self.states.append(curState)
        curState.states.append(curState)

    # collect specialization that stores progression and state
    def collect(self, itemLoc):
        isProg = self.services.isProgression(itemLoc.Item, self.ap, self.container)
        super(FillerProgSpeed, self).collect(itemLoc)
        if isProg:
            n = len(self.states)
            self.log.debug("prog indice="+str(n))
            self.progressionStatesIndices.append(n)
            self.progressionItemLocs.append(itemLoc)
        self.appendCurrentState()
        self.cache.reset()

    def isProgItem(self, item):
        if item.Type in self.progressionItemTypes:
            return True
        return self.services.isProgression(item, self.ap, self.container)

    def isEarlyGame(self):
        return len(self.progressionStatesIndices) <= 2 if self.stdStart else len(self.progressionStatesIndices) <= 3

    # check if remaining locations pool is conform to rando settings when filling up
    # with non-progression items
    def checkLocPool(self):
        sm = self.container.sm
 #       self.log.debug("checkLocPool {}".format([it.Name for it in self.itemPool]))
        if self.locLimit <= 0:
            return True
        progItems = self.container.getItems(self.isProgItem)
        self.log.debug("checkLocPool. progItems {}".format([it.Name for it in progItems]))
 #       self.log.debug("curItems {}".format([it.Name for it in self.currentItems]))
        if len(progItems) == 0:
            return True
        isMinorProg = any(self.restrictions.isItemMinor(item) for item in progItems)
        isMajorProg = any(self.restrictions.isItemMajor(item) for item in progItems)
        accessibleLocations = []
#        self.log.debug("unusedLocs: {}".format([loc.Name for loc in self.unusedLocations]))
        locs = self.currentLocations()
        for loc in locs:
            majAvail = self.restrictions.isLocMajor(loc)
            minAvail = self.restrictions.isLocMinor(loc)
            if ((isMajorProg and majAvail) or (isMinorProg and minAvail)) \
               and self.services.locPostAvailable(sm, loc, None):
                accessibleLocations.append(loc)
        self.log.debug("accesLoc {}".format([loc.Name for loc in accessibleLocations]))
        if len(accessibleLocations) <= self.locLimit:
            sys.stdout.write('|')
            sys.stdout.flush()
            return False
        # check that there is room left in all main areas
        room = {'Brinstar' : 0, 'Norfair' : 0, 'WreckedShip' : 0, 'LowerNorfair' : 0, 'Maridia' : 0 }
        if not self.stdStart:
            room['Crateria'] = 0
        for loc in self.container.unusedLocations:
            majAvail = self.restrictions.isLocMajor(loc)
            minAvail = self.restrictions.isLocMinor(loc)
            if loc.Area in room and ((isMajorProg and majAvail) or (isMinorProg and minAvail)):
                room[loc.Area] += 1
        for r in room.values():
            if r > 0 and r <= self.locLimit:
                sys.stdout.write('|')
                sys.stdout.flush()
                return False
        return True

    def addEnergyAsNonProg(self):
        collectedEnergy = self.container.getCollectedItems(lambda item: item.Category == 'Energy')
        return self.restrictions.split == 'Chozo' or (len(collectedEnergy) <= 2 and self.settings.progSpeed != 'slowest')

    def nonProgItemCheck(self, item):
        return (item.Category == 'Energy' and self.addEnergyAsNonProg()) or (not self.stdStart and item.Category == 'Ammo') or (self.restrictions.isEarlyMorph() and item.Type == 'Morph') or not self.isProgItem(item)

    def getNonProgItemPoolRestriction(self):
        return self.nonProgItemCheck

    def pickHelpfulMinor(self, item):
        self.helpfulMinorPicked = not self.container.sm.haveItem(item.Type).bool
        if self.helpfulMinorPicked:
            self.log.debug('pickHelpfulMinor. pick '+item.Type)
        return self.helpfulMinorPicked

    def getNonProgItemPoolRestrictionStart(self):
        self.helpfulMinorPicked = random.random() >= self.minorHelpProb
        self.log.debug('getNonProgItemPoolRestrictionStart. helpfulMinorPicked='+str(self.helpfulMinorPicked))
        return lambda item: (item.Category == 'Ammo' and not self.helpfulMinorPicked and self.pickHelpfulMinor(item)) or self.nonProgItemCheck(item)

    # return True if stuck, False if not
    def fillNonProgressionItems(self):
        if self.itemLimit <= 0:
            return False
        poolRestriction = self.getNonProgItemPoolRestrictionStart()
        self.container.restrictItemPool(poolRestriction)
        if self.container.isPoolEmpty():
            self.container.unrestrictItemPool()
            return False
        itemLocation = None
        nItems = 0
        locPoolOk = True
        self.log.debug("NON-PROG")
        while not self.container.isPoolEmpty() and nItems < self.itemLimit and locPoolOk:
            itemLocation = self.generateItem()
            if itemLocation is not None:
                nItems += 1
                self.log.debug("fillNonProgressionItems: {} at {}".format(itemLocation.Item.Name, itemLocation.Location.Name))
                # doing this first is actually important, as state is saved in collect
                self.container.unrestrictItemPool()
                self.collect(itemLocation)
                locPoolOk = self.checkLocPool()
                poolRestriction = self.getNonProgItemPoolRestriction()
                self.container.restrictItemPool(poolRestriction)
            else:
                break
        self.container.unrestrictItemPool()
        return itemLocation is None

    def generateItemFromStandardPool(self):
        self.log.debug('generateItemFromStandardPool')
        itemLoc = self.generateItem()
        if itemLoc is not None and self.currentProgSpeed in ['medium', 'slow', 'slowest'] and\
           not self.isEarlyGame() and not self.services.can100percent(self.ap, self.container):
            itemLocWithout = self.generateItem(ComebackCheckType.ComebackWithoutItem)
            if itemLocWithout is None:
                # the *only* available locations are locs we couldn't come back from without the item,
                # consider ourselves stuck (mitigates 'supers at spospo' syndrome)
                return None
        return itemLoc

    def getItemFromStandardPool(self):
        itemLoc = self.generateItemFromStandardPool()
        isStuck = itemLoc is None
        if not isStuck:
            sys.stdout.write('-')
            sys.stdout.flush()
            self.collect(itemLoc)
        return isStuck

    def initRollbackPoints(self):
        minRollbackPoint = 0
        maxRollbackPoint = len(self.states)
        if len(self.progressionStatesIndices) > 0:
            minRollbackPoint = self.progressionStatesIndices[-1]
        self.log.debug('initRollbackPoints: min=' + str(minRollbackPoint) + ", max=" + str(maxRollbackPoint))
        return minRollbackPoint, maxRollbackPoint

    def initRollback(self):
        self.log.debug('initRollback: progressionStatesIndices 1=' + str(self.progressionStatesIndices))
        if len(self.progressionStatesIndices) > 0 and self.progressionStatesIndices[-1] == len(self.states) - 1:
            # the state we are about to remove was a progression state
            self.progressionStatesIndices.pop()
        if len(self.states) > 0:
            self.states.pop() # remove current state, it's the one we're stuck in
        self.log.debug('initRollback: progressionStatesIndices 2=' + str(self.progressionStatesIndices))

    def getSituationId(self):
        progItems = str(sorted([il.Item.Type for il in self.progressionItemLocs]))
        position = str(sorted([ap.Name for ap in self.services.currentAccessPoints(self.ap, self.container)]))
        return progItems+'/'+position

    def hasTried(self, itemLoc):
        if self.isEarlyGame():
            return False
        itemType = itemLoc.Item.Type
        situation = self.getSituationId()
        ret = False
        if situation in self.rollbackItemsTried:
            ret = itemType in self.rollbackItemsTried[situation]
            if ret:
                self.log.debug('has tried ' + itemType + ' in situation ' + situation)
        return ret

    def updateRollbackItemsTried(self, itemLoc):
        itemType = itemLoc.Item.Type
        situation = self.getSituationId()
        if situation not in self.rollbackItemsTried:
            self.rollbackItemsTried[situation] = []
        self.log.debug('adding ' + itemType + ' to situation ' + situation)
        self.rollbackItemsTried[situation].append(itemType)

    def getFallbackState(self):
        self.log.debug("getFallbackState")
        curState = self.getCurrentState()
        fallbackState = self.states[-2] if len(self.states) > 1 else self.initState
        if (len(self.lastFallbackStates) > 0 and curState == self.lastFallbackStates[-1]):
            self.log.debug("getFallbackState. rewind fallback")
            return fallbackState
        # n = sum(1 for state in self.lastFallbackStates if state == fallbackState)
        # if n >= 3:
        #     self.log.debug("getFallbackState. kickstart needed")
        #     self.lastFallbackStates = None
        #     return None
        return curState

    # goes back in the previous states to find one where
    # we can put a progression item
    def rollback(self):
        nItemsAtStart = len(self.container.currentItems)
        nStatesAtStart = len(self.states)
        self.log.debug("rollback BEGIN: nItems={}, nStates={}".format(nItemsAtStart, nStatesAtStart))
        ret = None
        self.initRollback()
        if len(self.states) == 0:
            self.initState.apply(self)
            self.log.debug("rollback END initState apply, nCurLocs="+str(len(self.currentLocations())))
            if self.vcr != None:
                self.vcr.addRollback(nStatesAtStart)
            sys.stdout.write('<'*nStatesAtStart)
            sys.stdout.flush()
            return None
        # to stay consistent in case no solution is found as states list was popped in init
        fallbackState = self.getFallbackState()
        # if fallbackState is None: # kickstart needed
        #     return None
        self.lastFallbackStates.append(fallbackState)
        i = 0
        possibleStates = []
        self.log.debug('rollback. nStates='+str(len(self.states)))
        while i >= 0 and len(possibleStates) == 0:
            states = self.states[:] + [fallbackState]
            minRollbackPoint, maxRollbackPoint = self.initRollbackPoints()
            i = maxRollbackPoint
            while i >= minRollbackPoint:
                state = states[i]
                state.apply(self)
                self.log.debug('rollback. state applied. Container=\n'+self.container.dump())
                itemLoc = self.generateItemFromStandardPool()
                if itemLoc is not None and not self.hasTried(itemLoc) and self.services.isProgression(itemLoc.Item, self.ap, self.container):
                    possibleStates.append((state, itemLoc))
                i -= 1
            # nothing, let's rollback further a progression item
            if len(possibleStates) == 0 and i >= 0:
                if len(self.progressionStatesIndices) > 0:
                    sys.stdout.write('!')
                    sys.stdout.flush()
                    self.progressionStatesIndices.pop()
                else:
                    break
        if len(possibleStates) > 0:
            (state, itemLoc) = random.choice(possibleStates)
            self.updateRollbackItemsTried(itemLoc)
            state.apply(self)
            ret = itemLoc
            if self.vcr != None:
                nRoll = nItemsAtStart - len(self.container.currentItems)
                if nRoll > 0:
                    self.vcr.addRollback(nRoll)
        else:
            self.log.debug('fallbackState apply')
            fallbackState.apply(self)
            if self.vcr != None:
                self.vcr.addRollback(1)
        sys.stdout.write('<'*(nStatesAtStart - len(self.states)))
        sys.stdout.flush()
        self.log.debug("rollback END: {}".format(len(self.container.currentItems)))
        return ret

    # def kickStart(self):
    #     self.initState.apply(self)
    #     self.lastFallbackStates = []
    #     pairItemLocDict = self.services.getStartupProgItemsPairs(self.ap, self.container)
    #     if pairItemLocDict == None:
    #         # no pair found
    #         self.log.debug("kickStart KO")
    #         return False
    #     self.collectPair(pairItemLocDict)
    #     self.log.debug("kickStart OK")
    #     return True

    def step(self, onlyBossCheck=False):
        self.cache.reset()
        if self.services.can100percent(self.ap, self.container) and self.settings.progSpeed not in ['slowest', 'slow']:
            (itemLocDict, isProg) = self.services.getPossiblePlacementsNoLogic(self.container)
            itemLoc = self.chooseItemLocNoLogic(itemLocDict)
            if itemLoc is None:
                self.restrictions.disable()
                self.cache.reset()
                self.errorMsg = "Restrictions disabled"
                (itemLocDict, isProg) = self.services.getPossiblePlacementsNoLogic(self.container)
                itemLoc = self.chooseItemLocNoLogic(itemLocDict)
            assert itemLoc is not None
            self.ap = self.services.collect(self.ap, self.container, itemLoc)
            return True
        self.determineParameters()
        # fill up with non-progression stuff
        isStuck = self.fillNonProgressionItems()
        if not self.container.isPoolEmpty():
            isStuck = self.getItemFromStandardPool()
            if isStuck:
                if onlyBossCheck == False and self.services.onlyBossesLeft(self.ap, self.container):
                    self.settings.maxDiff = infinity
                    return self.step(onlyBossCheck=True)
                if onlyBossCheck == True:
                    # we're stuck even after bumping diff.
                    # it was a onlyBossesLeft false positive, restore max diff
                    self.settings.maxDiff = self.maxDiff
                # check that we're actually stuck
                itemLoc = None
                if not self.services.can100percent(self.ap, self.container):
                    # stuck, rollback to make progress if we can't access everything yet
                    itemLoc = self.rollback()
                    # if itemLoc is None and self.lastFallbackStates is None:
                    #     # kickstart needed
                    #     return self.kickStart()
                if itemLoc is not None:
                    self.collect(itemLoc)
                    isStuck = False
                else:
                    isStuck = self.getItemFromStandardPool()
#        self.log.debug("step end. itemLocations="+getItemLocationsStr(self.container.itemLocations))
        return not isStuck

    def getProgressionItemLocations(self):
        return self.progressionItemLocs