from eLCS.Constants import cons
from eLCS.Classifier import Classifier

import random
import copy


class ClassifierSet(object):
    """This module handles all the classifier sets

    This includes the population, match set and correct sets along with mechanisms and
    heuristics that act on these sets.

    This class can be initialized with the:
    1.  Creation of a new population, or
    2.  Reboots the population (i.e. read in from a previously saved population)
    """

    def __init__(self, pop_reboot_path=None):
        """Initializes the Classifier Set

        :param str pop_reboot_path: Path to the population, defaults to None
        """

        # Major Parameters
        self.popSet = []  # List of classifiers/rules (list of ClassiferSet objects)
        self.matchSet = []  # List of references to rules in population that match
        self.correctSet = []  # List of references to rules in population that both match and specify correct phenotype
        self.microPopSize = 0  # Tracks the current micro population size, i.e. the population size which takes rule numerosity into account.
        self.runtimeParams = []  # List that stores the result at each iteration

        # Evaluation Parameters
        self.aveGenerality = 0.0
        self.expRules = 0.0
        self.attributeSpecList = []
        self.attributeAccList = []
        self.avePhenotypeRange = 0.0

        # Set Constructors
        if pop_reboot_path == None:
            # Initialize a new population
            self.makePop()

        elif isinstance(pop_reboot_path, str):
            # Initialize a population based on an existing saved rule population
            self.rebootPop(pop_reboot_path)

        else:
            print("ClassifierSet: Error building population.")

    # Population Constructor Methods
    def makePop(self):
        """ Initializes the rule population, as an empty list"""
        self.popSet = []

    def rebootPop(self, pop_reboot_path):
        """Remakes a previously evolved population from a saved text file

        :param pop_reboot_path:
        :return:
        """

        print("Rebooting the following population: " + str(pop_reboot_path) + "_RulePop.txt")
        # *******************Initial file handling**********************************************************
        datasetList = []
        try:
            f = open(pop_reboot_path + "_RulePop.txt", 'r')
        except Exception as inst:
            print(type(inst))
            print(inst.args)
            print(inst)
            print('cannot open', pop_reboot_path + "_RulePop.txt")
            raise
        else:
            self.headerList = f.readline().rstrip('\n').split('\t')  # strip off first row
            for line in f:
                lineList = line.strip('\n').split('\t')
                datasetList.append(lineList)
            f.close()

        # **************************************************************************************************
        for each in datasetList:
            cl = Classifier(each)
            self.popSet.append(cl)
            numerosityRef = cons.env.formatData.numAttributes + 3
            self.microPopSize += int(each[numerosityRef])
        print("Rebooted Rule Population has " + str(len(self.popSet)) + " Macro Pop Size.")

    # Classifier set constructor methods
    def makeMatchSet(self, state_phenotype, exploreIter):
        """Constructs a match set from the population

        Covering is initiated if the match set is empty or a rule with the current correct phenotype is absent.

        :param list state_phenotype: Listing consisting of the training state and training phenotype
        :param int exploreIter: The current iteration
        """

        # Initial values
        state = state_phenotype[0]
        phenotype = state_phenotype[1]

        # Covering check:
        # 1.  Checks that a match is present, and
        # 2.  That at least one match dictates the correct phenotype.
        doCovering = True
        setNumerositySum = 0

        # Carry out matching
        cons.timer.startTimeMatching()

        for i in range(len(self.popSet)):  # Go through the population
            cl = self.popSet[i]  # One classifier at a time
            if cl.match(state):  # Check for match
                self.matchSet.append(i)  # If match - add classifier to match set
                setNumerositySum += cl.numerosity  # Increment the set numerosity sum

                # Covering Check--------------------------------------------------------
                if cons.env.formatData.discretePhenotype:  # Discrete phenotype
                    if cl.phenotype == phenotype:  # Check for phenotype coverage
                        doCovering = False
                else:  # Continuous phenotype
                    if float(cl.phenotype[0]) <= float(phenotype) <= float(
                            cl.phenotype[1]):  # Check for phenotype coverage
                        doCovering = False
        cons.timer.stopTimeMatching()

        # -------------------------------------------------------
        # COVERING
        # -------------------------------------------------------
        while doCovering:
            newCl = Classifier(setNumerositySum + 1, exploreIter, state, phenotype)
            self.addClassifierToPopulation(newCl, True)
            self.matchSet.append(len(self.popSet) - 1)  # Add covered classifier to matchset
            doCovering = False

    def makeCorrectSet(self, phenotype):
        """Constructs a correct set out of the given match set

        :param phenotype:
        :return:
        """
        for i in range(len(self.matchSet)):
            ref = self.matchSet[i]
            # -------------------------------------------------------
            # DISCRETE PHENOTYPE
            # -------------------------------------------------------
            if cons.env.formatData.discretePhenotype:
                if self.popSet[ref].phenotype == phenotype:
                    self.correctSet.append(ref)
                    # -------------------------------------------------------
            # CONTINUOUS PHENOTYPE
            # -------------------------------------------------------
            else:
                if float(phenotype) <= float(self.popSet[ref].phenotype[1]) and float(phenotype) >= float(
                        self.popSet[ref].phenotype[0]):
                    self.correctSet.append(ref)

    def makeEvalMatchSet(self, state):
        """Constructs a match set for evaluation purposes which does not activate either covering or deletion.

        :param state:
        :return:
        """
        for i in range(len(self.popSet)):  # Go through the population
            cl = self.popSet[i]  # A single classifier
            if cl.match(state):  # Check for match
                self.matchSet.append(i)  # Add classifier to match set

    # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    # CLASSIFIER DELETION METHODS
    # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    def deletion(self, exploreIter):
        """Returns the population size back to the maximum set by the user by deleting rules.

        :param exploreIter:
        :return:
        """
        cons.timer.startTimeDeletion()
        while self.microPopSize > cons.N:
            self.deleteFromPopulation()
        cons.timer.stopTimeDeletion()

    def deleteFromPopulation(self):
        """ Deletes one classifier in the population.

        The classifier that will be deleted is chosen by roulette wheel selection considering the deletion vote.
        Returns the macro-classifier which got decreased by one micro-classifier.
        """
        meanFitness = self.getPopFitnessSum() / float(self.microPopSize)

        # Calculate total wheel size------------------------------
        sumCl = 0.0
        voteList = []
        for cl in self.popSet:
            vote = cl.getDelProp(meanFitness)
            sumCl += vote
            voteList.append(vote)
        # --------------------------------------------------------
        choicePoint = sumCl * random.random()  # Determine the choice point

        newSum = 0.0
        for i in range(len(voteList)):
            cl = self.popSet[i]
            newSum = newSum + voteList[i]
            if newSum > choicePoint:  # Select classifier for deletion
                # Delete classifier----------------------------------
                cl.updateNumerosity(-1)
                self.microPopSize -= 1
                if cl.numerosity < 1:  # When all micro-classifiers for a given classifier have been depleted.
                    self.removeMacroClassifier(i)
                    self.deleteFromMatchSet(i)
                    self.deleteFromCorrectSet(i)
                return

        print("ClassifierSet: No eligible rules found for deletion in deleteFromPopulation.")
        return

    def removeMacroClassifier(self, ref):
        """Removes the specified (macro-) classifier from the population.

        :param ref:
        :return:
        """
        self.popSet.pop(ref)

    def deleteFromMatchSet(self, deleteRef):
        """Delete reference to classifier in population, contained in self.matchSet.

        :param deleteRef:
        :return:
        """
        if deleteRef in self.matchSet:
            self.matchSet.remove(deleteRef)

        # Update match set reference list--------
        for j in range(len(self.matchSet)):
            ref = self.matchSet[j]
            if ref > deleteRef:
                self.matchSet[j] -= 1

    def deleteFromCorrectSet(self, deleteRef):
        """Delete reference to classifier in population, contained in self.corectSet.

        :param deleteRef:
        :return:
        """
        if deleteRef in self.correctSet:
            self.correctSet.remove(deleteRef)

        # Update match set reference list--------
        for j in range(len(self.correctSet)):
            ref = self.correctSet[j]
            if ref > deleteRef:
                self.correctSet[j] -= 1

    # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    # GENETIC ALGORITHM
    # --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    def runGA(self, exploreIter, state, phenotype):