#! python2

from __future__ import print_function
from itertools import *
from decimal import Decimal
from copy import copy
import math
import sys

def exportResults(filePath, results):
    with open(filePath, 'w') as f:
        for line in results:
            f.write(line+'\n')
def importData(filePath):
    f = open(filePath, "r")
    headers = []
    data = []
    caseNumber = 1
    for line in f:
        if "<" in line and ">" in line:
            continue
        if "[" in line and "]" in line:
            headers = ["caseNumbers"]+line.translate(None, '[]').strip().split()
            continue
        if line.isspace():
            continue
        data.append([str(caseNumber)]+line.rstrip('\n').split())
        caseNumber = caseNumber+1
    return [headers, data]

def Symbolicise(data):
    symbols = {}
    for column in range(1,len(data[0])-1):
        try:
            x = float(data[0][column])
        except:
                continue
        distinctSet = sorted(set([float(data[x][column]) for x in range(len(data))]))
        symbols[str(distinctSet[0])] = str(distinctSet[0]) +".."+str((distinctSet[0]+distinctSet[1])/2)
        for itemIndx in range(1,len(distinctSet)-1):
            symbols[str(distinctSet[itemIndx])] = str((distinctSet[itemIndx]+distinctSet[itemIndx-1])/2) +".."+str((distinctSet[itemIndx]+distinctSet[itemIndx+1])/2)
        symbols[str(distinctSet[len(distinctSet)-1])] = str((distinctSet[len(distinctSet)-1]+distinctSet[len(distinctSet)-2])/2) +".."+str((distinctSet[len(distinctSet)-1]))                
        for case in data:
            case[column] = symbols[str(float(case[column]))]

# Computing Partitions
# Q is the set of all attributes(also called labels) and descisions (set of all cases?)
# U is the set of all cases (entities) (Description is more like set of all row numbers treated like pointers to the rows)
# P is an arbitrary nonempty subset of Q ( of U?)
# indiscernibility relation of P is an equivalence relation on U...it induces a partition of U generated by P and is called P* of U
# if U is known then you jsut call it partition P*.
# partition P* is the set of all equivalence classes. P is a function that returns the arbitrary subset so P is a palceholder for {a}* {b}* {a,b}* etc
# since it generates all of those it's known as P* with the expected {} contained inside the P otherwilse {P*} could produce {{a}*}
#
def getDStar(data):
    distinctSet = []
    for case in data:
        distinctSet.append( case[len(case)-1])
    distinctSet = list(set(distinctSet))
    DStar = [[] for x in range(len(distinctSet))]
    for case in data:
        DStar[distinctSet.index(case[len(case)-1])].append(case[0])
    return DStar 

def generateAttributeCombos(attributes):
    combos = []
    for x in range(0,len(attributes)):
        combos = combos + list(combinations(attributes, x+1))
    return combos

def getTheStarSet(partition, labels, data):
    #ex: ('A','B'), ['caseNumbers',A','B','C','D'], [[1,1,2,3,4],..,[n,1,1,3,3]]
    columns = []
    scopedSet= []
    distinctSet = []
    #get label indecies of attributes in the partition
    for att in partition:
        columns.append(labels.index(att))
    #Reduce every case to it's entity + attribute columns + decision
    #BUGFIX: remove decision from the scoped set
    for case in data:
        scopedSet.append(([case[0]]+[case[x] for x in columns]))
        # old version: scopedSet.append(([case[0]]+[case[x] for x in columns]+[case[len(case)-1]]))
    [distinctSet.append(i[1:len(i)]) for i in scopedSet if not distinctSet.count(i[1:len(i)])]
    starSet = [[] for x in range(len(distinctSet))]
    for case in scopedSet:
        starSet[distinctSet.index(case[1:len(i)])].append(case[0])
    return [partition,starSet ]

def generatePartitionsForSet(dataIn):
    data = dataIn[1]
    labels = dataIn[0]
    attributeGroups = generateAttributeCombos(labels[1:len(labels)-1])
    starSets = []
    for partition in attributeGroups:
        starSet = getTheStarSet(partition,labels, data)
        starSets.append(starSet)
    return starSets

def NotDStarIsLessThanOrEqualToDStar(NotDStar, DStar):
    # set inequalities
    # so {{1},{2},{3},{4},{5},{6},{7}} <= {{1,2,4,5},{3,6,7}} is true
    # but {{1},{2},{3,6},{4},{5,7}} <= {{1,2,4,5},{3,6,7}} is not
    # This is because there is no way to group the subsets of 
    # {{1},{2},{3,6},{4},{5,7}} to create the subsets of  {{1,2,4,5},{3,6,7}}
    setOfConcerns = [x for x in NotDStar if len(x) > 1]
    if len(setOfConcerns) is 0:
        return True #NotDStar Is Less Than Or Equal To DStar
    for subset in setOfConcerns:
        fail = True
        for grouping in DStar:
            if set(subset).issubset(set(grouping)):
                fail = False
                break
        if fail:
            return False
    return True

def compareRuleToCase(rule, case, labels):
    columns = [labels.index(rule[x][0]) for x in range(len(rule)-1)]
    for x in range(len(columns)):
        if rule[x][1] != case[columns[x]]:
            return False
    return True

def isResultSetValid(resultSet,validResult):
    for d in resultSet:
        if(d[1]) != validResult[-1][1]:
            return False
    return True

def FormatRule(rule):
    formattedRule = []
    for condition in rule[:len(rule)-1]:
        formattedRule.append("({},{})".format(condition[0],condition[1]))
    formattedRule = " & ".join(formattedRule)
    formattedRule += " -> ({},{})".format(rule[-1][0],rule[-1][1])
    return formattedRule


def InduceRulesFromGlobalCovering(gc, labels, data):
    scopedSet = []
    columns = [labels.index(gc[x]) for x in range(len(gc))]
    columns.append(len(labels)-1)
    #generate a rule from the global covering and the nth case
    inducedRules = set()
    for case in data:
        baseRule = [[labels[columns[x]],case[columns[x]]] for x in range(len(columns))]
        # test the rule to remove attributes
            # find cases the rule applies to
        foundCases = [[data[x][0],data[x][-1]] for x in range(len(data)) if compareRuleToCase(baseRule,data[x],labels)]
        tempRule = copy(baseRule)
        for x in baseRule[:len(baseRule)-1]:
            # remove attribute
            Q = copy(tempRule)
            Q.remove(x)
            # find cases the new rule applies to
            newCases = [[data[x][0],data[x][-1]] for x in range(len(data)) if compareRuleToCase(Q,data[x],labels)]
            # check if rule is not certain
            if isResultSetValid(newCases,Q):
                tempRule = copy(Q)
            # set new rule if it checks out
            # attempt to remove another attribute
        inducedRules.add(FormatRule(tempRule))
    return inducedRules

    
# U is the data table. 
# Each row is called a case (also an entity). 
# The last column(attribute/parameter) of a row is typically the descision(outcome/result/choice). 
# The subset of all rows(cases) that have the same decision are called a concept.
# Equivalence classes are also called blocks
# {d}* is a set of subsets grouped by descision ie. {{yes, yes, yes}{no, no},{maybe}} but by their case row numbers instead like this {{row1, row3, row4}{row2, row6},{row5}}
#
# A note on Discernible... This is not distinct. In the case of 3 attributes and one descision(ex:{a,b,c,d}) {1,1,2,4} this is indiscernible from {1,1,3,4} for the {a,b} partition
# Note: The above example needs a lot of pretext.

# A is all attributes (['A','B','C',..])
# B is a nonempty subset of A (['B','C'])
# U is all cases (pretty much the dataset + potentially all future inputs)
# IND(B) is an indiscernibility relationship where, for (the arbitrary cases ) x and y of set U, 
# (x,y) are of IND(B) if an only if the values for all attributes from B (the nonempty subset of sattributes) are identical
# This just means that in the context of only the attributes provided by B. You can't tell x and y apart if all attributes in context are the same. They, in that context, are effectively identical.
# StarSets(my word I added) are IND(Set) are the family of all elementary sets which is just the set of discernable subsets of attributes...

# in the algo
# "compute partition A*" is one of the starSets [('C',), [['1'], ['2'], ['3', '4', '5'], ['6'], ['7']]] or [('A', 'B'), [['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7']]] or etc
# so the idea is you pass in a partition then calculate the partitionStar and that is your A* for this loop.
# P=A means set P to the set of all sttributes. The := symbol means refernece/redefine as but we'll jsut make a copy of A
# R is set to null but will eventually be the remaining meaningful attributes for that partition. How to output that properly is not a consern at this stage. Just return it for now.
# if the A* set is <= d* as detailed above then enter the loop
## inside the for each
## for each attribute in A (list of all attributes)
## Set placeholder Q to equal P - A (one by one subtracting attributes from the set)
## get Q* and check if it's also less than d*
## if it is, then P = the reduced attribute set Q
##loop
# after this set R to whatever P is and return that.


# The input (Set of all attributes A, partiton {d}* on U [This is just a set of subsets of entities from U (the set of everything) grouped by descision])
def lem1Staging(U, symbolic = False):
    if not symbolic:
        Symbolicise(U[1])
    dStar = getTheStarSet(U[0][len(U[0])-1:],U[0],U[1])
    A = U[0][1:len(U[0])-1]
    singleGlobalCovering = lem1(U,A,dStar[1])
    return singleGlobalCovering

def lem1(U,A,dStar):
    AStar = getTheStarSet(A,U[0], U[1])
    P = copy(A) #alternate
    R = None
    # print("Is ",AStar, " less than ",dStar,sep='\n')
    if NotDStarIsLessThanOrEqualToDStar(AStar[1], dStar):#not the worst comparison function
        # print("Yes it is")
        # print("ASetPair",ASetPair[0])
        for a in A: #alternate
        # for a in ASetPair[0]:
            # print("P",P)
            # print("a",a)
            Q = copy(P)#.remove(a) # remove the value a from P     P-a
            # print("P - a",P)
            Q.remove(a)
            # print("P",P)
            if len(Q) < 1:
                break
            # print("Q",Q)
            # print("U[0]",U[0])
            # print("U[1]",U[1])
            QStar = getTheStarSet(Q,U[0], U[1])#will probably become a generate all discernable partitions and for each over them
            # print("Is ",QStar[1], " less than ",DStar,sep='\n')
            if NotDStarIsLessThanOrEqualToDStar(QStar[1], dStar):#not the worst comparison function
                # print("Yes it is")
                P = copy(Q)
        R = copy(P)
        # print(R)
    return R

def main():
    path = r"C:\Users\DrMur\DataMining\Programming Project\\"
    # 
    inputFileNames = ["flu.txt","test.txt","german.txt","austr.txt","wine.txt"]
    #outputFileName = "results_"+inputFileName
    for fileName in inputFileNames:
        print("Beginning: ",fileName)
        data = importData(path+fileName)
        results = lem1Staging(data, symbolic=False)
        # print(results)
        rules = InduceRulesFromGlobalCovering(results,data[0],data[1])
        exportResults(path+"results_"+fileName,rules)


if __name__ == "__main__":
    main()




