import numpy as np
'''
created by:Ashish Kumar
created on:15/09/20


User can solve any lpp question that can be solved by the simplex, it shows the simplex table and 
user itself as to find the relevent solution or details from the table.

@:exception
Solution in this package having error of approx +-0.010 value in the solution, so take round the final answer
with in this range.

If you find any problem in this package feel free to add issue here:- https://github.com/Ashish2000L/linear_programing
'''
class TPsimplex:
    '''
    @:param
    No_of_constrains:int->  This is the number of equations in the problem
    Number_of_costCoeff_in_equation:int->   This is the number of variables in total, i.e number of variable in the optimized function
    Optimize_type:str-> This is the type of solution user want i.e 'min'(Minimization) or 'max'(Maximation)
    equation:list->  This is the quation in the format [coeffs of cost cofficients,Inequality(string),rhs of the string], must be a multidimensional string.
                If the coefficient of any cost coefficient is 0 in any equation, mention it in the same order
    Optimize_equation:list-> This is you optimize equation

    @Exit_codes
    500 -> Looks like he is not getting any leaving variable
    '''

    def __init__(self,No_of_constrains:int,Number_of_costCoeff_in_equation:int,Optimize_type:str,equation:list,optimize_equation:list,write:bool=False,file_name:str=None):
        self.number_of_constrains=No_of_constrains
        self.equation=equation
        self.optimize_fn=optimize_equation
        self.number_of_variables=Number_of_costCoeff_in_equation
        self.coeff_of_var=[]
        self.additional_var=[]
        self.coeff_of_rhs=[]
        self.basic_var=[]
        self.variable=[]
        self.optimize_final=[]
        self.main_matrix=[]
        self.artificial_var=[]
        self.entering_indx=0
        self.leaving_indx=0
        self.itr=0
        self.optimize_type=Optimize_type
        self.do_write=write
        self.file_name=file_name
        self.error=False
        self.__check_equation()
        #return self.Optimal_Solution()


    def __check_equation(self):

        if self.number_of_constrains!=len(self.equation):
            raise ValueError("Number of Constrains does not match with given number of equations ")
        elif self.optimize_fn is None:
            raise AssertionError("Optimize_equation cannot be null")
        elif len(self.optimize_fn)!=self.number_of_variables:
            raise ValueError('Length of coefficient in optimize function must be equal to number of variables\n'
                             'i.e if any coefficient having 0 coefficient, mention it as 0 instead of leaving that '
                             'blank in proper order')
        elif str(type(self.optimize_type))!="<class 'str'>":
            raise TypeError("Optimize_type must me string, given {0}".format(type(self.optimize_type)))
        elif (self.optimize_type!='max') and (self.optimize_type!='min'):
            raise ValueError("Optimize_type must be either 'max'(Maximation) or 'min'(Minimization) type only")

        for i in self.equation:
            if str(type(i))!="<class 'list'>":
                raise TypeError('List required, found {0}'.format(str(type(i))))
            elif len(i)==0:
                raise AssertionError('Coefficient of equation cannot be null')
            elif len(i)!=self.number_of_variables+2:
                raise ValueError('Inappropriate Equation found, please provide the equation in the given format \n'
                                 'i.e [Coefficient_of_variables,"Inequality",RHS_Coefficient_of_equation]')
            elif str(type(i[self.number_of_variables]))!="<class 'str'>":
                raise ValueError('Inequality must be string in given equation found {0}'.format(str(type(i[self.number_of_variables+1]))))
            elif i[self.number_of_variables] not in ['<=','>=','=']:
                raise ValueError("Only '<=','>=','=' type of inequality is allowed found {0}".format(i[self.number_of_variables]))

        if self.do_write and self.file_name==None:
            raise ValueError("File name is required if the the write is set to true")
        self.__make_new_variables()


    def __make_new_variables(self):

        coeff_variable=[]
        additional_varialbe=[]
        rhs_of_equation=[0]
        basic_variable=['a ']
        opti_F=[0 for i in range(self.number_of_variables)]

        variabls=['X'+str(i+1) for i in range(self.number_of_variables)]

        k=0
        for i in range(self.number_of_constrains):
            additional_varialbe.append([])
            for j in range(self.number_of_constrains):
                additional_varialbe[k].append(0)
            k+=1

        k=0
        p=0
        for i in self.equation:
            coeff_variable.append([])
            var = 1

            for j in i:

                if j=='<=':
                    additional_varialbe[k].pop(k-1)
                    additional_varialbe[k].insert(k, 1)
                    variabls.append('S'+str(k+1))
                    basic_variable.append('S' + str(k + 1))
                    opti_F.append(0)
                elif j=='>=':
                    for l in range(p):
                        coeff_variable[k].append(0)
                    coeff_variable[k].append(-1)
                    opti_F.insert(k,0)
                    opti_F.append(-1)
                    additional_varialbe[k].pop(k)
                    additional_varialbe[k].insert(k, 1)
                    variabls.insert(self.number_of_variables,'S'+str(k+1))
                    variabls.append('A'+str(k+1))
                    basic_variable.append('A' + str(k + 1))
                    self.artificial_var.append('A' + str(k + 1))

                    p+=1

                elif j=='=':
                    additional_varialbe[k].pop(k - 1)
                    additional_varialbe[k].insert(k, 1)
                    variabls.append('A' + str(k+1))
                    basic_variable.append('A' + str(k + 1))
                    self.artificial_var.append('A' + str(k + 1))
                    opti_F.append(-1)

                if var==self.number_of_variables+2:
                    rhs_of_equation.append(j)

                if var<=self.number_of_variables:
                    coeff_variable[k].append(j)

                var += 1
            k+=1


        for i in coeff_variable:
            if len(i)!=self.number_of_variables:
                coeff_variable = self.__make_proper_matrix(coeff_variable)


        variabls.append('Sol')
        variabls.insert(0, 'BV')
        self.variable = variabls
        self.basic_var = basic_variable
        self.coeff_of_var=coeff_variable
        self.additional_var=additional_varialbe
        self.coeff_of_rhs=rhs_of_equation
        self.optimize_final=opti_F

        print("\n\nSTARTING SIMPLEX TABLE\n")
        self.__print_start_matrix()
        self.__make_initial_table()


    def __print_start_matrix(self):

        whole_matrix = [['a ']]

        main_whole_matrix=[]

        for i in self.optimize_final:
            whole_matrix[0].append(i)
        main_whole_matrix.append(self.optimize_final)

        p = 1
        for i in self.coeff_of_var:
            whole_matrix.append([])
            main_whole_matrix.append([])
            whole_matrix[p].append(self.basic_var[p])
            for j in i:
                whole_matrix[p].append(j)
                main_whole_matrix[p].append(j)
            p += 1

        p = 1
        for i in self.additional_var:

            for j in i:
                whole_matrix[p].append(j)
                main_whole_matrix[p].append(j)
            p += 1

        p = 0
        for i in self.coeff_of_rhs:
            whole_matrix[p].append(i)
            main_whole_matrix[p].append(i)
            p += 1

            self.main_matrix=main_whole_matrix

        for i in self.variable:
            print(i, end=' ')
        print('\r')
        for i in whole_matrix:
            for j in i:
                print(j, end='  ')
            print('\r')

        if self.do_write:
            file=open(self.file_name,'w')
            file.write('STARTING SIMPLEX TABLE\n\n')
            for i in self.variable:
                file.write(str(i))
                file.write('\t')

            file.write('\n')

            for i in whole_matrix:
                for j in i:

                    file.write(str(j))
                    file.write('\t')

                file.write('\n')

            file.close()




    def __make_proper_matrix(self,coeff_var)->list:
        max=0
        for i in coeff_var:
            if len(i)>max:
                max=len(i)

        p=0
        for i in coeff_var:
            var=len(i)
            for j in range(max-var):
                coeff_var[p].append(0)
            p+=1

        return coeff_var


    def __make_initial_table(self):
        negatives=[]
        for index,value in enumerate(self.optimize_final):

            if value<0:
                negatives.append(index)

        vals = len(self.coeff_of_var[0])-1
        for i,v in enumerate(self.main_matrix):
            if i+vals in negatives:
                for j,k in enumerate(v):
                    self.main_matrix[0][j]=self.main_matrix[0][j]+k

        print('\n\nINITIAL SIMPLEX TABLE\n')
        self.__print_matrix(msg='INITIAL SIMPLEX TABLE')
        self.__process_simplex_phase_1()


    def __print_matrix(self,msg):
        for i in self.variable:
            print(i,end='\t  ')
        print()
        p=0
        for i in self.main_matrix:
            print(self.basic_var[p],end='\t')
            for j in i:
                if j>=0:
                    print(" "+str(j),end='\t')
                elif j<0:
                    print(j,end='\t')
            p+=1
            print()

        if self.do_write:
            file=open(self.file_name,'a')
            file.write('\n\n'+msg+'\n')
            for i in self.variable:
                file.write(str(i))
                file.write('\t\t  ')
            file.write('\n')
            p = 0
            for i in self.main_matrix:
                file.write(self.basic_var[p])
                file.write('\t\t')
                for j in i:
                    if j >= 0:
                        file.write(" "+str(j))
                        file.write('\t\t')
                    elif j < 0:
                        file.write(str(j))
                        file.write('\t\t')
                p += 1
                file.write('\n')

            file.close()



    def __process_simplex_phase_1(self):
        print('\n\nPHASE-1\n')
        if self.do_write:
            file=open(self.file_name,'a')
            file.write('\n\nPHASE 1\n')
            file.close()
        while not self.__check_for_optimal_table():
            self.__find_entry_indx()
            self.__find_leaving_indx()
            if self.__set_enter_and_leave_val()==False:
                return
        print('\n\nPHASE-2\n')
        self.__process_simplex_phase_2()


    def __check_for_optimal_table(self,type='1_phase')->bool:
        if type=='1_phase':
            f=[self.main_matrix[0][i]  for i in range(len(self.main_matrix[0])-1)]

            for i in f:
                if i>0:
                    return False
            return True
        elif type=='min':
            f = [self.main_matrix[0][i] for i in range(len(self.main_matrix[0]) - 1)]

            for i in f:
                if i >0:
                    return False
            return True
        elif type=='max':
            f = [self.main_matrix[0][i] for i in range(len(self.main_matrix[0]) - 1)]

            for i in f:
                if i<0:
                    return False
            return True


    def __find_entry_indx(self,type='1_phase'):
        matrix=self.optimize_final

        f=[]
        self.entering_indx=None
        if type=='1_phase':
            max = 0
            for i,v in enumerate(matrix):
                if i<len(self.coeff_of_var[0]):
                    f.append(v)

            for i,v in enumerate(f):
                if v >max :
                    max=v
                    self.entering_indx=i
        elif type=='max':
            min=0
            for i, v in enumerate(self.main_matrix[0]):
                if i < len(self.main_matrix[0]) - 1:
                    f.append(v)

            for i, v in enumerate(f):
                if v < min:
                    min = v
                    self.entering_indx = i

        elif type=='min':
            max=0
            for i, v in enumerate(self.main_matrix[0]):
                if i < len(self.main_matrix[0]) - 1:
                    f.append(v)

            for i, v in enumerate(f):
                if v > max:
                    max = v
                    self.entering_indx = i


    def __check_for_other_alternative(self,min:int):

        ln=len(self.main_matrix[0])-1
        for i,v in enumerate(self.basic_var):
            b = self.main_matrix[i][ln]
            x = self.main_matrix[i][self.entering_indx]
            if v in self.artificial_var:
                if (float(b/x)==min):
                    self.leaving_indx=i


    def __find_leaving_indx(self, type='1_phase'):

        x=[]
        b=[]
        min = 10000
        self.leaving_indx=None
        if type=='1_phase':
            min = 10000
            l=len(self.optimize_final)-1
            for k in self.main_matrix:
                x.append(k[self.entering_indx])
                b.append(k[l])

            x.pop(0)
            b.pop(0)
            for i in range(len(x)):
                if x[i]>0 :
                    m=float(b[i]/x[i])
                    if m<min:
                        min=m
                        self.leaving_indx=i+1

            self.__check_for_other_alternative(min)

        elif type=='max':

            l = len(self.main_matrix[0])-1
            for k in self.main_matrix:
                x.append(k[self.entering_indx])
                b.append(k[l])

            x.pop(0)
            b.pop(0)

            for i in range(len(x)):
                if x[i] > 0:
                    m = float(b[i] / x[i])
                    if m < min:
                        min = m
                        self.leaving_indx = i+1

        elif type=='min':

            l = len(self.main_matrix[0]) - 1
            for k in self.main_matrix:
                x.append(k[self.entering_indx])
                b.append(k[l])

            x.pop(0)
            b.pop(0)

            for i in range(len(x)):
                if x[i] > 0:
                    m = float(b[i] / x[i])
                    if m < min:
                        min = m
                        self.leaving_indx = i + 1


    def __set_enter_and_leave_val(self):

        temp_index=0
        temp_list1=[]
        temp_list2=[]
        if(self.leaving_indx==None):
            self.error=True
            #print("\n\nLooks like we are not getting any leaving varible, please traceback to see the problem. If you find this solution wrong please raise an issue at:\nhttps://github.com/Ashish2000L/linear_programing")
            #self.Optimal_Solution()
            return False


        enter=self.variable[self.entering_indx+1]
        leave=self.basic_var[self.leaving_indx]
        self.basic_var.pop(self.leaving_indx)
        self.basic_var.insert(self.leaving_indx,enter)
        for i,v in enumerate(self.variable):
            if v==leave:
                temp_index=i-1
        for i in self.main_matrix:
            temp_list1.append(i[temp_index])
            temp_list2.append(i[self.entering_indx])
        for i in range(len(self.main_matrix)):
            self.main_matrix[i][self.entering_indx]=temp_list1[i]
        k=temp_list2[self.leaving_indx]
        for i,v in enumerate(self.main_matrix[self.leaving_indx]):
            if i!=self.entering_indx:
                self.main_matrix[self.leaving_indx][i]=round(float(v/k),3)
        temp_leav_val_list=self.main_matrix[self.leaving_indx]
        for i,v in enumerate(self.main_matrix):
            if i!=self.leaving_indx:
                m=temp_list2[i]*-1
                for k,j in enumerate(v):
                    if k !=self.entering_indx:
                        self.main_matrix[i][k]=float(format(j+(m*temp_leav_val_list[k]),'.5f'))
        self.itr+=1
        print('\n\nITERATION {0}\n'.format(self.itr))
        self.__print_matrix(msg='ITERATION '+str(self.itr))

        #self.Optimal_Solution()


    def __process_simplex_phase_2(self):
        pop_indx=[]
        F=[]
        length=len(self.variable)-1
        while length>=0:
            for j in self.artificial_var:
                if j==self.variable[length]:
                    pop_indx.append(length-1)
                    self.variable.pop(length)
            length-=1



        for i,v in enumerate(self.main_matrix):
            for j in pop_indx:
                v.pop(j)
            self.main_matrix[i]=v

        l=len(self.main_matrix[0])

        for i in self.optimize_fn:
            F.append(float(i*-1))

        for i in range(l-len(F)):
            F.append(float(0))

        self.main_matrix.pop(0)
        self.basic_var.pop(0)
        self.basic_var.insert(0,'F ')
        self.main_matrix.insert(0,F)


        self.__print_matrix(msg='PHASE-2')

        temp_indx=[]
        z=[]
        for i in self.basic_var:
            for j,v in enumerate(self.variable):
                if i==v:
                    temp_indx.append((j-1,v))
                    z.append(j-1)

        k=[]

        for i,v in enumerate(self.main_matrix[0]):
            if i in z:
                if v !=0:
                    for j in temp_indx:
                        if j[0]==i:
                            k.append(j)

        temp_indx.clear()
        for i in k:
            for j,v in enumerate(self.basic_var):
                if i[1]==v:
                    temp_indx.append((i[0],j))

        F=self.main_matrix[0]
        for i in temp_indx:
            for j,v in enumerate(self.main_matrix):
                if j==i[1]:
                    k=F[i[0]]*-1
                    for x,y in enumerate(v):
                        F[x]=round(F[x]+(y*k),3)

        self.main_matrix[0]=F

        print('\n\nINITIAL SIMPLEX TABLE\n')
        self.__print_matrix(msg='INITIAL SIMPLEX TABLE')
        self.__process_simplex_final_phase()


    def __process_simplex_final_phase(self):
        while not self.__check_for_optimal_table(type=self.optimize_type):
            self.__find_entry_indx(type=self.optimize_type)
            self.__find_leaving_indx(type=self.optimize_type)
            if self.__set_enter_and_leave_val()==False:
                return


    def Optimal_Solution(self):

        if self.error==False:
            ln=len(self.main_matrix[0])-1
            if self.main_matrix[0][ln]>0:
                print("\n\n\nSOLUTION\n")
                self.__print_matrix(msg='SOLUTION')
                final_solution=[['F',self.main_matrix[0][ln]]]

                cost_coeff_var=['X'+str(i+1) for i in range(self.number_of_variables)]
                for i,v in enumerate(self.basic_var):

                    if v in cost_coeff_var:
                        final_solution.append([v,self.main_matrix[i][ln]])

                return np.asarray(final_solution)
            else:
                return "Either No Fisible solution or an error occured!"
                #return "\nEither the equation have not fisible solution or an error occured.\n Please staceback to look for the issue, if you find this solution wrong you can create an issue here: \n https://github.com/Ashish2000L/linear_programing"
        else:
            return "Looks like Either No optimal Solution Exist or there is an error, Please check manually!"
