def __init__(self,
                 randomSeed = 0,
                 parameterized = True,
                 definesProbability = 0.5,
                 constantsProbability = 0.0,
                 verbose = False,
                 verbose_super = False):
        """
        Randomly changes values of the L-System.
        """
        self.verbose = verbose
        self.verbose_super = verbose

        self.lsystem = ParametricLSystem()    # Init with a empty Lsystem

        self.parameterized = parameterized
        self.rnd = random.Random()
        if randomSeed > 0: self.rnd.seed(randomSeed)

        self.template_modules_library = ModulesTemplateLibrary(self,self.rnd,parameterized,definesProbability,constantsProbability)

        # Parameters
        self.targetIterations = 3               # How many iterations for the resulting lsystem
        self.branchProbability = 0.5            # [0,1] @note: Used only when generating a random pstring
        self.branchCloseProbability = 0.2       # [0,1] If high, it will close branches soon after opening them. If low, it will let them be larger.
 def copyFrom(other_instance):
     new_instance = GeneticInstance(ParametricLSystem.copyFrom(other_instance.lsystem))
     new_instance.copyParametersFrom(other_instance)
     return new_instance
class RandomGenerator():
    """
    Randomly generates a L-system with a uniform distribution.
    """

    def __init__(self,
                 randomSeed = 0,
                 parameterized = True,
                 definesProbability = 0.5,
                 constantsProbability = 0.0,
                 verbose = False,
                 verbose_super = False):
        """
        Randomly changes values of the L-System.
        """
        self.verbose = verbose
        self.verbose_super = verbose

        self.lsystem = ParametricLSystem()    # Init with a empty Lsystem

        self.parameterized = parameterized
        self.rnd = random.Random()
        if randomSeed > 0: self.rnd.seed(randomSeed)

        self.template_modules_library = ModulesTemplateLibrary(self,self.rnd,parameterized,definesProbability,constantsProbability)

        # Parameters
        self.targetIterations = 3               # How many iterations for the resulting lsystem
        self.branchProbability = 0.5            # [0,1] @note: Used only when generating a random pstring
        self.branchCloseProbability = 0.2       # [0,1] If high, it will close branches soon after opening them. If low, it will let them be larger.
        #self.definesProbability =  0.5          # [0,1]
        #self.constantsProbability =  1.0        # [0,1]

    def __str__(self):
        s = "Randomizer"
        if self.lsystem is not None: s += " with lsystem " + str(self.lsystem)
        return s

    def setLSystem(self,lsystem):
        """
        Sets the L-System that will be modified

        @param lsystem: The L-System to modify.
        @type lsystem: ParametricLSystem
        """
        self.lsystem = lsystem
        self.updateCurrentlyUsedLetters()

    def clear(self):
        """
        Sets empty values
        """
        self.lsystem.clear()
        self.updateCurrentlyUsedLetters()

    def resetToSimple(self):
        """
        Resets the L-System to a simple format
        """
        self.clear()
        self.createSimpleLSystem()

    def createSimpleLSystem(self):
        """
        Creates the simplest Lsystem for this generator. Can be extended.
        """
        self.lsystem.setIterations(self.targetIterations)
        self.lsystem.setAxiomFromString("")
        self.lsystem.axiom.appendModule(self.generateModuleOfLetter("F"))
        self.updateCurrentlyUsedLetters()

    def updateCurrentlyUsedLetters(self):
        """
        Updates the list of ParametricModule letters that are already in use in the current L-System.
        """
        self.currentlyUsedLetters = []

        def checkAddToCurrentlyUsedLetters(self,s):
            if s not in self.currentlyUsedLetters:
                self.currentlyUsedLetters.append(s)

        parametricAxiom = self.lsystem.axiom
        if parametricAxiom is not None:
            available_letters = [m.letter for m in self.template_modules_library.getTemplateModules()]
            for pModule in parametricAxiom:
                if pModule.letter in available_letters: checkAddToCurrentlyUsedLetters(self,pModule.letter)

        for production in self.lsystem.productions:
            if production.predecessor.letter in available_letters: checkAddToCurrentlyUsedLetters(self,production.predecessor.letter)
            for pModule in production.successor:
                if pModule.letter in available_letters: checkAddToCurrentlyUsedLetters(self,pModule.letter)

    def randomize(self):
        """
        Randomize all the values of the current L-System.
        """
        self.lsystem.clear()
        #self.lsystem.setIterations(self.rnd.randint(MIN_RANDOM_ITERATIONS,MAX_RANDOM_ITERATIONS))
        self.lsystem.setIterations(self.targetIterations)   # The iterations are fixed

        ndefines = self.rnd.randint(MIN_RANDOM_DEFINES,MAX_RANDOM_DEFINES)
        self.generateRandomDefines(ndefines)

        self.lsystem.axiom = self.generateRandomParametricString()

        nproductions = self.rnd.randint(MIN_RANDOM_PRODUCTIONS,MAX_RANDOM_PRODUCTIONS)
        self.generateRandomProductions(nproductions)

    def generateRandomDefines(self,ndefines):
        for i in range(ndefines): self.generateRandomDefine()

    def generateRandomDefine(self):
        return self.addGlobalDefine("d"+str(len(self.lsystem.globalDefines)),
                                    Utilities.getRandomTwoDigitFloat(self.rnd,MIN_DEFINE_VALUE,MAX_DEFINE_VALUE))


    def generateRandomProductions(self,nproductions):
        for i in range(nproductions):
            self.generateAndAddRandomProduction()

    ########################
    # Utilities
    ########################


    # Modules from templates

    def generatePredecessorOfLetter(self,letter):
        """ Generates a Predecessor Module from a letter """
        templateModule = self.template_modules_library.getTemplateModuleByLetter(letter)
        module = self.template_modules_library.createActualModuleFromTemplate(templateModule, isPredecessor = True)
        return module

    def generateModuleOfLetter(self,letter):
        """ Generates a Module from a letter """
        templateModule = self.template_modules_library.getTemplateModuleByLetter(letter)
        module = self.template_modules_library.createActualModuleFromTemplate(templateModule)
        return module

    def generateRandomModule(self, notUsingModules = []):
        """ Returns a random Module """
        notUsingLetters = list(m.letter for m in notUsingModules)
        templateModules = self.template_modules_library.getTemplateModules()
        templateModules = list(filter(lambda m: m.letter not in notUsingLetters, templateModules))
        templateModules = self.getRandomItemFromList(templateModules)
        module = self.template_modules_library.createActualModuleFromTemplate(templateModules)
        return module

    # Modules from other stuff

    def generateWeightedRandomModule(self):
        """ Returns a random ParametricModule based on the supplied weights """
        templateModules = self.template_modules_library.getTemplateModules()
        templateModule = self.getRandomItemFromWeightedList([s for s in templateModules],[s.weight for s in templateModules])
        module = self.template_modules_library.createActualModuleFromTemplate(templateModule)
        return module

    def generateRandomCurrentModule(self):
        """ Returns a random ParametricModule from the currently used ones """
        module = ParametricModule(self.getRandomItemFromList(self.currentUsedModules))
        return module

    def generateRandomCurrentModuleNotUsedAsPrecedent(self):
        """
        Returns a random ParametricModule from the currently used ones, if not already used in a predecessor.
        Also makes sure the module has correct parameters.
        """
        #print(self.lsystem)
        notUsedAsPrecedentLetters = list(filter(self.letterIsNotUsedAsPrecedent,self.currentlyUsedLetters))
        #print("Not used as precedent letters: " + str(notUsedAsPrecedentLetters))
        if len(notUsedAsPrecedentLetters) == 0: return None
        letter = self.getRandomItemFromList(notUsedAsPrecedentLetters)
        #print("Chosen letter: " + str(letter))
        module = self.generatePredecessorOfLetter(letter)
        return module

    def generateRandomPredecessor(self):
        return self.generateRandomModule()

    def generateRandomPredecessorFromCurrentlyUsedModules(self):
        return self.generateRandomCurrentModuleNotUsedAsPrecedent()

    # Checks for modules

    def hasLettersNotUsedInPrecedents(self):
        notUsedAsPrecedentLetters = list(filter(self.letterIsNotUsedAsPrecedent,self.currentlyUsedLetters))
        return len(notUsedAsPrecedentLetters) > 0

    def letterIsNotUsedAsPrecedent(self,s):
        for prod in self.lsystem.productions:
            if s == prod.predecessor.letter: return False
        return True

    # Parametric Strings

    def generateRandomParametricString(self, minLength = MIN_GENERATED_STRING_LENGTH, maxLength = MAX_GENERATED_STRING_LENGTH, predecessor_module = None):
        """
        Generates a random string composed of ParametricModules.
        May also randomly open and close branches.
        May also randomly add new defines.
        Forces the string's validity.

        If a predecessor_module is passed, its parameter variables (x,y) need to appear in the generated string
        We add them to the current generated string at the end.
        """
        if self.branchProbability > 0.0: openBranches = 0
        n_modules = self.rnd.randint(minLength,maxLength)
        pstring = ParametricString()
        pstring.setGlobals(self.lsystem.globalDefines)

        for i in range(n_modules):  # We add N modules

            # Generate the module
            new_module = self.generateWeightedRandomModule()#parametric=True)

            # Randomly open branches (NOT: only if the module is a new rotation, otherwise it doesn't make sense)
            if self.branchProbability > 0.0:
                if openBranches > 0 and self.rnd.random() < self.branchCloseProbability:
                    pstring.appendCloseBranch()
                    openBranches -= 1
                if self.rnd.random() < self.branchProbability:   #new_module.isOrientation() and
                    pstring.appendOpenBranch()
                    openBranches += 1

            # Append the module
            pstring.appendModule(new_module)

        # Close the remaining branches
        if self.branchProbability > 0.0:
            for i in range(openBranches): pstring.appendCloseBranch()

        # Change some of the parameters to the predecessor module's variable parameters
        if self.parameterized:
            print("PRED: " + str(predecessor_module))
            if predecessor_module is not None:
                potential_changed_modules = pstring.modulesList
                potential_changed_modules = list(filter(lambda p: not p.isBracket(), pstring.modulesList))

                #print "Potential changed modules: "
                #for m in potential_changed_modules: print m

                # Each predecessor parameter is copied once, if possible
                for param in predecessor_module.params:
                    chosen_module = self.getRandomItemFromList(potential_changed_modules)
                    #if len(chosen_module.params) > 0:
                    index = self.rnd.randint(0,len(chosen_module.params)-1)
                    chosen_module.params[index] = param

        return pstring

    def generateEmptyParametricString(self):
        pstring = ParametricString()
        pstring.setGlobals(self.lsystem.globalDefines)
        return pstring

    # Defines

    def addGlobalDefine(self,name,value):
        return self.lsystem.addGlobalDefine(name,value)

    def overrideGlobalDefine(self,i,name,value):
        self.lsystem.overrideGlobalDefine(i,name,value)

    # Productions

    def getProduction(self,i):
        return self.lsystem.productions[i]

    def addExistingProduction(self,prod):
        self.lsystem.addExistingProduction(prod)

    def addNewProduction(self):
        return self.lsystem.addNewProduction()

    def addProductionFromElements(self,pre,cond,succ):
        prod = self.addNewProduction()
        prod.setPredecessorModule(pre)
        prod.setConditionFromString(cond)
        prod.setSuccessorPstring(succ)
        return prod

    def overrideProduction(self,i,prod):
        self.lsystem.overrideProduction(i,prod)

    def deleteProductionAt(self,index,redistribute = True):
        """
        Delete a production.

        @param redistribute: If True and the deleted production was stochastic, the remaining value is redistributed.
        @type redistribute: bool
        """
        deleted_production = self.lsystem.productions[index]
        del  self.lsystem.productions[index]

        if redistribute:
            # If this production was stochastic, we need to redistribute its weight to the other productions with the same predecessor
            condition_type = deleted_production.condition.type
            if condition_type == '#':
                predecessor_letter = deleted_production.predecessor.letter
                condition_value = deleted_production.condition.value
                self.redistributeStochasticValue(predecessor_letter,condition_value)

        return deleted_production

    def redistributeStochasticValue(self,predecessor_letter,redistributed_value,excludedProduction=None):
        remaining_value = 1-redistributed_value
        print("Redistributing weight: the weight to distribute is " + str(redistributed_value) + " and the remanining weight is " + str(remaining_value))
        for prod in self.lsystem.productions:
            if prod.predecessor.letter == predecessor_letter and prod != excludedProduction:
                print("Production has value " + str(prod.condition.value))
                prod.condition.value = round((prod.condition.value+prod.condition.value/redistributed_value*redistributed_value)*100)/100.0
                print("Production is thus given value " + str(prod.condition.value))

    def generateAndAddRandomProduction(self):
        #TODO: Add stochastic variations
        self.addProductionFromElements(self.generateRandomPredecessor(),"*",self.generateRandomParametricString())

    def getNumberOfExistingProductions(self):
        return len(self.lsystem.productions)

    def getRandomExistingProduction(self, maxLength = None, containedLetters=[],
                                    atLeastCountLetters = 1,
                                    doNotConsiderProductionWithPredecessor=None,
                                    containsBranches = None):
        """
        Returns one of the existing productions.
        Ignores productions that reached maxLength with their successor.
        Returns None if no production exists
        """
        if self.getNumberOfExistingProductions() == 0: return None, -1

        availableIndices = self.getAllProductionIndicesWith(maxLength,containedLetters,atLeastCountLetters,doNotConsiderProductionWithPredecessor,containsBranches)

        production_index = self.getRandomItemFromList(availableIndices)
        if len(availableIndices) == 0: return None, -1
        return self.lsystem.productions[production_index], production_index

    def getAllProductionIndicesWith(self, maxLength = None, containedLetters=[],
                                    atLeastCountLetters = 1,
                                    doNotConsiderProductionWithPredecessor=None,
                                    containsBranches = None):
        availableIndices = [i for i in range(self.getNumberOfExistingProductions())]
        for production_index in range(self.getNumberOfExistingProductions()):
            production = self.lsystem.productions[production_index]
            successor = production.successor
            if maxLength and len(successor) > maxLength:
                availableIndices.remove(production_index)
            elif containsBranches is not None and successor.hasBranches() != containsBranches:
                availableIndices.remove(production_index)
            elif len(containedLetters)>0 and not successor.containsAllLettersAtLeastCount(containedLetters,atLeastCountLetters):
                availableIndices.remove(production_index)
            elif doNotConsiderProductionWithPredecessor is not None and production.predecessor.letter == doNotConsiderProductionWithPredecessor:
                availableIndices.remove(production_index)
        if len(availableIndices) == 0: return []
        return availableIndices

    def hasProductionsWith(self, maxLength = None, containedLetters=[],
                                    atLeastCountLetters = 1,
                                    doNotConsiderProductionWithPredecessor=None,
                                    containsBranches = None):
        availableIndices = self.getAllProductionIndicesWith(maxLength,containedLetters,atLeastCountLetters,doNotConsiderProductionWithPredecessor,containsBranches)
        return len(availableIndices) > 0


    def getRandomExistingStochasticProduction(self):
        def isStochastic(production): return production.condition.type == '#'
        prods = list(filter(isStochastic,self.lsystem.productions))
        return self.getRandomItemFromList(prods)

    # Various
    def getRandomItemFromList(self,list):
        if len(list) == 0: return None
        return list[self.rnd.randint(0,len(list)-1)]

    def getRandomItemFromWeightedList(self,list,weights):
        if len(list) == 0: return None
        assert(len(list) == len(weights))
        tot_weight = 0
        n = range(len(list))
        tot_weight = sum(weights)

        choice = self.rnd.randint(0,tot_weight)
        cumulative_weight = 0
        for i in n:
            cumulative_weight += weights[i]
            if choice <= cumulative_weight:
                return list[i]
        assert(False)  # Should never pass here


    #####################
    # Mutations
    #####################

    def appendModuleOfLetter(self,letter,pString):
        new_module = self.generateModuleOfLetter(letter)
        return self.appendModuleToPstring(new_module,pString)

    def appendModuleToPstring(self,new_module,pString):
        pString = ParametricString.copyFrom(pString)
        pString.modulesList.append(new_module)
        if new_module.letter not in self.currentlyUsedLetters:  self.currentlyUsedLetters.append(new_module.letter)
        return pString

    def insertModuleOfLetterRandomly(self,letter,pString):
        new_module = self.generateModuleOfLetter(letter)
        return self.insertModuleIntoPstringRandomly(new_module,pString)

    def insertModuleIntoPstringRandomly(self,new_module,pString):
        pString = ParametricString.copyFrom(pString)
        index = self.rnd.randint(0,len(pString))    # Max is len(pString) so that we can also insert at the end
        pString.modulesList.insert(index,new_module)
        if new_module.letter not in self.currentlyUsedLetters:  self.currentlyUsedLetters.append(new_module.letter)
        return pString

    def addRandomModule(self,pString):
        """
        Add a random Module to a ParametricString, chosen from the available ones.
        This may select an unused Module too.
        #TODO: this is actually an "insertInRandomModule"
        """
        #print("Current pString: " + str(pString))
        pString = ParametricString.copyFrom(pString)
        new_module = self.generateWeightedRandomModule()#parametric=True)
        index = self.rnd.randint(0,len(pString))    # Max is len(pString) so that we can also insert at the end
        #print("From string " + str(pString) + " we add at index " + str(index) + " a new Module " + str(new_module))
        pString.modulesList.insert(index,new_module)
        #print("Result: " + str(pString))

        if new_module.letter not in self.currentlyUsedLetters:
            self.currentlyUsedLetters.append(new_module.letter)

        return pString

    def removeModuleOfLetter(self,letter,pString):
        module = pString.getFirstModuleOfLetter(letter)
        return self.removeModule(module, pString)

    # TODO: all these 'append', 'remove', 'insert' and so on would be better placed in the ParametricString class
    def removeModule(self,module,input_pstring):
        #print(module)
        #print(pString)
        output_pstring = ParametricString.copyFrom(input_pstring)
        if module in input_pstring.modulesList:
            index = input_pstring.index(module)
            removeBrackets = self.checkBracketsRemoval(index,output_pstring)
            del output_pstring.modulesList[index]
            if removeBrackets: self.performBracketsRemoval(index,output_pstring)
        return output_pstring

    def checkBracketsRemoval(self,index,pString):
        # We check if we need to remove the brackets: if no other module is around the chosen index
        return index > 0 and index < len(pString)-1 and pString[index-1].isOpenBracket() and pString[index+1].isClosedBracket()

        """print(index>0)
        print( len(pString)-1)
        print(index < len(pString)-1 )
        print(pString[index-1] )
        print(str(pString[index-1]) == '[')
        if index < len(pString)-1:
            print(pString[index+1] )
            print(type(pString[index+1]))
            print(str(pString[index+1]) == "]")
        print("\nWe got pString: " + str(pString))
        print("We'll remove at index " + str(index) + " the letter " + str(pString[index]))
        print("Also removing brackets? " + str(removeBrackets))"""

    def performBracketsRemoval(self,index,pString):
        removeBrackets = True
        while removeBrackets is True:
            #print("Now pString is " + str(pString))
            del pString.modulesList[index]
            #print("Now pString is " + str(pString))
            del pString.modulesList[index-1]
            #print("Now pString is " + str(pString))
            #if len(pString) > 0: print("At left we now have " + str(pString[index-2]))

            # Keep checking for brackets
            index = index-1
            if not self.checkBracketsRemoval(index,pString):
                #print("We finished removing brackets!")
                removeBrackets = False
            #else:
                #print("We still need to remove brackets!")

        #print("Final pString is " + str(pString) +"\n")
        return pString

    def removeRandomModule(self,pString):
        pString = ParametricString.copyFrom(pString)
        available_letters = [m.letter for m in self.template_modules_library.getTemplateModules()]
        while True:
            index = self.rnd.randint(0,len(pString)-1)
            if not pString[index].isBracket() and pString[index].letter in available_letters: break

        # We also remove the brackets if no other module is there
        removeBrackets = self.checkBracketsRemoval(index, pString)
        del pString.modulesList[index]
        if removeBrackets: self.performBracketsRemoval(index, pString)
        #TODO: Do something if we completely remove this production
        return pString

    def changeRandomModule(self,pString):
        pString = ParametricString.copyFrom(pString)
        available_letters = [m.letter for m in self.template_modules_library.getTemplateModules()]

        # Choose what module we want to change
        modules = pString.getActualModules()
        modules = list(filter(lambda m: m.letter in available_letters,modules))
        index = self.rnd.randint(0,len(modules)-1)
        module_to_change = modules[index]
        #print("We will change module at : " + str(index) + " " + str(module_to_change))

        # Choose what we will change it to, avoid replacing with the same letter
        new_module = self.generateRandomModule(notUsingModules = [module_to_change])
        #print("Changed to " + str(new_module))

        # If parameterized, we make sure that the parameter value remains the same, if possible
        if self.parameterized:
            for i in range(len(new_module.params)):
                if i < len(module_to_change.params):
                    new_module.params[i] = module_to_change.params[i]

        #print("From string " + str(string) + " we change module " + str(string[index]) + " at index " + str(index) + " to a new module " + new_module)
        pString.modulesList[pString.modulesList.index(module_to_change)] = new_module
        #print("Result: " + string)
        return pString

    def deleteRandomProduction(self):
        index = self.rnd.randint(0,len(self.lsystem.productions)-1)
        self.deleteProductionAt(index)