import recipy
import db
import fi
from random import choice
from random import randint
from os import path
from os import remove
from os import listdir
from operator import itemgetter
from itertools import repeat


# The handler function acts as a container for the rest of the genetic
# algorithm, maintaing the primary life loop. It also deals with a success case
# and the appropriate logging actions.

def gaHandler():
    gaConfig()
    dbData = gaSetup()
    generation = gaPopulate(dbData, [])
    genNo = 1
    while True:
        scores = fi.fiFitness(generation)
        solution = gaEvaluate(scores)
        if solution is not False:
            print("An acceptable solution has been found!")
            print(solution)
            break
        print("Running")
        gaLog(generation, genNo)
        generation = gaPopulate(dbData, gaVariation(scores))
        genNo = genNo + 1

# gaLog is a function designed to dump the entire current population to disk,
# in order to allow a post-execution analysis of the way the population
# changes. There is a need for this as the way the population changes is highly
# variable between executions, as such a large amount of randomness is used.


def gaLog(data, genNo):
    logDir = path.join(path.dirname(__file__), "logs/")
    with open(logDir + str(genNo) + ".log", "w") as f:
        f.write(str(data))

# gaConfig is used to read and supply the ga configuration to any other
# function here. It looks for a line not beginning with # and has an
# '=', and then takes the two strings either side of the '=', the key
# value pair for that particular setting.


def gaConfig():
    global config
    config = {}
    inipath = path.join(path.dirname(__file__), "config/ga.ini")
    with open(inipath, "r") as inifile:
        for line in inifile:
            split = line.find("=")
            if split > 0 and line[0] != "#":
                config[line[:split-1]] = line[split+2:-1]

# gaSetup is a one-time function that is run at the start of a run, and
# grabs the data from the db module, which is then kept in memory until
# the end of the program. It also removes the log files from the previous run
# before this run generates it's own files.


def gaSetup():
    for log in listdir(path.join(path.dirname(__file__), "logs/")):
        remove(path.join(path.dirname(__file__), "logs/") + log)
    dbData = db.dbRead()
    return dbData

# This function takes two arguments: the data to generate a new organism
# from, which is in turn passed to the gaNewOrganism function, and the current
# generation of organisms, which shouldn't be the full size of the population,
# so this function will 'top-up' the population until it reaches the limit
# defined in the configuration file.


def gaPopulate(dbData, currentGen):
    for i in range(len(currentGen), int(config["population"]) + 1):
        currentGen.append(gaNewOrganism(dbData))
    return currentGen

# The gaNewOrganism functions takes one argument, dbData, which contains the
# data loaded from the database, in a pythonic form, and uses this to create a
# singular new organism which is returned to be appended onto the current
# generation.


def gaNewOrganism(dbData):
    new = [[] for i in repeat(None, int(config["periods"]))]
    for i in range(int(config["periods"])):
        for staff in dbData["Staff"]:
            validS = (dbData["Staff_Subject"][int(staff[0]) - 1][1]).split(",")
            subject = choice(validS)
            validR = (dbData["Subject_Room"][int(subject) - 1][1]).split(",")
            room = choice(validR)
            roomName = dbData["Rooms"][int(room) - 1][1]
            subjectName = dbData["Subjects"][int(subject) - 1][1]
            new[i].append([staff[1], roomName, subjectName])
    return new

# gaEvaluate is a short function to iterate through the scores list and check
# if any phenotype is of an acceptable level of quality to be accepted as a
# valid solution. The fitness module will generate the scores, and the returned
# boolean of this function determines when the main loop will break.


def gaEvaluate(scores):
    for index, scorePair in enumerate(scores):
        score = scores[index][1]
        if score >= int(config["threshold"]):
            if str(score - int(score))[2:] != "1":
                return scores[index][0]
        else:
            return False

# gaVariation is the function responsible for determining the changes to the
# population that occur between generations. There is a mix of mutation and
# breeding, as well as some porportion of the population which 'bypasses' this
# stage in order to prevent the solution quality from ever dropping.


def gaVariation(scores):
    bypassNumber = (int(config["population"])/64)*1
    killNumber = (int(config["population"])/64)*16
    breedNumber = (int(config["population"])/64)*44
    mutateNumber = (int(config["population"])/64)*3
    dictScores = {}
    dictOrganisms = {}
    for index, scorePair in scores.items():
        dictScores[index] = scorePair[1]
        dictOrganisms[index] = scorePair[0]
    print(max(dictScores.values()))
    dictScores = dict(sorted(dictScores.items(), key=itemgetter(1),
                             reverse=True)[killNumber:])
    bypass = dict(sorted(dictScores.items(),
                         key=itemgetter(1))[:bypassNumber])
    dictScores = dict(sorted(dictScores.items(),
                             key=itemgetter(1))[bypassNumber:])
    mutated = dict(sorted(dictScores.items(),
                          key=itemgetter(1))[:mutateNumber])
    dictScores = dict(sorted(dictScores.items(),
                             key=itemgetter(1))[:mutateNumber])
    orgMutate = {}
    orgBreed = {}
    orgBypass = {}
    for k in scores:
        if k in bypass:
            orgBypass[k] = dictOrganisms[k]
        elif k in mutated:
            orgMutate[k] = dictOrganisms[k]
        elif k in dictScores:
            orgBreed[k] = dictOrganism[k]
    newGen = orgBypass.values() + gaMutate(orgMutate.values())
    newGen = newGen + gaBreed(orgBreed.values())
    return newGen

# gaMutate is a function that takes one argument, an organism that will
# be given a random mutation in order to attempt to randomly improve it's
# effectiveness. It then returns the mutated organism.


def gaMutate(currentGen):
    print(len(currentGen))
    for indexNo, organism in enumerate(currentGen):
        if 0 == randint(0, int(config["mutation_rate"]) - 1):
            for index, period in enumerate(organism):
                if indexNo & 1 == 0:
                    randomNum = randint(1, len(period) - 1)
                    mutateBuffer = period[randomNum]
                else:
                    organism[index - 1][randomNum] = period[randomNum]
                    period[randomNum] = mutateBuffer
        currentGen[indexNo - 1] = organism
    return currentGen

# gaBreed is a function that will breed together random pairs from the set of
# the population selected for breeding. It will return these new organisms in
# one list and return that, but only half as many will be returned. In the
# event an odd number of organisms are passed to the function, one will simply
# be returned without changing anything.


def gaBreed(currentGen):
    newGen = []
    for organism in range(len(currentGen)):
        if organism + 1 & 1 == 0:
            pairB = currentGen[i]
            newOrganism = []
            for i in range(int(config["periods"])):
                if i + 1 & 1 == 0:
                    newOrganism.append(pairA[i + 1])
                else:
                    newOrganism.append(pairB[i + 1])
        else:
            pairA = currentGen[i + 1]
        newGen.append(newOrganism)
    return newGen