#!/usr/bin/env python3
import numpy as np
import csv
import itertools
import math

distancemeasure="max" #mse, max


column_label=0
column_capacity=3
column_resistance=4


config_parallel=2
config_cells=12

#config_parallel*config_cells*config_packs needed
batteriesneeded=config_parallel*config_cells


batteries=[]
batterylabels=[]

with open('12S_LiIon_Akkupack_selected.csv', 'r') as csvfile:
    csvreader = csv.reader(csvfile, delimiter=';')
    firstrow=True
    for row in csvreader:
        label=row[column_label]
        capacity=row[column_capacity]
        resistance=row[column_resistance]
        if not firstrow:
            capacity=float(capacity)
            resistance=float(resistance)

            batteries.append([label,capacity,resistance])
            #batterylabels.append(label)
        firstrow=False

print(str(len(batteries))+" Batteries found")
if len(batteries)==batteriesneeded:
    print("You have enough batteries for "+str(config_cells)+"S"+str(config_parallel)+"P")
elif len(batteries)>batteriesneeded:
    print("You have "+str(len(batteries)-batteriesneeded)+" batteries in spare")
elif len(batteries)<batteriesneeded:
    print("You need "+str(batteriesneeded-len(batteries))+" more batteries!")
    exit()


#calculate mean capacity of all cells
meancapacity=0
for i in batteries:
    meancapacity+=i[1] #add capacities
meancapacity/=len(batteries)
print("Mean Capacity= "+str(meancapacity))

comb_parallel_all=[]
for comb_parallel in itertools.combinations(batteries,config_parallel):
    comb_parallel_all.append(comb_parallel)

comb_parallel_all_meancapacities=[]
for current_parallel in comb_parallel_all:
    #print(current_parallel)
    meancapacity_parallel=0
    for current_cell_from_parallel in current_parallel:
        meancapacity_parallel+=current_cell_from_parallel[1] #add capacities

    meancapacity_parallel/=len(current_parallel)
    #print("Mean Capacity Parallel set="+str(meancapacity_parallel))
    comb_parallel_all_meancapacities.append( meancapacity_parallel )

deviationFromMean = [abs(x - meancapacity) for x in comb_parallel_all_meancapacities]
assert len(comb_parallel_all)==len(deviationFromMean), "Menacapacity array lenght not matching"

sortedIndices=np.argsort(deviationFromMean)


bestSelection=[]
bestSelectionDistance=10000000000

def getDistanceFromSelection(selection):
    if distancemeasure=="mse":
        return getDistanceFromSelectionMSE(selection)
    elif distancemeasure=="max":
        return getDistanceFromSelectionMAX(selection)

def getDistanceFromSelectionMSE(selection): #Mean Squared Error
    global deviationFromMean
    distance=0
    for i in selection:
        distance+= math.pow(deviationFromMean[i],2)
    distance/=len(selection)
    return distance

def getDistanceFromSelectionMAX(selection): #Mean Squared Error
    global deviationFromMean
    _deviations=[deviationFromMean[x] for x in selection]
    distance=max(_deviations)
    return distance

def greedy(parallelCombinations,remainingSortedIndices,currentSelection):
    global config_cells
    global bestSelection
    global bestSelectionDistance
    global deviationFromMean
    #print("greedy("+str(len(parallelCombinations))+","+str(len(remainingSortedIndices))+","+str(currentSelection)+")")

    if len(remainingSortedIndices)<1 or len(currentSelection)>=config_cells : #nothing left

        currentSelectionDistance=getDistanceFromSelection(currentSelection)
        if currentSelectionDistance < bestSelectionDistance: #new best found
            bestSelectionDistance=currentSelectionDistance
            bestSelection=currentSelection
            print("")
            print("New best found "+str(currentSelection))
            _print_deviationFromMean=[round(deviationFromMean[a],1) for a in currentSelection]
            print("Dev. from mean "+str(_print_deviationFromMean))
            print("Distance="+str(currentSelectionDistance))
            for i in bestSelection:
                print(str(comb_parallel_all[i])+" mc="+str(comb_parallel_all_meancapacities[i]))


        return #backtrack

    if len(currentSelection)>0 and getDistanceFromSelection(currentSelection)>bestSelectionDistance: #distance is already too high
        return #backtrack early

    for i in remainingSortedIndices: #try all in sorted order
        newremainingSortedIndices=[x for x in remainingSortedIndices] #copy list, should be the same for every i

        _selectedIndex=i #greedy take element i


        #remove elements with one of the used batteries
        for used in parallelCombinations[_selectedIndex]:
            #print(used)
            for rsi in newremainingSortedIndices: #check all remaining indices
                containesBattery=False
                for pcb in parallelCombinations[rsi]:
                    if pcb==used:
                        containesBattery=True

                if containesBattery:
                    newremainingSortedIndices=[x for x in newremainingSortedIndices if x is not rsi] #remove current index rsi from remainingSortedIndices

        #repeat until empty
        greedy(parallelCombinations,newremainingSortedIndices,currentSelection+[_selectedIndex])




greedy(comb_parallel_all,sortedIndices, [])
print(bestSelection)

print("###")
for i in bestSelection:
    print(str(comb_parallel_all[i])+" mc="+str(comb_parallel_all_meancapacities[i]))
