import numpy as np
from kawin.KWNBase import PrecipitateBase
from kawin.PopulationBalance import PopulationBalanceModel
from kawin.GrainBoundaries import GBFactors
import copy
import csv
from itertools import zip_longest
import time

class PrecipitateModel (PrecipitateBase):
    '''
    Euler implementation of the KWN model designed for binary systems

    Parameters
    ----------
    t0 : float
        Initial time in seconds
    tf : float
        Final time in seconds
    steps : int
        Number of time steps
    phases : list (optional)
        Precipitate phases (array of str)
        If only one phase is considered, the default is ['beta']
    linearTimeSpacing : bool (optional)
        Whether to have time increment spaced linearly or logarithimically
        Defaults to False
    elements : list (optional)
        Solute elements in system
        Note: order of elements must correspond to order of elements set in Thermodynamics module
        If binary system, then defualt is ['solute']
    '''
    def __init__(self, t0, tf, steps, phases = ['beta'], linearTimeSpacing = False, elements = ['solute']):
        #Initialize base class
        super().__init__(t0, tf, steps, phases, linearTimeSpacing, elements)

        if self.numberOfElements == 1:
            self._growthRate = self._growthRateBinary
            self._Beta = self._BetaBinary1
        else:
            self._growthRate = self._growthRateMulti
            self._Beta = self._BetaMulti

        #Additional outputs
        self.additionalFunctions = []
        self.additionalFunctionNames = []
        self.additionalOutputs = None

    def _resetArrays(self):
        '''
        Resets and initializes arrays for all variables

        In addition to PrecipitateBase, the equilibrium aspect ratio area and population balance models are created here
        '''
        super()._resetArrays()
        self.PBM = [PopulationBalanceModel() for p in self.phases]

        #Index of particle size classes which below, precipitates are unstable
        self.RdrivingForceIndex = np.zeros(len(self.phases), dtype=np.int32)

        #Aspect ratio
        self.eqAspectRatio = [[] for p in self.phases]

    def reset(self):
        '''
        Resets model results
        '''
        super().reset()

        #Bounds of the bins in PSD
        for i in range(len(self.phases)):
            self.PBM[i].reset()

        #Resets PSD outputs
        self._setupAdditionalOutputs()

    def save(self, filename, compressed = False, toCSV = False):
        '''
        Save results into a numpy .npz format

        Parameters
        ----------
        filename : str
        compressed : bool
            If true, will save compressed .npz format
        toCSV : bool
            If true, wil save to .csv
        '''
        variables = ['t0', 'tf', 'steps', 'phases', 'linearTimeSpacing', 'elements', \
            'time', 'xComp', 'Rcrit', 'Gcrit', 'Rad', 'avgR', 'avgAR', 'betaFrac', 'nucRate', 'precipitateDensity', 'dGs', 'xEqAlpha', 'xEqBeta']
        vDict = {v: getattr(self, v) for v in variables}
        if self.additionalOutputs is not None:
            vDict['additionalOutputs'] = self.additionalOutputs
            if not toCSV:
                vDict['additionalFunctionNames'] = self.additionalFunctionNames
        for p in range(len(self.phases)):
            vDict['PSDdata_'+self.phases[p]] = [self.PBM[p].min, self.PBM[p].max, self.PBM[p].bins]
            vDict['PSDsize_' + self.phases[p]] = self.PBM[p].PSDsize
            vDict['PSD_' + self.phases[p]] = self.PBM[p].PSD
            vDict['PSDbounds_' + self.phases[p]] = self.PBM[p].PSDbounds
            vDict['eqAspectRatio_' + self.phases[p]] = self.eqAspectRatio[p]

        if toCSV:
            vDict['t0'] = np.array([vDict['t0']])
            vDict['tf'] = np.array([vDict['tf']])
            vDict['steps'] = np.array([vDict['steps']])
            vDict['linearTimeSpacing'] = np.array([vDict['linearTimeSpacing']])
            if self.numberOfElements == 2:
                vDict['xComp'] = vDict['xComp'].T
            arrays = []
            headers = []
            for v in vDict:
                vDict[v] = np.array(vDict[v])
                if len(vDict[v].shape) == 2:
                    for i in range(len(vDict[v])):
                        arrays.append(vDict[v][i])
                        if v == 'xComp':
                            headers.append(v + '_' + self.elements[i])
                        else:
                            headers.append(v + '_' + self.phases[i])
                elif v == 'xEqAlpha' or v == 'xEqBeta':
                    for i in range(len(self.phases)):
                        for j in range(self.numberOfElements):
                            arrays.append(vDict[v][i,:,j])
                            headers.append(v + '_' + self.phases[i] + '_' + self.elements[j])
                elif v == 'additionalOutputs':
                    for i in range(len(self.phases)):
                        for j in range(len(self.additionalFunctionNames)):
                            arrays.append(vDict[v][i,:,j])
                            headers.append(v + '_' + self.phases[i] + '_' + self.additionalFunctionNames[j])
                else:
                    arrays.append(vDict[v])
                    headers.append(v)
            rows = zip_longest(*arrays, fillvalue='')
            if '.csv' not in filename.lower():
                filename = filename + '.csv'
            with open(filename, 'w', newline='') as f:
                csv.writer(f).writerow(headers)
                csv.writer(f).writerows(rows)
        else:
            if compressed:
                np.savez_compressed(filename, **vDict)
                #np.savez_compressed(filename, **vDict, allow_pickle=True)
            else:
                np.savez(filename, **vDict)
                #np.savez(filename, **vDict, allow_pickle=True)

    def load(filename):
        '''
        Loads data

        Parameters
        ----------
        filename : str

        Returns
        -------
        PrecipitateModel object
            Note: this will only contain model outputs which can be used for plotting
        '''
        setupVars = ['t0', 'tf', 'steps', 'phases', 'linearTimeSpacing', 'elements']
        if '.np' in filename.lower():
            data = np.load(filename, allow_pickle=True)

            #Input arbitrary values for PSD parameters (rMin, rMax, bins) since this will be changed shortly after
            model = PrecipitateModel(data['t0'], data['tf'], data['steps'], data['phases'], data['linearTimeSpacing'], data['elements'])
            for p in range(len(model.phases)):
                PSDvars = ['PSDdata_' + model.phases[p], 'PSD_' + model.phases[p], 'PSDsize_' + model.phases[p], 'eqAspectRatio_' + model.phases[p], 'PSDbounds_' + model.phases[p]]
                #For back compatibility
                if PSDvars[0] not in data:
                    PSDvars = ['PSDdata' + str(p), 'PSD' + str(p), 'PSDsize' + str(p), 'eqAspectRatio' + str(p), 'PSDbounds' + str(p)]
                setupVars = np.concatenate((setupVars, PSDvars))
                model.PBM[p] = PopulationBalanceModel(data[PSDvars[0]][0], data[PSDvars[0]][1], int(data[PSDvars[0]][2]), True)
                model.PBM[p].PSD = data[PSDvars[1]]
                model.PBM[p].PSDsize = data[PSDvars[2]]
                model.eqAspectRatio[p] = data[PSDvars[3]]
                model.PBM[p].PSDbounds = data[PSDvars[4]]
            for d in data:
                if d not in setupVars:
                    setattr(model, d, data[d])
            if 'additionalOutputs' not in data:
                model.additionalOutputs = None
                model.additionalFunctions = []
                model.additionalFunctionNames = []
        elif '.csv' in filename.lower():
            with open(filename, 'r') as csvFile:
                data = csv.reader(csvFile, delimiter=',')
                i = 0
                headers = []
                columns = {}
                #Grab all columns
                for row in data:
                    if i == 0:
                        headers = row
                        columns = {h: [] for h in headers}
                    else:
                        for j in range(len(row)):
                            if row[j] != '':
                                columns[headers[j]].append(row[j])
                    i += 1

                t0, tf, steps, phases, elements = float(columns['t0'][0]), float(columns['tf'][0]), int(columns['steps'][0]), columns['phases'], columns['elements']
                linearTimeSpacing = True if columns['linearTimeSpacing'][0] == 'True' else False
                model = PrecipitateModel(t0, tf, steps, phases, linearTimeSpacing, elements)

                for p in range(len(model.phases)):
                    PSDvars = ['PSDdata_' + model.phases[p], 'PSD_' + model.phases[p], 'PSDsize_' + model.phases[p], 'eqAspectRatio_' + model.phases[p], 'PSDbounds_' + model.phases[p]]
                    #For back compatibility
                    if PSDvars[0] not in columns:
                        PSDvars = ['PSDdata' + str(p), 'PSD' + str(p), 'PSDsize' + str(p), 'eqAspectRatio' + str(p), 'PSDbounds' + str(p)]
                    setupVars = np.concatenate((setupVars, PSDvars))
                    model.PBM[p] = PopulationBalanceModel(float(columns[PSDvars[0]][0]), float(columns[PSDvars[0]][1]), int(float(columns[PSDvars[0]][2])), True)
                    model.PBM[p].PSD = np.array(columns[PSDvars[1]], dtype='float')
                    model.PBM[p].PSDsize = np.array(columns[PSDvars[2]], dtype='float')
                    model.eqAspectRatio[p] = np.array(columns[PSDvars[3]], dtype='float')
                    model.PBM[p].PSDbounds = np.array(columns[PSDvars[4]], dtype='float')

                restOfVariables = ['time', 'xComp', 'Rcrit', 'Gcrit', 'Rad', 'avgR', 'avgAR', 'betaFrac', 'nucRate', 'precipitateDensity', 'dGs', 'xEqAlpha', 'xEqBeta', 'additionalOutputs']
                restOfColumns = {v: [] for v in restOfVariables}
                additionalFunctionNames = []
                for d in columns:
                    if d not in setupVars:
                        if d == 'time':
                            restOfColumns[d] = np.array(columns[d], dtype='float')
                        elif d == 'xComp':
                            if model.numberOfElements == 1:
                                restOfColumns[d] = np.array(columns[d], dtype='float')
                            else:
                                restOfColumns['xComp'].append(columns[d], dtype='float')
                        else:
                            selectedVar = ''
                            for r in restOfVariables:
                                if r in d:
                                    selectedVar = r
                            if selectedVar == 'additionalOutputs':
                                additionalFunctionNames.append(d[18:])
                            restOfColumns[selectedVar].append(np.array(columns[d], dtype='float'))
                for d in restOfColumns:
                    restOfColumns[d] = np.array(restOfColumns[d])
                    setattr(model, d, restOfColumns[d])

                #For multicomponent systems, adjust as necessary such that number of elements will be the last axis
                if model.numberOfElements > 1:
                    model.xComp = model.xComp.T
                    if len(model.phases) == 1:
                        model.xEqAlpha = np.expand_dims(model.xEqAlpha, 0)
                        model.xEqBeta = np.expand_dims(model.xEqBeta, 0)
                    else:
                        model.xEqAlpha = np.reshape(model.xEqAlpha, ((len(model.phases), model.numberOfElements, len(model.time))))
                        model.xEqBeta = np.reshape(model.xEqBeta, ((len(model.phases), model.numberOfElements, len(model.time))))
                    model.xEqAlpha = np.transpose(model.xEqAlpha, (0, 2, 1))
                    model.xEqBeta = np.transpose(model.xEqBeta, (0, 2, 1))

                #If additional outputs exists, then reshape array to (phase, iterations, functions)
                if len(additionalFunctionNames) > 0:
                    numberOfFunctions = int(len(additionalFunctionNames) / len(model.phases))
                    model.additionalOutputs = np.reshape(model.additionalOutputs, (len(model.phases), numberOfFunctions, len(model.time)))
                    model.additionalOutputs = np.transpose(model.additionalOutputs, (0, 2, 1))
                    model.additionalFunctionNames = []
                    for i in range(numberOfFunctions):
                        model.additionalFunctionNames.append(additionalFunctionNames[i][len(model.phases[0])+1:])
                    model.additionalFunctionNames = np.array(model.additionalFunctionNames)

        return model

    def _divideTimestep(self, i, dt):
        '''
        Divides timestep at iteration i
        '''
        super()._divideTimestep(i, dt)

        if len(self.additionalFunctions) > 0:
            self.additionalOutputs = np.append(self.additionalOutputs, np.zeros((len(self.phases), 1, len(self.additionalFunctions))), axis=1)

    def setPBMParameters(self, cMin = 1e-10, cMax = 1e-9, bins = 150, minBins = 100, maxBins = 200, adaptive = True, phase = None):
        '''
        Sets population balance model parameters for each phase

        Parameters
        ----------
        cMin : float
            Minimum bin size
        cMax : float
            Maximum bin size
        bins : int
            Initial number of bins
        minBins : int
            Minimum number of bins - will not be used if adaptive = False
        maxBins : int
            Maximum number of bins - will not be used if adaptive = False
        adaptive : bool
            Sets adaptive bin sizes - bins may still change upon nucleation
        phase : str
            Phase to consider (will set all phases if phase = None or 'all')
        '''
        if phase is None or phase == 'all':
            for p in range(len(self.phases)):
                self.PBM[p] = PopulationBalanceModel(cMin, cMax, bins, minBins, maxBins)
                self.PBM[p].setAdaptiveBinSize(adaptive)
        else:
            index = self.phaseIndex(phase)
            self.PBM[index] = PopulationBalanceModel(cMin, cMax, bins, minBins, maxBins)
            self.PBM[index].setAdaptiveBinSize(adaptive)

    def loadParticleSizeDistribution(self, data, phase = None):
        '''
        Loads particle size distribution for specified phase

        Parameters
        ----------
        data : array
            Array of data containing precipitate sizes
        phase : str (optional)
            Phase to consider (defaults to first precipitate in list)
        '''
        index = self.phaseIndex(phase)
        self.PBM[index].LoadDistribution(data)

    def addAdditionalOutput(self, name, f):
        '''
        Creates output based off PSD

        Parameters
        ----------
        name : str
            Name of the function
        f : function
            Takes in model, phase index and iteration index and returns a value
        '''
        if name in self.additionalFunctionNames:
            i = 1
            name = name + '_{}'.format(i)
            while name in self.additionalFunctionNames:
                i += 1
                name = name[:-2]
                name = name + '_{}'.format(i)
            print('Warning: Function \'{}\' has already been set, this function will be stored as \'{}\''.format(name[:-2], name))

        self.additionalFunctions.append(f)
        self.additionalFunctionNames = np.append(self.additionalFunctionNames, name)

    def _setupAdditionalOutputs(self):
        '''
        Function to setup PSD output arrays, will be used in setup and reset functions
        '''
        #Resets PSD outputs
        if len(self.additionalFunctions) > 0:
            self.additionalOutputs = np.zeros((len(self.phases), self.steps, len(self.additionalFunctions)))

    def _calculateAdditionalOutputs(self, i):
        '''
        Calculates additional PSD functions
        '''
        for f in range(len(self.additionalFunctions)):
            for p in range(len(self.phases)):
                self.additionalOutputs[p, i, f] = self.additionalFunctions[f](self, p, i)

    def getAdditionalOutput(self, name):
        '''
        Gets additional output by name

        Parameters
        ----------
        name : str
            Name of function used for the additional output

        Returns
        -------
        (p, N) array for the output for each phase
        '''
        if name in self.additionalFunctionNames:
            index, = np.where(self.additionalFunctionNames == name)
            return self.additionalOutputs[:, :, index[0]]

    def particleRadius(self, phase = None):
        '''
        Returns PSD bounds of given phase

        Parameters
        ----------
        phase : str (optional)
            Phase to consider (defaults to first precipitate in list)
        '''
        index = self.phaseIndex(phase)
        return self.PBM[index].PSDbounds

    def particleGibbs(self, radius = None, phase = None):
        '''
        Returns Gibbs Thomson contribution of a particle given its radius

        Parameters
        ----------
        radius : array (optional)
            Precipitate radaii (defaults to None, which will use boundaries
                of the size classes of the precipitate PSD)
        phase : str (optional)
            Phase to consider (defaults to first precipitate in list)
        '''
        if radius is None:
            index = self.phaseIndex(phase)
            radius = self.PBM[index].PSDbounds
        return super().particleGibbs(radius, phase)

    def PSD(self, phase = None):
        '''
        Returns frequency of particle size distribution of given phase

        Parameters
        ----------
        phase : str (optional)
            Phase to consider (defaults to first precipitate in list)
        '''
        index = self.phaseIndex(phase)
        return self.PBM[index].PSD

    def createLookup(self, i = 0):
        '''
        This creates a lookup table mapping the particle size classes to the interfacial composition
        '''
        #RdrivingForceIndex will find the index of the largest particle size class where the precipitate is unstable
        #This is determined by the interfacial composition function, where it should return -1 or None
        #All compositions from the PSD bounds will be set to the compositions just above RdrivingForceLimit
        #This is just to allow for particles to dissolve instead of pile up in the smallest bin
        self.RdrivingForceIndex = np.zeros(len(self.phases), dtype=np.int32)

        #Keep as separate arrays so that number of PSD classes can change within precipitate phases
        self.PSDXalpha = []
        self.PSDXbeta = []

        for p in range(len(self.phases)):
            #Interfacial compositions at equilibrium (planar interface)
            self.xEqAlpha[p,i], self.xEqBeta[p,i] = self.interfacialComposition[p](self.T[i], 0)
            if self.xEqAlpha[p,i] == -1 or self.xEqAlpha[p,i] is None:
                self.xEqAlpha[p,i] = 0
                self.xEqBeta[p,i] = 0

            #Interfacial compositions at each size class in PSD
            self.PSDXalpha.append(np.zeros(self.PBM[p].bins + 1))
            self.PSDXbeta.append(np.zeros(self.PBM[p].bins + 1))

            self.PSDXalpha[p], self.PSDXbeta[p] = self.interfacialComposition[p](self.T[i], self.particleGibbs(self.PBM[p].PSDbounds, self.phases[p]))
            self.RdrivingForceIndex[p] = np.argmax(self.PSDXalpha[p] != -1)-1
            self.RdrivingForceIndex[p] = 0 if self.RdrivingForceIndex[p] < 0 else self.RdrivingForceIndex[p]
            self.RdrivingForceLimit[p] = self.PBM[p].PSDbounds[self.RdrivingForceIndex[p]]

            #Sets particle radii smaller than driving force limit to driving force limit composition
            #If RdrivingForceIndex is at the end of the PSDX arrays, then no precipitate in the size classes of the PSD is stable
            #This can occur in non-isothermal situations where the temperature gets too high
            if self.RdrivingForceIndex[p]+1 < len(self.PSDXalpha[p]):
                self.PSDXalpha[p][:self.RdrivingForceIndex[p]+1] = self.PSDXalpha[p][self.RdrivingForceIndex[p]+1]
                self.PSDXbeta[p][:self.RdrivingForceIndex[p]+1] = self.PSDXbeta[p][self.RdrivingForceIndex[p]+1]
            else:
                self.PSDXalpha[p] = np.zeros(self.PBM[p].bins + 1)
                self.PSDXbeta[p] = np.zeros(self.PBM[p].bins + 1)

    def setup(self):
        '''
        Sets up additional variables in addition to PrecipitateBase

        Sets up additional outputs, population balance models, equilibrium aspect ratio and equilibrium compositions
        '''
        super().setup()

        self._setupAdditionalOutputs()

        #Equilibrium aspect ratio and PBM setup
        #If calculateAspectRatio is True, then use strain energy to calculate aspect ratio for each size class in PSD
        #Else, then use aspect ratio defined in shape factors
        self.eqAspectRatio = [None for p in range(len(self.phases))]
        for p in range(len(self.phases)):
            self.PBM[p].reset()

            if self.calculateAspectRatio[p]:
                self.eqAspectRatio[p] = self.strainEnergy[p].eqAR_bySearch(self.PBM[p].PSDbounds, self.gamma[p], self.shapeFactors[p])
                arFunc = lambda R, p1=p : self._interpolateAspectRatio(R, p1)
                self.shapeFactors[p].setAspectRatio(arFunc)
            else:
                self.eqAspectRatio[p] = self.shapeFactors[p].aspectRatio(self.PBM[p].PSDbounds)

        #Only create lookup table for binary system
        if self.numberOfElements == 1:
            self.createLookup(0)
        else:
            self.PSDXalpha = [None for p in range(len(self.phases))]
            self.PSDXbeta = [None for p in range(len(self.phases))]

            #Set first index of eq composition
            for p in range(len(self.phases)):
                #Use arbitrary dg, R and gE since only the eq compositions are needed here
                _, _, _, xEqAlpha, xEqBeta = self.interfacialComposition[p](self.xComp[0], self.T[0], 0, 1, 0)
                if xEqAlpha is not None:
                    self.xEqAlpha[p,0] = xEqAlpha
                    self.xEqBeta[p,0] = xEqBeta

    def _interpolateAspectRatio(self, R, p):
        '''
        Linear interpolation between self.eqAspectRatio and self.PBM[p].PSDbounds

        Parameters
        ----------
        R : float
            Equivalent spherical radius
        p : int
            Phase index
        '''
        return np.interp(R, self.PBM[p].PSDbounds, self.eqAspectRatio[p])

    def _iterate(self, i):
        '''
        Iteration function
        '''
        #Nucleation and growth rate are independent of time increment
        #They can be calculated first and used to determine the time increment for numerical stability
        self._nucleate(i)
        self._setNucleateRadius(i)
        self._growthRate(i)
        self._timeIncrementCheck(i)

        #Backup variables in case size classes on PSD changes
        self.growthBackup = copy.copy(self.growth)
        self.PSDXalphaBackup = copy.copy(self.PSDXalpha)
        self.PSDXbetaBackup = copy.copy(self.PSDXbeta)
        self.eqAspectRatioBackup = copy.copy(self.eqAspectRatio)
        self.RdrivingForceIndexBackup = copy.copy(self.RdrivingForceIndex)
        self.RdrivingForceLimitBackup = copy.copy(self.RdrivingForceLimit)

        postDTCheck = False
        while not postDTCheck:
            dt = self.time[i] - self.time[i-1]
            self._calculatePSD(i, dt)
            self._massBalance(i)

            if i < self.steps - 1:
                postDTCheck = self._postTimeIncrementCheck(i)
            else:
                postDTCheck = True

        #Calculate additional PSD function
        self._calculateAdditionalOutputs(i)

    def _noCheckDT(self, i):
        '''
        Function if adaptive time stepping is not used
        Will calculated growth rate since it is done in the _checkDT function (not a good way of doing this, but works for now)
        '''
        return

    def _checkDT(self, i):
        '''
        Checks max growth rate and updates dt correspondingly
        '''
        dt = self._calculateDT(i-1, self.maxDTFraction)
        dtAll = [dt]

        if self.checkPSD:
            if self.T[i] == self.T[i-1]:
                dtPBM = [self.PBM[p].getDTEuler(dt, self.growth[p], self.maxDissolution, self.RdrivingForceIndex[p]) for p in range(len(self.phases))]
            else:
                dtPBM = [dt]
            dt = np.amin(np.concatenate(([dt], dtPBM)))
            dtAll.append(dt)

        if i > 1:
            dtPrev = self.time[i-1] - self.time[i-2]
        else:
            dtPrev = dt

        #Nucleation rate constraint
        if self.checkNucleation:
            dtNuc = dt * np.ones(len(self.phases)+1)
            for p in range(len(self.phases)):
                if self.nucRate[p,i] > self.minNucleationRate and self.nucRate[p,i-1] > self.minNucleationRate and self.nucRate[p,i-1] != self.nucRate[p,i]:
                    dtNuc[p] = self.maxNucleationRateChange * dtPrev / np.abs(np.log10(self.nucRate[p,i-1] / self.nucRate[p,i]))
            dt = np.amin(dtNuc)
            dtAll.append(dt)

        #Temperature change constraint
        if self.checkTemperature:
            Tchange = self.T[i] - self.T[i-1]
            dtTemp = dt
            if Tchange > self.maxNonIsothermalDT:
                dtTemp = self.maxNonIsothermalDT * (self.time[i] - self.time[i-1]) / Tchange
                dt = np.amin([dt, dtTemp])

        if self.checkRcrit:
            dtRad = dt * np.ones(len(self.phases)+1)
            if not all((self.Rcrit[:,i-1] == 0) & (self.Rcrit[:,i] - self.Rcrit[:,i-1] == 0) & (self.dGs[:,i] <= 0)):
                indices = (self.Rcrit[:,i-1] > 0) & (self.Rcrit[:,i] - self.Rcrit[:,i-1] != 0) & (self.dGs[:,i] > 0)
                dtRad[:-1][indices] = self.maxRcritChange * dtPrev / np.abs((self.Rcrit[:,i][indices] - self.Rcrit[:,i-1][indices]) / self.Rcrit[:,i-1][indices])
            dt = np.amin(dtRad)
            dtAll.append(dt)

        if self.checkVolumePre or self.checkCompositionPre:
            dV = np.zeros(len(self.phases))
            for p in range(len(self.phases)):
                #Calculate estimate volume change based off growth rate and nucleated particles
                #TODO: account for non-spherical precipitates
                dVi = self.PBM[p].PSD * self.PBM[p].PSDsize**2 * 0.5 * (self.growth[p][1:] + self.growth[p][:-1])
                dVi[dVi < 0] = 0
                dV = self.VmAlpha / self.VmBeta[p] * (self.GB[p].areaFactor * np.sum(dVi) + self.GB[p].volumeFactor * self.nucRate[p,i] * self.Rad[p,i]**3)

            if self.checkVolumePre:
                dtVol = dt * np.ones(len(self.phases) + 1)
                for p in range(len(self.phases)):
                    if dV != 0:
                        dtVol[p] = self.maxVolumeChange / (2 * np.abs(dV))
                #if not all((self.Rad[:,i]**3*self.nucRate[:,i] > 1e-30)):
                #    indices = (self.Rad[:,i]**3*self.nucRate[:,i] > 1e-30)
                #    dtVol[:-1][indices] = self.maxVolumeChange / (10 * (4*np.pi*self.Rad[:,i][indices]**3*self.nucRate[:,i][indices]/3))
                dt = np.amin(dtVol)
                dtAll.append(dt)

            if self.checkCompositionPre:
                dtComp = dt * np.ones(self.numberOfElements + 1)
                fvsum = np.sum(self.betaFrac[:,i-1])
                xbavg = np.zeros(self.numberOfElements)
                if self.numberOfElements == 1:
                    xbavg[0] = 0 if fvsum == 0 else (self.xComp[0] - self.xComp[i-1] * (1 - fvsum)) / fvsum
                    dxadt = (self.xComp[i-1] - xbavg) * np.sum(dV) / (1 - fvsum)
                else:
                    for e in range(self.numberOfElements):
                        xbavg[e] = 0 if fvsum == 0 else (self.xComp[0,e] - self.xComp[i-1,e] * (1 - fvsum)) / fvsum
                    dxadt = (self.xComp[i-1,:] - xbavg) * np.sum(dV) / (1 - fvsum)
                dxadt[dxadt == 0] = self.maxCompositionChange / (2 * dt)
                dtComp[:self.numberOfElements] = self.maxCompositionChange / (2 * dxadt)

                dt = np.amin(dtComp)
                dtAll.append(dt)

        #Minimum dt is the lower of the minimum allowed time increment or the time to the next pre-defined increment
        minDT = self._calculateDT(i-1, self.minDTFraction)
        dt = np.amax([dt, minDT])

        #Override time increment with the predefined time steps
        #This prevents the next time increment from becoming 0 or negative
        dt = np.amin([dt, self.time[i] - self.time[i-1]])

        if dt < self.time[i] - self.time[i-1]:
            #print(dtAll)
            self._divideTimestep(i, dt)

    def _noPostCheckDT(self, i):
        '''
        Function if no adaptive time stepping is used, no need to do anything in this function
        '''
        return True

    def _postCheckDT(self, i):
        '''
        CURRENTLY UNUSED AND MAY BE REMOVED LATER

        If adaptive time step is used, this checks new values at iteration i
        and compares with simulation contraints

        If contraints are not met, then remove current values and divide time step
        '''
        #Only perform checks in non-isothermal situations
        if np.abs(self.T[i] - self.T[i-1]) > 1:
            return True

        #Composition and volume change are checks in absolute changes
        #This prevents any unneccessary reduction in time increments for dilute solutions, or
        #if there is a long incubation time until nucleations starts occuring

        if self.checkVolumePost:
            volChange = np.abs(self.betaFrac[:,i] - self.betaFrac[:,i-1])
            #If current volume fraction is 0, then ignore (either precipitation has not occured or precipitates has dissolved)
            volChange[self.betaFrac[:,i] == 0] = 0
            volCheck = np.amax(volChange) < self.maxVolumeChange
        else:
            volCheck = True

        if self.checkComposition:
            if self.numberOfElements == 1:
                compCheck = (np.abs(self.xComp[i] - self.xComp[i-1]) < self.maxCompositionChange) & (self.xComp[i] > 0)
            else:
                compCheck = (np.amax(np.abs(self.xComp[i,:] - self.xComp[i-1,:])) < self.maxCompositionChange) & (np.amin(self.xComp[i,:] > self.minComposition))
        else:
            compCheck = True

        checks = [volCheck, compCheck]

        #If any test fails, then reset iteration and divide time increment
        if not all(checks):
            dt = (self.time[i] - self.time[i-1]) / 2
            minDT = self._calculateDT(i-1, self.minDTFraction)

            #If proposed time increment is smaller than the minimum allowed increment, then skip the checks
            if dt < minDT:
                return True

            #Only revert changes to variables that aren't stored per iteration
            #Variables related to nucleation are not dependent on the time increment
            #Variables related to the particle size distribution (composition, volume fraction, etc)
            # will be overridden if the time increment changes
            self.prevFConc[0] = copy.copy(self.prevFConc[1])

            for p in range(len(self.phases)):
                self.PBM[p].revert()
            self.growth = copy.copy(self.growthBackup)
            self.PSDXalpha = copy.copy(self.PSDXalphaBackup)
            self.PSDXbeta = copy.copy(self.PSDXbetaBackup)
            self.eqAspectRatio = copy.copy(self.eqAspectRatioBackup)
            self.RdrivingForceIndex = copy.copy(self.RdrivingForceIndexBackup)
            self.RdrivingForceLimit = copy.copy(self.RdrivingForceLimitBackup)

            self._divideTimestep(i, dt)

            return False
        else:
            return True

    def _nucleate(self, i):
        '''
        Calculates the nucleation rate at current timestep
        This can be done before the initial time increment checks are performed
        '''
        for p in range(len(self.phases)):
            #If parent phases exists, then calculate the number of potential nucleation sites on the parent phase
            #This is the number of lattice sites on the total surface area of the parent precipitate
            nucleationSites = np.sum([4 * np.pi * self.PBM[p2].SecondMoment() * (self.avo / self.VmBeta[p2])**(2/3) for p2 in self.parentPhases[p]])

            if self.GB[p].nucleationSiteType == GBFactors.BULK:
                #bulkPrec = np.sum([self.GB[p2].volumeFactor * self.PBM[p2].ThirdMoment() for p2 in range(len(self.phases)) if self.GB[p2].nucleationSiteType == GBFactors.BULK])
                #nucleationSites += self.bulkN0 - bulkPrec * (self.avo / self.VmAlpha)
                bulkPrec = np.sum([self.PBM[p2].ZeroMoment() for p2 in range(len(self.phases)) if self.GB[p2].nucleationSiteType == GBFactors.BULK])
                nucleationSites += self.bulkN0 - bulkPrec
            elif self.GB[p].nucleationSiteType == GBFactors.DISLOCATION:
                bulkPrec = np.sum([self.PBM[p2].FirstMoment() for p2 in range(len(self.phases)) if self.GB[p2].nucleationSiteType == GBFactors.DISLOCATION])
                nucleationSites += self.dislocationN0 - bulkPrec * (self.avo / self.VmAlpha)**(1/3)
            elif self.GB[p].nucleationSiteType == GBFactors.GRAIN_BOUNDARIES:
                boundPrec = np.sum([self.GB[p2].gbRemoval * self.PBM[p2].SecondMoment() for p2 in range(len(self.phases)) if self.GB[p2].nucleationSiteType == GBFactors.GRAIN_BOUNDARIES])
                nucleationSites += self.GBareaN0 - boundPrec * (self.avo / self.VmAlpha)**(2/3)
            elif self.GB[p].nucleationSiteType == GBFactors.GRAIN_EDGES:
                edgePrec = np.sum([np.sqrt(1 - self.GB[p2].GBk**2) * self.PBM[p2].FirstMoment() for p2 in range(len(self.phases)) if self.GB[p2].nucleationSiteType == GBFactors.GRAIN_EDGES])
                nucleationSites += self.GBedgeN0 - edgePrec * (self.avo / self.VmAlpha)**(1/3)
            elif self.GB[p].nucleationSiteType == GBFactors.GRAIN_CORNERS:
                cornerPrec = np.sum([self.PBM[p2].ZeroMoment() for p2 in range(len(self.phases)) if self.GB[p2].nucleationSiteType == GBFactors.GRAIN_CORNERS])
                nucleationSites += self.GBcornerN0 - cornerPrec

            if nucleationSites < 0:
                nucleationSites = 0
            self.nucRate[p, i] = nucleationSites * self._nucleationRate(p, i)

    def _calculatePSD(self, i, dt):
        '''
        Updates the PSD using the population balance model from coarsening and nucleation rate
        This also updates the fraction of precipitates, matrix composition and average radius
        '''
        for p in range(len(self.phases)):
            #Backup PSD for time increment checks
            #Also backup PSDXbeta for precipitate composition with no diffusion
            self.PBM[p].createBackup()
            self._prevPSDXbeta = copy.copy(self.PSDXbeta)

            change1, newIndices = self.PBM[p].UpdateEuler(dt, self.growth[p])
            change2 = self.PBM[p].Nucleate(self.nucRate[p, i] * dt, self.Rad[p, i])
            if change1 or change2:
                #Add aspect ratio, do this before growth rate and interfacial composition since those are dependent on this
                if self.calculateAspectRatio[p]:
                    self.eqAspectRatio[p] = self.strainEnergy[p].eqAR_bySearch(self.PBM[p].PSDbounds, self.gamma[p], self.shapeFactors[p])
                else:
                    self.eqAspectRatio[p] = self.shapeFactors[p].aspectRatio(self.PBM[p].PSDbounds)

                self.growth[p] = np.zeros(len(self.PBM[p].PSDbounds))
                if self.numberOfElements == 1:
                    if newIndices is None:
                        #This is very slow to do
                        self.createLookup(i)
                    else:
                        self.PSDXalpha[p] = np.concatenate((self.PSDXalpha[p], np.zeros(self.PBM[p].bins+1 - len(self.PSDXalpha[p]))))
                        self.PSDXbeta[p] = np.concatenate((self.PSDXbeta[p], np.zeros(self.PBM[p].bins+1 - len(self.PSDXbeta[p]))))
                        self.PSDXalpha[p][newIndices:], self.PSDXbeta[p][newIndices:] = self.interfacialComposition[p](self.T[i-1], self.particleGibbs(self.PBM[p].PSDbounds[newIndices:], self.phases[p]))
                    self.growth[p] = self._singleGrowthBinary(i, p)
                else:
                    self.growth[p] = self._singleGrowthMulti(i, p)

            #Set negative frequencies in PSD to 0
            #Also set any less than the minimum possible radius to be 0
            self.PBM[p].PSD[:self.RdrivingForceIndex[p]] = 0
            self.PBM[p].PSD[self.PBM[p].PSDsize < self.minRadius] = 0

    def _massBalance(self, i):
        '''
        Updates matrix composition and volume fraction of precipitates
        '''
        fBeta = np.zeros(len(self.phases))
        if self.numberOfElements == 1:
            fConc = np.zeros(len(self.phases))
        else:
            fConc = np.zeros((len(self.phases), self.numberOfElements))

        for p in range(len(self.phases)):
            #Sum up particles and average for particles
            Ntot = self.PBM[p].ZeroMoment()
            RadSum = self.PBM[p].Moment(order=1)
            ARsum = self.PBM[p].WeightedMoment(0, self.shapeFactors[p].aspectRatio(self.PBM[p].PSDsize))
            fBeta[p] = self.VmAlpha / self.VmBeta[p] * self.GB[p].volumeFactor * self.PBM[p].ThirdMoment()

            if self.numberOfElements == 1:
                if self.infinitePrecipitateDiffusion[p]:
                    fConc[p] = self.VmAlpha / self.VmBeta[p] * self.GB[p].volumeFactor * self.PBM[p].WeightedMoment(3, 0.5 * (self.PSDXbeta[p][:-1] + self.PSDXbeta[p][1:]))
                else:
                    y = self.VmAlpha / self.VmBeta[p] * self.GB[p].areaFactor * np.sum(self.PBM[p]._prevPSDbounds[1:]**2 * self.PBM[p]._fv[1:] * self._prevPSDXbeta[p][1:] * (self.PBM[p]._prevPSDbounds[1:] - self.PBM[p]._prevPSDbounds[:-1]))
                    fConc[p] = self.prevFConc[0,p,0] + y
                self.prevFConc[1,p,0] = copy.copy(self.prevFConc[0,p,0])
                self.prevFConc[0,p,0] = fConc[p]
            else:
                if self.infinitePrecipitateDiffusion[p]:
                    for a in range(self.numberOfElements):
                        fConc[p,a] = self.VmAlpha / self.VmBeta[p] * self.GB[p].volumeFactor * self.PBM[p].WeightedMoment(3, 0.5 * (self.PSDXbeta[p][:-1,a] + self.PSDXbeta[p][1:,a]))
                else:
                    for a in range(self.numberOfElements):
                        y = self.VmAlpha / self.VmBeta[p] * self.GB[p].areaFactor * np.sum(self.PBM[p]._prevPSDbounds[1:]**2 * self.PBM[p]._fv[1:] * self._prevPSDXbeta[p][1:,a] * (self.PBM[p]._prevPSDbounds[1:] - self.PBM[p]._prevPSDbounds[:-1]))
                        fConc[p,a] = self.prevFConc[0,p,a] + y
                self.prevFConc[1,p] = copy.copy(self.prevFConc[0,p])
                self.prevFConc[0,p] = fConc[p]

            #Average radius and precipitate density
            if Ntot > 0:
                self.avgR[p, i] = RadSum / Ntot
                self.precipitateDensity[p, i] = Ntot
                self.avgAR[p, i] = ARsum / Ntot
            else:
                self.avgR[p, i] = 0
                self.precipitateDensity[p, i] = 0
                self.avgAR[p, i] = 0

            #Volume fraction (max at 1)
            if fBeta[p] > 1:
                fBeta[p] = 1
            if self.betaFrac[p, i-1] == 1:
                fBeta[p] = 1

            self.betaFrac[p, i] = fBeta[p]

        #Composition (min at 0)
        if self.numberOfElements == 1:
            if np.sum(fBeta) < 1:
                self.xComp[i] = (self.xComp[0] - np.sum(fConc)) / (1 - np.sum(fBeta))
            else:
                self.xComp[i] = 0
        else:
            if np.sum(fBeta) < 1:
                self.xComp[i] = (self.xComp[0] - np.sum(fConc, axis=0)) / (1 - np.sum(fBeta))
                self.xComp[i][self.xComp[i] < 0] = self.minComposition
            else:
                self.xComp[i] = np.zeros(self.numberOfElements)

    def _singleGrowthBinary(self, i, p):
        '''
        Calculates growth rate for a single phase
        This is separated from _growthRateBinary since it's used in _calculatePSD

        Matrix/precipitate composition are not calculated here since it's
        already calculated in createLookup
        '''
        growthRate = np.zeros(self.PBM[p].bins + 1)
        #If no precipitates are stable, don't calculate growth rate and set PSD to 0
        #This should represent dissolution of the precipitates
        if self.RdrivingForceIndex[p]+1 < len(self.PSDXalpha[p]):
            superSaturation = (self.xComp[i-1] - self.PSDXalpha[p]) / (self.VmAlpha * self.PSDXbeta[p] / self.VmBeta[p] - self.PSDXalpha[p])
            growthRate = self.shapeFactors[p].kineticFactor(self.PBM[p].PSDbounds) * self.Diffusivity(self.xComp[i-1], self.T[i]) * superSaturation / (self.effDiffDistance(superSaturation) * self.PBM[p].PSDbounds)
        else:
            self.PBM[p].PSD = np.zeros(self.PBM[p].bins)

        return growthRate

    def _growthRateBinary(self, i):
        '''
        Determines current growth rate of all particle size classes in a binary system
        '''
        #Update equilibrium interfacial compositions
        #This will be override if createLookup is called
        self.xEqAlpha[:,i] = self.xEqAlpha[:,i-1]
        self.xEqBeta[:,i] = self.xEqBeta[:,i-1]

        #Update lookup table if temperature changes too much
        self.dTemp += self.T[i] - self.T[i-1]
        if np.abs(self.dTemp) > self.maxTempChange:
            self.createLookup(i)
            self.dTemp = 0

        #growthRate = np.zeros((len(self.phases), self.bins + 1))
        growthRate = []
        for p in range(len(self.phases)):
            growthRate.append(self._singleGrowthBinary(i, p))

        self.growth = growthRate

    def _singleGrowthMulti(self, i, p):
        '''
        Calculates growth rate for a single phase
        This is separated from _growthRateMulti since it's used in _calculatePSD

        This will also calculate the matrix/precipitate composition
        for the radius in the PSD as well as equilibrium (infinite radius)
        '''
        growth, xAlpha, xBeta, xEqAlpha, xEqBeta = self.interfacialComposition[p](self.xComp[i-1], self.T[i], self.dGs[p,i-1] * self.VmBeta[p], self.PBM[p].PSDbounds, self.particleGibbs(phase=self.phases[p]))

        #If two-phase equilibrium not found, two possibilities - precipitates are unstable or equilibrium calculations didn't converge
        if growth is None:
            #If driving force is negative, then precipitates are unstable
            if self.dGs[p,i] < 0:
                #Completely reset the PBM, including bounds and number of bins
                #In case nucleation occurs again, the PBM will be at a good length scale
                self.PBM[p].reset()
                self.PSDXalpha[p] = np.zeros((self.PBM[p].bins + 1, self.numberOfElements))
                self.PSDXbeta[p] = np.zeros((self.PBM[p].bins + 1, self.numberOfElements))
                self.xEqAlpha[p,i] = np.zeros(self.numberOfElements)
                self.xEqBeta[p,i] = np.zeros(self.numberOfElements)
                return np.zeros(self.PBM[p].bins + 1)
            #Else, equilibrium did not converge and just use previous values
            #Only the growth rate needs to be updated, since all other terms are previous
            #Also revert the PSD in case this function was called to adjust for the new PSD bins
            else:
                self.PBM[p].revert()
                return self.growth[p]
        else:
            #Update interfacial composition for each precipitate size
            self.PSDXalpha[p] = xAlpha
            self.PSDXbeta[p] = xBeta
            self.xEqAlpha[p,i] = xEqAlpha
            self.xEqBeta[p,i] = xEqBeta

            #Add shape factor to growth rate - will need to add effective diffusion distance as well
            return self.shapeFactors[p].kineticFactor(self.PBM[p].PSDbounds) * growth

    def _growthRateMulti(self, i):
        '''
        Determines current growth rate of all particle size classes in a multicomponent system
        '''
        growthRate = []
        for p in range(len(self.phases)):
            growthRate.append(self._singleGrowthMulti(i, p))
        self.growth = growthRate

    def plot(self, axes, variable, bounds = None, timeUnits = 's', radius='spherical', *args, **kwargs):
        '''
        Plots model outputs

        Parameters
        ----------
        axes : Axis
        variable : str
            Specified variable to plot
            Options are 'Volume Fraction', 'Total Volume Fraction', 'Critical Radius',
                'Average Radius', 'Volume Average Radius', 'Total Average Radius',
                'Total Volume Average Radius', 'Aspect Ratio', 'Total Aspect Ratio'
                'Driving Force', 'Nucleation Rate', 'Total Nucleation Rate',
                'Precipitate Density', 'Total Precipitate Density',
                'Temperature', 'Composition',
                'Size Distribution', 'Size Distribution Curve',
                'Size Distribution KDE', 'Size Distribution Density
                'Interfacial Composition Alpha', 'Interfacial Composition Beta'

                Note: for multi-phase simulations, adding the word 'Total' will
                    sum the variable for all phases. Without the word 'Total', the variable
                    for each phase will be plotted separately

                    Interfacial composition terms are more relavent for binary systems than
                    for multicomponent systems

        bounds : tuple (optional)
            Limits on the x-axis (float, float) or None (default, this will set bounds to (initial time, final time))
        radius : str (optional)
            For non-spherical precipitates, plot the Average Radius by the -
                Equivalent spherical radius ('spherical')
                Short axis ('short')
                Long axis ('long')
            Note: Total Average Radius and Volume Average Radius will still use the equivalent spherical radius
        *args, **kwargs - extra arguments for plotting
        '''
        sizeDistributionVariables = ['Size Distribution', 'Size Distribution Curve', 'Size Distribution KDE', 'Size Distribution Density']
        compositionVariables = ['Interfacial Composition Alpha', 'Interfacial Composition Beta']

        scale = []
        for p in range(len(self.phases)):
            if self.GB[p].nucleationSiteType == self.GB[p].BULK or self.GB[p].nucleationSiteType == self.GB[p].DISLOCATION:
                if radius == 'spherical':
                    scale.append(self._GBareaRemoval(p) * np.ones(len(self.PBM[p].PSDbounds)))
                else:
                    scale.append(1/self.shapeFactors[p].eqRadiusFactor(self.PBM[p].PSDbounds))
                    if radius == 'long':
                        scale.append(self.shapeFactors[p].aspectRatio(self.PBM[p].PSDbounds) / self.shapeFactors[p].eqRadiusFactor(self.PBM[p].PSDbounds))
            else:
                scale.append(self._GBareaRemoval(p) * np.ones(len(self.PBM[p].PSDbounds)))

        if variable in compositionVariables:
            if variable == 'Interfacial Composition Alpha':
                yVar = self.PSDXalpha
                ylabel = 'Composition in Alpha phase'
            else:
                yVar = self.PSDXbeta
                ylabel = 'Composition in Beta Phase'

            if (len(self.phases)) == 1:
                axes.semilogx(self.PBM[0].PSDbounds, yVar[0], *args, **kwargs)
            else:
                for p in range(len(self.phases)):
                    axes.plot(self.PBM[p].PSDbounds, yVar[p], label=self.phases[p], *args, **kwargs)
                axes.legend()
            axes.set_xlim([self.PBM[0].PSDbounds[0], self.PBM[0].PSDbounds[-1]])
            axes.set_xlabel('Radius (m)')
            axes.set_ylabel(ylabel)

        elif variable in sizeDistributionVariables:
            ylabel = 'Frequency (#/$m^3$)'
            if variable == 'Size Distribution':
                functionName = 'PlotHistogram'
            elif variable == 'Size Distribution KDE':
                functionName = 'PlotKDE'
            elif variable == 'Size Distribution Density':
                functionName = 'PlotDistributionDensity'
                ylabel = 'Distribution Density (#/$m^4$)'
            else:
                functionName = 'PlotCurve'

            if len(self.phases) == 1:
                getattr(self.PBM[0], functionName)(axes, scale=scale[0], *args, **kwargs)
            else:
                for p in range(len(self.phases)):
                    getattr(self.PBM[p], functionName)(axes, label=self.phases[p], scale=scale[p], *args, **kwargs)
                axes.legend()
            axes.set_xlabel('Radius (m)')
            axes.set_ylabel(ylabel)
            axes.set_xlim([0, np.amax([pb.max for pb in self.PBM])])
            if variable == 'Size Distribution Density':
                axes.set_ylim([0, 1.1*np.amax(np.concatenate(([np.amax(pb.PSD/(pb.PSDbounds[1:] - pb.PSDbounds[:-1])) for pb in self.PBM], [1])))])
            else:
                axes.set_ylim([0, 1.1*np.amax(np.concatenate(([np.amax(pb.PSD) for pb in self.PBM], [1])))])

        elif variable == 'Cumulative Size Distribution':
            ylabel = 'CDF'
            if len(self.phases) == 1:
                self.PBM[0].PlotCDF(axes, scale=scale[0], *args, **kwargs)
            else:
                for p in range(len(self.phases)):
                    self.PBM[p].PlotCDF(axes, label=self.phases[p], scale=scale[p], *args, **kwargs)
                axes.legend()
            axes.set_xlabel('Radius (m)')
            axes.set_ylabel(ylabel)
            axes.set_xlim([0, np.amax([pb.max for pb in self.PBM])])

        elif variable == 'Aspect Ratio Distribution':
            if len(self.phases) == 1:
                axes.plot(self.PBM[0].PSDbounds * np.interp(self.PBM[p].PSDbounds, self.PBM[0].PSDbounds, scale[0]), self.eqAspectRatio[0], *args, **kwargs)
            else:
                for p in range(len(self.phases)):
                    axes.plot(self.PBM[p].PSDbounds * np.interp(self.PBM[p].PSDbounds, self.PBM[p].PSDbounds, scale[p]), self.eqAspectRatio[p], label=self.phases[p], *args, **kwargs)
                axes.legend()
            axes.set_xlim([0, np.amax(self.PBM[p].PSDbounds * np.interp(self.PBM[p].PSDbounds, self.PBM[p].PSDbounds, scale[p]))])
            axes.set_ylim(bottom=1)
            axes.set_xlabel('Radius (m)')
            axes.set_ylabel('Aspect ratio distribution')

        else:
            super().plot(axes, variable, bounds, timeUnits, radius, *args, **kwargs)


from django.test import  TestCase
from neomodel import db
import time
from import_export import apoc_del_redundant_high_med

class TurtlePostProcessingTestCase(TestCase):
    def setUp(self):
        ts = time.time()
        uri1 = f"http://{ts}/foo"
        uri2 = f"http://{ts}/bar"
        uri3 = f"http://{ts}/baz"

        db.cypher_query(f'''
            CREATE
            (a:Organization {{uri:"{uri1}",name:"foo"}}),
            (b:Organization {{uri:"{uri2}",name:"bar"}}),
            (c:Organization {{uri:"{uri3}",name:"baz"}}),
            (a)-[:sameAsHigh]->(b)-[:sameAsHigh]->(a),
            (a)-[:sameAsHigh]->(c)-[:sameAsHigh]->(a),
            (b)-[:sameAsHigh]->(c)-[:sameAsHigh]->(b)
            RETURN *
        ''')

        self.uri1 = uri1
        self.uri2 = uri2
        self.uri3 = uri3

    def test_deletes_not_needed_same_as(self):
        uri1 = self.uri1 # Just for convenience in case need to copy paste items below into shell
        uri2 = self.uri2
        uri3 = self.uri3

        counts1, _ = db.cypher_query(f'MATCH (n {{uri:"{uri1}"}})-[o:sameAsHigh]-(p) return count(o)' )
        counts2, _ = db.cypher_query(f'MATCH (n {{uri:"{uri2}"}})-[o:sameAsHigh]-(p) return count(o)' )
        counts3, _ = db.cypher_query(f'MATCH (n {{uri:"{uri3}"}})-[o:sameAsHigh]-(p) return count(o)' )

        assert counts1[0][0] == 4
        assert counts2[0][0] == 4
        assert counts3[0][0] == 4

        apoc_del_redundant_high_med()

        counts1_after, _ = db.cypher_query(f'MATCH (n {{uri:"{uri1}"}})-[o:sameAsHigh]-(p) return count(o)' )
        counts2_after, _ = db.cypher_query(f'MATCH (n {{uri:"{uri2}"}})-[o:sameAsHigh]-(p) return count(o)' )
        counts3_after, _ = db.cypher_query(f'MATCH (n {{uri:"{uri3}"}})-[o:sameAsHigh]-(p) return count(o)' )

        assert counts1_after[0][0] == 2
        assert counts2_after[0][0] == 2
        assert counts3_after[0][0] == 2


driving = input('請問你有沒有開過車?')
if driving != '有' and driving != '沒有':
	print('只能輸入有或沒有')
	raise SystemExit

age = input('請問你的年齡?')
age = int(age)

if driving == '有':
	if age >= 18:
		print('你通過測驗了')
	else:
		print('奇怪你怎麼會開過車')


class Solution:
    def combinations(self,A,B,C,index,aux):
        if index == 9:
            if sum(aux) == B and len(aux) == C:
                self.ans.append(aux)
            return

        pick = self.combinations(A,B,C,index+1,aux + [A[index]])
        dont = self.combinations(A,B,C,index+1,aux)
        return pick and dont

    def Solve(self, A, B):
        self.ans = []
        data = [i for i in range(1,10)]
        self.combinations(data,B,A,0,[])
        return self.ans

if __name__ == '__main__':
    A = 2
    B = 18
    C = Solution()
    print(C.Solve(A,B))

BackTracking/216. Combination Sum III.py


from tkinter import *
import math
import pandas
import pandas as pd
from nltk.sentiment.vader import SentimentIntensityAnalyzer
import os.path
from Scripts.filtering import *
from Scripts.webscraper import *
from PIL import ImageTk, Image
from Scripts.review_analysis import *

##########Initialise values############
window = Tk()
HotelCSVData = pd.read_csv("Filtered_Datafiniti_Hotel_Main_Review.csv")
window.geometry("1920x1080")
window.option_add("*Background", "#fff6ec")
window.title("Hotelopolis")
window.iconbitmap("icon/hotelopolis.ico")
senti = SentimentIntensityAnalyzer()
#image = ImageTk.PhotoImage(Image.open("icon/hotelopolis.png"))

Framecolor = "#DAA06D" #color for all frames

#list of hotels on menu (button form)
HotelMenuList=[]
#list of hotels taken from csv
HotelsList =[]
#list of recommended hotels at the side when user clicks hotels
RecommendedHotelsList=[]
#list of bookmarked hotel objects to access class
BookmarkedHotelsObjectsList=[]
#list of bookmarked hotels for dropdown menu
BookmarkedHotelsNameList=[]
#list of review items
ReviewItemsList=[]

#for writing bookmarks
Bookmarkdata=None #if file exists, read from it

#original hotel options
HotelOptions=[]
for name in HotelCSVData["name"]: #add all hotels in csv
    if(pd.isnull(name)): #get rid of any null cells
       continue
    if(name not in HotelOptions):#as the name is repeated multiple times in csv, just take one
        HotelOptions.append(name)

#options for sorting in dropdown menu
MainMenuSortOptions = [
    'Alphabetical A-Z',
    'Alphabetical Z-A',
    'Rating (High to Low)',
    'Rating (Low to High)'
    ]

#options for review filtering
ReviewFilterOptions=[
    'All',
    '1 star',
    '2 stars',
    '3 stars',
    '4 stars',
    '5 stars'
]

def UpdateBookmarkCsv(toggledhotel = None):
    if(toggledhotel == None):
        if(os.path.exists("Bookmarks.csv")):
            Bookmarkdata = pd.read_csv("Bookmarks.csv", index_col=0)
            listofbookmarks = list(Bookmarkdata['name'])
            if(len(HotelOptions) > len(listofbookmarks)):
                NewHotels = [hotel for hotel in HotelOptions if hotel not in listofbookmarks]#check for new hotel and add if neccessary
                BookmarkDataFrame = pd.DataFrame(NewHotels, columns=['name'])
                BookmarkDataFrame["bookmarked"] = False
                Bookmarkdata = pandas.concat([Bookmarkdata, BookmarkDataFrame],axis=0, ignore_index=True)
                Bookmarkdata.to_csv("Bookmarks.csv")

            elif(len(HotelOptions) < len(listofbookmarks)):
                OldHotels = [hotel for hotel in listofbookmarks if hotel not in HotelOptions]  # check for new hotel and add if neccessary
                for hotel in OldHotels:
                    Bookmarkdata.drop(Bookmarkdata[Bookmarkdata['name'] == hotel].index, inplace=True)
                    Bookmarkdata.to_csv("Bookmarks.csv")
        else:
            BookmarkDataFrame = pd.DataFrame(HotelOptions, columns=['name'])  #since bookmarked csv not created, create one and set all hotels to not bookmarked
            BookmarkDataFrame["bookmarked"] = False
            BookmarkDataFrame.to_csv("Bookmarks.csv")

    else:
        Bookmarkdata = pd.read_csv("Bookmarks.csv")
        Bookmarkdata.loc[Bookmarkdata.name == toggledhotel.Name, "bookmarked"] = toggledhotel.Bookmarked
        Bookmarkdata.to_csv("Bookmarks.csv", index=False)  # update csv based on new bookmarks

    Bookmarkdata = pd.read_csv("Bookmarks.csv", index_col=0)
    return Bookmarkdata

class Hotel:
    def __init__(self, name, address, scoreslist, reviewslist, bookmarked): #initialise hotel objects
        self.Name = name
        self.Address = address
        self.ScoresList = scoreslist
        self.ReviewsList = reviewslist
        self.Bookmarked = bookmarked

        #get sentimental anayisis of hotel reviews
        analysis ={}
        analysis["Positive"] = [senti.polarity_scores(i)["pos"] for i in self.ReviewsList]
        analysis["Negative"] = [senti.polarity_scores(i)["neg"] for i in self.ReviewsList]

        TotalScore = 0.00
        if(' ' in self.ScoresList): #detect for hotels without given score and use sentimental analysis score to get review score
            PositiveList = list(analysis["Positive"])
            NegativeList = list(analysis["Negative"])
            for index, values in enumerate(PositiveList):
                totalratingadded = PositiveList[index] + NegativeList[index]
                if(totalratingadded !=0):
                    calculated = round((PositiveList[index]/totalratingadded) * 5) #calculated score by adding positive and negative then divide by total, times 5(out of 5 stars)
                    if(calculated == 0): #to make completely negative reviews have a score of 1
                        calculated=1
                    self.ScoresList[index] = calculated
                    TotalScore += calculated
                else:#if review is completely neutral, give it a score of 3
                    calculated = 3
                    self.ScoresList[index] = calculated
                    TotalScore += calculated
        else:
            for rating in self.ScoresList:
                TotalScore += int(rating)

        self.AverageScore = TotalScore / len(self.ScoresList) #calculate average score by adding all reviews score and divide by number of reviews

        self.Facilities = facility_check(','.join(reviewslist)) #get facilities of hotel from finding keywords from reviews

        GoodReviewsList=[]
        BadReviewsList = []
        for index, score in enumerate(self.ScoresList):
            if(int(score) >=4):
                GoodReviewsList.append(self.ReviewsList[index])
            if (int(score) <= 3):
                BadReviewsList.append(self.ReviewsList[index])

        self.pos_WordCloudReview = ','.join(GoodReviewsList)
        self.neg_WordCloudReview = ','.join(BadReviewsList)

#################functions#####################
def ClearMainMenu(): #clear all widgets on main menu
    MainMenuFrame.grid_forget()
    FilterFrame.grid_forget()

def ClearHotelDetails(): #clear all widgets on hotel details
    RecommendationFrame.grid_forget()
    HotelDetailsFrame.grid_forget()
    MenuButton.grid_forget()
    for Review in ReviewItemsList:
        Review.destroy()

    ReviewItemsList.clear()

def RefreshHotels():
    HotelCSVData = pd.read_csv("Filtered_Datafiniti_Hotel_Main_Review.csv")

    for name in HotelCSVData["name"]:
        if name not in HotelOptions:
            ReviewsList = list(HotelCSVData.loc[HotelCSVData.name == name, 'reviews.text'])
            ReviewsRating = list(HotelCSVData.loc[HotelCSVData.name == name, 'reviews.rating'])
            AddressList = HotelCSVData.loc[HotelCSVData.name == name, 'address']
            Address = AddressList.iat[0]
            Bookmarked = False
            HotelObject = Hotel(name, Address, ReviewsRating, ReviewsList, Bookmarked)
            HotelsList.append(HotelObject)
            HotelOptions.append(name)

    UpdateBookmarkCsv()
    # sort and filter main menu based on default values
    FilterAndSortHotelDetails(SortVariable)

def ToggleBookmark(): #toggle bookmarked status of current hotel
    currenthotelname = HotelNameLabel.cget("text")
    for hotel in HotelsList:
        if(hotel.Name == currenthotelname):
            hotel.Bookmarked = BookmarkedVariable.get()
            if(hotel.Bookmarked):
                BookmarkedHotelsNameList.append(hotel.Name)
                BookmarkedHotelsObjectsList.append(hotel)
                if ("No Bookmarks") in BookmarkedHotelsNameList:
                    BookmarkedHotelsNameList.remove("No Bookmarks")
            else:
                BookmarkedHotelsNameList.remove(hotel.Name)
                BookmarkedHotelsObjectsList.remove(hotel)
                if(len(BookmarkedHotelsNameList)==0):
                    BookmarkedHotelsNameList.append("No Bookmarks")

            UpdateBookmarkCsv(hotel)

    BookmarkDropdown["menu"].delete(0, 'end')
    for hotel in BookmarkedHotelsNameList:
        BookmarkDropdown["menu"].add_command(label= hotel, command=lambda hotel = hotel:SelectFromBookmark(hotel))  #update bookmark dropdown

def FilterAndSortHotelDetails(sortoption = None): #update main menu based on filter and sort options
    for HotelButton in HotelMenuList: #destroy all buttons as we will create them based on all the filter and sort options
        HotelButton.destroy()

    HotelMenuList.clear() #clear list of hotel buttons as they all are destroyed

    #create list to check which ameneties user selected
    amenetiesselectedlist = []
    if(restaurant.get() != "na"):
        amenetiesselectedlist.append(restaurant.get())
    if (pool.get() != "na"):
        amenetiesselectedlist.append(pool.get())
    if (jacuzzi.get() != "na"):
        amenetiesselectedlist.append(jacuzzi.get())
    if(gym.get()!="na"):
        amenetiesselectedlist.append(gym.get())
    if (spa.get() != "na"):
        amenetiesselectedlist.append(spa.get())

    #sort list to be displayed based on user selected sort
    if(SortVariable.get() == 'Alphabetical A-Z'):
        SortedList = sorted(HotelsList, key= lambda x:x.Name)
    elif(SortVariable.get() == 'Alphabetical Z-A'):
        SortedList = sorted(HotelsList, key= lambda x:x.Name, reverse=True)
    elif (SortVariable.get() == 'Rating (High to Low)'):
        SortedList = sorted(HotelsList, key=lambda x: x.AverageScore, reverse=True)
    elif (SortVariable.get() == 'Rating (Low to High)'):
        SortedList = sorted(HotelsList, key=lambda x: x.AverageScore)

    #create buttons based on sorted list, ameneties selected and filtering options
    if(onestar.get() == 0 and twostar.get() == 0 and threestar.get() == 0 and fourstar.get() == 0 and fivestar.get() == 0): #if no star filter selected
        for index, hotel in enumerate(SortedList):
            if (len(amenetiesselectedlist) > 0 and not any(x in hotel.Facilities for x in amenetiesselectedlist)): #if hotel does not have selected ameneites, skip it and dont create a button
                continue
            MenuHotelFrame = Frame(master=MainMenuFrame, highlightthickness=2, highlightbackground=Framecolor)
            MenuHotelFrame.grid(row=index + 1, column=0, sticky=N, pady=10, padx=10)
            facilitieslist = ', '.join(hotel.Facilities)
            HotelPicture =  Label(MenuHotelFrame, text="Insert pic", anchor=CENTER)
            HotelPicture.grid(row=0, column=0, sticky=W, padx=10)
            HotelButton = Button(MenuHotelFrame, text=hotel.Name + "\n"+hotel.Address + "\n Average Score: %0.1f/5" % hotel.AverageScore + "\n Facilities: " + facilitieslist,
                                     command=lambda hotel=hotel: DisplayHotelDetails(hotel), width=60, height=6, font=("Arial", 10), relief=GROOVE)
            HotelButton.grid(row=0, column=1, sticky=W)
            HotelMenuList.append(MenuHotelFrame) #add hotel button back into list
    else:
        for index, hotel in enumerate(SortedList): #else if star filter selected, create buttons based on star filter
            if (len(amenetiesselectedlist) > 0 and not any(x in hotel.Facilities for x in amenetiesselectedlist)): #if hotel does not have selected ameneites, skip it and dont create a button
                continue
            if(int(hotel.AverageScore) == onestar.get() or int(hotel.AverageScore) == twostar.get() or int(hotel.AverageScore) == threestar.get() or int(hotel.AverageScore) == fourstar.get() or int(hotel.AverageScore) == fivestar.get()):
                MenuHotelFrame = Frame(master=MainMenuFrame, highlightthickness=2, highlightbackground=Framecolor)
                MenuHotelFrame.grid(row=index + 1, column=0, sticky=N, pady=10, padx=10)
                facilitieslist = ', '.join(hotel.Facilities)
                HotelPicture = Label(MenuHotelFrame, text="Insert pic", anchor=CENTER)
                HotelPicture.grid(row=0, column=0, sticky=W, padx=10)
                HotelButton = Button(MenuHotelFrame,
                                     text=hotel.Name + "\n" + hotel.Address + "\n Average Score: %0.1f/5" % hotel.AverageScore + "\n Facilities: " + facilitieslist,
                                     command=lambda hotel=hotel: DisplayHotelDetails(hotel), width=60, height=6,
                                     font=("Arial", 10), relief=GROOVE)
                HotelButton.grid(row=0, column=1, sticky=W)
                HotelMenuList.append(MenuHotelFrame)  # add hotel button back into list

    #show no results label if completely empty
    if(len(HotelMenuList) == 0):
        Emptylabel.grid(row= 1, column=0, sticky=N, pady=10)
    else:
        Emptylabel.grid_forget()#clear no result label if there are hotels to display

    UpdateScrollbar()

def SelectFromBookmark(selectedbookmark): #called when u select a hotel from the bookmark dropdown. It will call displayhoteldetails function based on selection
    for hotel in BookmarkedHotelsObjectsList:
        if(hotel.Name == selectedbookmark):
            DisplayHotelDetails(hotel)
            break
    BookmarkNameVariable.set("Bookmarks") #reset bookmark dropdown title

def DisplayWorldCloud():
    currenthotel = None
    currenthotelname = HotelNameLabel.cget("text")
    for hotel in HotelsList:
        if (hotel.Name == currenthotelname):
            currenthotel = hotel

    neg_showWordCloud(currenthotel.neg_WordCloudReview, currenthotelname)
    pos_showWordCloud(currenthotel.pos_WordCloudReview, currenthotelname)

def UpdateRecommendations(hotel): #display recommendations at side of hotel details
    for HotelButton in RecommendedHotelsList: #destroy all buttons recommended before to refresh with new ones
        HotelButton.destroy()

    RecommendedHotelsList.clear()
    for index, otherhotel in enumerate(HotelsList):
        facilitieslist = ', '.join(otherhotel.Facilities)
        if (math.ceil(otherhotel.AverageScore) >= round(hotel.AverageScore) and otherhotel.Name != hotel.Name): #check for hotels with similar of higher star ratings to recommend
            RecoHotelFrame = Frame(master=RecommendationFrame, highlightthickness=2, highlightbackground=Framecolor)
            RecoHotelFrame.grid(row=index + 1, column=0, sticky=N, pady=10, padx=10)
            RecoHotelPicture =  Label(RecoHotelFrame, text="Insert pic", anchor=CENTER)
            RecoHotelPicture.grid(row=0, column=0, sticky=W, padx=10)
            RecoHotelMenuButton = Button(RecoHotelFrame, text=otherhotel.Name + "\n Average Score: %0.1f/5" % otherhotel.AverageScore + "\n Facilities: " + facilitieslist,
                                     command=lambda otherhotel=otherhotel: DisplayHotelDetails(otherhotel), width=26, height=6, font=("Arial", 10), relief=GROOVE, wraplength=200)
            RecoHotelMenuButton.grid(row=0, column=1, sticky=N)
            RecommendedHotelsList.append(RecoHotelFrame)

def FilterReviews(filterby):
    for item in ReviewItemsList:
        item.destroy()
    ReviewItemsList.clear()

    currenthotel = None
    currenthotelname = HotelNameLabel.cget("text")
    for hotel in HotelsList:
        if (hotel.Name == currenthotelname):
            currenthotel = hotel

    filteredvar = 0
    if(filterby == "1 star"):
        filteredvar = 1
    elif(filterby == "2 stars"):
        filteredvar = 2
    elif (filterby == "3 stars"):
        filteredvar = 3
    elif (filterby == "4 stars"):
        filteredvar = 4
    elif (filterby == "5 stars"):
        filteredvar = 5

    # display reviews
    for index, score in enumerate(currenthotel.ScoresList):
        if(filteredvar!= 0 and filteredvar != int(score)):
            continue
        ReviewTextFrame = Frame(master=HotelReviewFrame, highlightbackground=Framecolor, highlightthickness=1)
        ReviewTextFrame.grid(row=index + 2, column=0, sticky=NSEW, pady=5)
        ReviewsScoreText = Label(ReviewTextFrame, text=("Score:", score))
        ReviewsScoreText.grid(row=index, column=0, sticky=NW)
        ReviewsText = Label(ReviewTextFrame, text=currenthotel.ReviewsList[index], justify=LEFT, wraplength=800)
        ReviewsText.grid(row=index + 1, column=0, sticky=NW)
        ReviewItemsList.append(ReviewTextFrame)

# Function to create a bar chart for reviews based on their stars rating
def DisplayBarChart():
    # Getting current hotel
    currenthotel = None
    currenthotelname = HotelNameLabel.cget("text")
    for hotel in HotelsList:
        if (hotel.Name == currenthotelname):
            currenthotel = hotel

    # Categorizing reviews based on their ratings
    one_count, two_count, three_count, four_count, five_count = 0,0,0,0,0
    for score in currenthotel.ScoresList:
        if int(score) == 5:
            five_count += 1
        elif int(score) == 4:
            four_count += 1
        elif int(score) == 3:
            three_count += 1
        elif int(score) == 2:
            two_count += 1
        else:
            one_count += 1

    # Displaying bar chart function from analysis
    showBarChart(one_count, two_count, three_count, four_count, five_count, currenthotelname)

# Function to create and display a pie chart
def DisplayPieChart():
    currenthotel = None
    currenthotelname = HotelNameLabel.cget("text")
    for hotel in HotelsList:
        if (hotel.Name == currenthotelname):
            currenthotel = hotel

    analysis ={}
    analysis["Positive"] = [senti.polarity_scores(i)["pos"] for i in currenthotel.ReviewsList]
    analysis["Negative"] = [senti.polarity_scores(i)["neg"] for i in currenthotel.ReviewsList]
    positive = sum(analysis["Positive"])
    negative = sum(analysis["Negative"])
    showPieChart(positive, negative, currenthotelname)

# Function for Show Analysis Button to display WordCloud, Bar Chart and Pie Chart
def showAnalysis():
    DisplayBarChart()
    DisplayPieChart()

def showEmotions():
    DisplayWorldCloud()

def UpdateScrollbar():
    # scrollbar
    # update size of content for scrollbar
    MainCanvas.update_idletasks()
    MainCanvas.config(scrollregion=MainCanvas.bbox("all"))
    # reset scrollbar position to start
    MainCanvas.xview_moveto(0)
    MainCanvas.yview_moveto(0)

def DisplayMainMenu():
    ClearHotelDetails() #clear hoteldetails gui and show main menu gui
    FilterFrame.grid(row=1, column=0, sticky=N, padx=40)
    MainMenuFrame.grid(row=1, column=1, sticky=NSEW)
    UpdateScrollbar()

#logic for UI display, it clears details and reformats it for the new hotel
def DisplayHotelDetails(hotel):
    ClearHotelDetails() #clear details of old displayed hotel as we will update new one with new widgets
    ClearMainMenu() #clear main menu

    #configure hotel name label
    HotelNameLabel.config(text=hotel.Name)

    # configure hotel address label
    AddressLabel.config(text=("Address: " + hotel.Address))

    facilitieslist = ', '.join(hotel.Facilities)
    FacilitiesLabel.config(text=("Facilities: " + facilitieslist))

    # configure bookmarked status of hotel
    BookmarkedVariable.set(hotel.Bookmarked)

    # configure hotel average score /5
    AverageScoreLabel.config(text= "Star Rating: %0.1f"%hotel.AverageScore+ "/5")

    #configure images
    HotelMainImage.config(text="Main Image")
    HotelSmallImageOne.config(text="Small Image")
    HotelSmallImageTwo.config(text="Small Image")
    HotelSmallImageThree.config(text="Small Image")

    ReviewFilterVariable.set(ReviewFilterOptions[0])

    #display reviews
    for index, score in enumerate(hotel.ScoresList):
        ReviewTextFrame = Frame(master=HotelReviewFrame, highlightbackground=Framecolor, highlightthickness=1)
        ReviewTextFrame.grid(row=index +2, column=0, sticky=NSEW, pady=5)
        ReviewsScoreText = Label(ReviewTextFrame, text=("Score:",score))
        ReviewsScoreText.grid(row=index, column=0, sticky=NW)
        ReviewsText = Label(ReviewTextFrame, text=hotel.ReviewsList[index], justify=LEFT, wraplength=800)
        ReviewsText.grid(row=index+1, column=0, sticky=NW)
        ReviewItemsList.append(ReviewTextFrame)

    UpdateRecommendations(hotel) #call update recommendations to show hotels with similar or better ratings
    #show widgets needed for hotel details
    RecommendationFrame.grid(row=1, column=0, padx=40, sticky=N)
    HotelDetailsFrame.grid(row=1, column=1, sticky=N)
    MenuButton.grid(row=0, column=0, padx=35, pady=20, sticky=NE)
    UpdateScrollbar()

## Create a add hotel pop up
def open_popup():
    global top
    top = Toplevel(MainFrame)
    top.geometry("500x150")
    top.title("Add hotel")
    Label(top, text= "Enter the url of the hotel below! (Tripadvisor.com or booking.com)", font=('Arial')).grid(row=0,column=3)
    global input_text
    input_text = Entry(top, width=40, textvariable="url")
    input_text.focus_set()
    input_text.grid(row=2, column=3)
    #input_text = Text(top, height=3, width=50)
    #input_text.grid(row=2, column=3)
    Ok_button = Button(top, text = "Ok add it!", command= take_input)
    Ok_button.grid(row=3, column=3)

## Take string input and start webscrapping
def take_input(*args):
    url = input_text.get()
    print(url)
    urlchecker(url)
    top.destroy()
    RefreshHotels()

## Click on button
def on_button():
    take_input()

#####Main code#####
Bookmarkdata = UpdateBookmarkCsv()
#Bookmarkdata = pd.read_csv("Bookmarks.csv", index_col=0)
for i in HotelOptions:
    #create hotels
    ReviewsList = list(HotelCSVData.loc[HotelCSVData.name == i, 'reviews.text'])
    ReviewsRating = list(HotelCSVData.loc[HotelCSVData.name == i, 'reviews.rating'])
    AddressList =  HotelCSVData.loc[HotelCSVData.name == i, 'address']
    Address = AddressList.iat[0]
    Bookmarked = Bookmarkdata.loc[Bookmarkdata.name == i, 'bookmarked'].bool()
    HotelObject = Hotel(i, Address, ReviewsRating, ReviewsList, Bookmarked)
    HotelsList.append(HotelObject)
    if(Bookmarked == True):
        BookmarkedHotelsObjectsList.append(HotelObject)
        BookmarkedHotelsNameList.append(HotelObject.Name)

##########UI LAYOUT##########
#frame for entire window
MainFrame = Frame(master=window)
MainFrame.pack(side=LEFT, fill=BOTH, expand=1)

#canvas for everything
MainCanvas = Canvas(master=MainFrame)
VScrollbar = Scrollbar(master=MainFrame, orient=VERTICAL, command=MainCanvas.yview)
HScrollbar = Scrollbar(master=MainFrame, orient=HORIZONTAL, command=MainCanvas.xview)
#scrollbar
VScrollbar.pack(side=RIGHT, fill=Y)
HScrollbar.pack(side=BOTTOM, fill=X)

#Frame for everything
ContentFrame= Frame(MainCanvas)
MainCanvas.create_window((0,0), window=ContentFrame, anchor=NW)
MainCanvas.configure(xscrollcommand=HScrollbar.set, yscrollcommand=VScrollbar.set)
MainCanvas.pack(side=LEFT, fill=BOTH, expand=1)

#App Title
AppTitleLabel = Label(ContentFrame, text="HOTELOPOLIS", font=("Arial", 40), anchor=CENTER)
AppTitleLabel.grid(row=0, column=1)

#Filter frame for filtering hotels
# Function for scrolling using mouse wheel
def _on_mouse_wheel(event):
    MainCanvas.yview_scroll(-1 * int((event.delta / 120)), "units")

#Mouse Wheel scrolling implementation
MainCanvas.configure(yscrollcommand=VScrollbar.set)
MainCanvas.bind('<Configure>', lambda e: MainCanvas.configure(scrollregion = MainCanvas.bbox("all")))
MainCanvas.bind_all("<MouseWheel>", _on_mouse_wheel)

#Filter Frame
FilterFrame = Frame(master=ContentFrame, highlightbackground=Framecolor, highlightthickness=1, padx=10, pady=5)

#Recommendation frame for recommended hotels
RecommendationFrame = Frame(master=ContentFrame, highlightbackground=Framecolor, highlightthickness=1,padx=10, pady=5)

#Frame for hotel buttons, hotel info
MainMenuFrame = Frame(master=ContentFrame)

#Frame for hotel details
HotelDetailsFrame = Frame(master=ContentFrame, highlightbackground=Framecolor, highlightthickness=1,padx=10, pady=5)

#Frame for display of hotel name, and bookmark and address
HotelDescriptionFrame = Frame(master=HotelDetailsFrame)
HotelDescriptionFrame.grid(row=0, column=0, sticky=NW)

#Frame for display of hotel name, and bookmark
HotelNameFrame = Frame(master=HotelDescriptionFrame)
HotelNameFrame.grid(row=0, column=0, sticky=NW)

#Frame for reviews
HotelPicturesFrame =  Frame(master=HotelDetailsFrame, highlightbackground=Framecolor, highlightthickness=1, padx=10, pady=5)
HotelPicturesFrame.grid(row=1, column=0, sticky=NSEW)

#Frame for main picture
HotelMainPictureFrame =  Frame(master=HotelPicturesFrame, padx=10)
HotelMainPictureFrame.grid(row=0, column=0, sticky=NW)

#Frame for small pictures
HotelSmallPicturesFrame =  Frame(master=HotelPicturesFrame)
HotelSmallPicturesFrame.grid(row=0, column=1, sticky=NW)

#Hotel Main Image Label
HotelMainImage = Label(HotelMainPictureFrame, text="Main pic", width=30, height=20, highlightbackground=Framecolor, highlightthickness=1, padx=10, pady=10)
HotelMainImage.grid(row=0, column=0)

#Hotel Main Image Label
HotelSmallImageOne = Label(HotelSmallPicturesFrame, text="small pic", width=10, height=10, highlightbackground=Framecolor, highlightthickness=1, padx=10, pady=10)
HotelSmallImageOne.grid(row=0, column=0)

#Hotel Main Image Label
HotelSmallImageTwo = Label(HotelSmallPicturesFrame, text="small pic", width=10, height=10, highlightbackground=Framecolor, highlightthickness=1, padx=10, pady=10)
HotelSmallImageTwo.grid(row=1, column=0)

#Hotel Main Image Label
HotelSmallImageThree = Label(HotelSmallPicturesFrame, text="small pic", width=10, height=10, highlightbackground=Framecolor, highlightthickness=1, padx=10, pady=10)
HotelSmallImageThree.grid(row=1, column=1)

#Frame for reviews
HotelReviewFrame =  Frame(master=HotelDetailsFrame)
HotelReviewFrame.grid(row=2, column=0, sticky=NW)

#Recommendation Label
RecommendationLabel = Label(RecommendationFrame, text="You may also like:")
RecommendationLabel.grid(row=0, column=0)

#Back to menu Button
MenuButton = Button(ContentFrame, text="Back To Menu", command=DisplayMainMenu,font=("Arial", 10), relief=GROOVE)

#Hotel name label in display hotel options
HotelNameLabel = Label(HotelNameFrame, font=("Arial", 30))
HotelNameLabel.grid(row=0, column=0, sticky=NW)

#Address label in display hotel options
AddressLabel = Label(HotelDescriptionFrame, text="",font=("Arial", 15))
AddressLabel.grid(row=1, column=0, sticky=NW)

# heading for reviews
ReviewListHeaderText = Label(HotelReviewFrame, text="Reviews", font=("Arial", 15))
ReviewListHeaderText.grid(row=0, column=0, sticky=NW)

#Address label in display hotel options
FacilitiesLabel = Label(HotelDescriptionFrame, text="",font=("Arial", 15))
FacilitiesLabel.grid(row=2, column=0, sticky=NW)

#average star review label in display hotel options
AverageScoreLabel = Label(HotelDescriptionFrame, text="",font=("Arial", 15))
AverageScoreLabel.grid(row=3, column=0, sticky=NW)

#No results label in main menu
Emptylabel = Label(MainMenuFrame,text="No Results", font=("Arial", 20), width=37)

#filter options
FilterLabel = Label(FilterFrame, text="Filters", font=("Arial", 20))
#filter variables
restaurant=StringVar()
jacuzzi=StringVar()
pool=StringVar()
gym=StringVar()
spa=StringVar()

#Label for ameneties and checkboxes for ameneties filter
AmenitiesLabel = Label(FilterFrame, text="Amenities", font=("Arial", 15))
RestaurantFilter = Checkbutton(FilterFrame, text="Restaurant", variable=restaurant, onvalue="restaurant", offvalue= "na", command=FilterAndSortHotelDetails)
JacuzziFilter = Checkbutton(FilterFrame, text="Jacuzzi", variable=jacuzzi, onvalue="jacuzzi", offvalue= "na", command=FilterAndSortHotelDetails)
PoolFilter = Checkbutton(FilterFrame, text="Pool", variable=pool, onvalue="pool", offvalue= "na", command=FilterAndSortHotelDetails)
GymFilter = Checkbutton(FilterFrame, text="Gym", variable=gym, onvalue="gym", offvalue= "na", command=FilterAndSortHotelDetails)
SpaFilter = Checkbutton(FilterFrame, text="Spa", variable=spa, onvalue="spa", offvalue= "na", command=FilterAndSortHotelDetails)
#deselect checkboxes, string var makes it select by default
RestaurantFilter.deselect()
JacuzziFilter.deselect()
PoolFilter.deselect()
GymFilter.deselect()
SpaFilter.deselect()

#Label for ratings and checkboxes for rating filter
RatingLabel = Label(FilterFrame, text="Rating", font=("Arial", 15))
onestar=IntVar()
twostar=IntVar()
threestar=IntVar()
fourstar=IntVar()
fivestar=IntVar()

OneStarFilter = Checkbutton(FilterFrame, text="1 Star", variable=onestar, onvalue=1, offvalue= 0, command=FilterAndSortHotelDetails)
TwoStarFilter = Checkbutton(FilterFrame, text="2 Stars", variable=twostar, onvalue=2, offvalue= 0, command=FilterAndSortHotelDetails)
ThreeStarFilter = Checkbutton(FilterFrame, text="3 Stars", variable=threestar, onvalue=3, offvalue= 0, command=FilterAndSortHotelDetails)
FourStarFilter = Checkbutton(FilterFrame, text="4 Stars", variable=fourstar, onvalue=4, offvalue= 0, command=FilterAndSortHotelDetails)
FiveStarFilter = Checkbutton(FilterFrame, text="5 Stars", variable=fivestar, onvalue=5, offvalue= 0, command=FilterAndSortHotelDetails)

FilterLabel.grid(row=0, column=0, sticky=W)
AmenitiesLabel.grid(row=1, column=0, sticky=W)
RestaurantFilter.grid(row=2, column=0, sticky=W)
JacuzziFilter.grid(row=3, column=0, sticky=W)
PoolFilter.grid(row=4, column=0, sticky=W)
GymFilter.grid(row=5, column=0, sticky=W)
PoolFilter.grid(row=6, column=0, sticky=W)
SpaFilter.grid(row=7, column=0, sticky=W)
RatingLabel.grid(row=8, column=0, sticky=W)
OneStarFilter.grid(row=9, column=0, sticky=W)
TwoStarFilter.grid(row=10, column=0, sticky=W)
ThreeStarFilter.grid(row=11, column=0, sticky=W)
FourStarFilter.grid(row=12, column=0, sticky=W)
FiveStarFilter.grid(row=13, column=0, sticky=W)

# set default value of sort
SortVariable = StringVar()
SortVariable.set(MainMenuSortOptions[0])
#add frame for sort widgets
SortFrame = Frame(master=MainMenuFrame, padx=10, pady=5)
SortFrame.grid(row=0, column=0, sticky=NW)
SortLabel = Label(SortFrame, text="Sort by:", font=("Arial", 14))
SortLabel.grid(row=0, column=0, sticky=NW)
#create sort dropdown
SortDropdown = OptionMenu(SortFrame, SortVariable, *MainMenuSortOptions, command=FilterAndSortHotelDetails)
SortDropdown.grid(row=0, column=1, sticky=NW)
add_hotel = Button(MainMenuFrame, text = "Add Hotel", command= open_popup)
add_hotel.grid(row=0, column=0, sticky=E, padx=10)

#set default value for bookmark widget
BookmarkNameVariable = StringVar()
BookmarkNameVariable.set("Bookmarks")
if (len(BookmarkedHotelsNameList) == 0): #give default list of bookmarks no bookmarks if no bookmarked hotels
    BookmarkedHotelsNameList.append("No Bookmarks")
# create bookmark dropdown
BookmarkDropdown = OptionMenu(ContentFrame, BookmarkNameVariable, *BookmarkedHotelsNameList, command=SelectFromBookmark)
BookmarkDropdown.grid(row=0, column=0, sticky=W, padx=40)

#bookmarked checkbutton in display hotel details
BookmarkedVariable = BooleanVar()
BookmarkedButton = Checkbutton(HotelNameFrame, text="Bookmarked", variable=BookmarkedVariable, onvalue=1, offvalue= 0, command=ToggleBookmark)
BookmarkedButton.grid(row=0, column=1, sticky=W)

#Show analysis
ShowAnalysisButton = Button(HotelNameFrame, text = "Show analysis", command=showAnalysis)
ShowAnalysisButton.grid(row=0, column=2)

#Show keywords
ShowEmotionButton = Button(HotelNameFrame, text = "Show keywords", command=showEmotions)
ShowEmotionButton.grid(row=0, column=3)

#add frame for sort widgets
ReviewFilterFrame = Frame(master=HotelReviewFrame, pady=5)
ReviewFilterFrame.grid(row=1, column=0, sticky=NSEW)
ReviewFilterLabel = Label(ReviewFilterFrame, text="Filter by:", font=("Arial", 10))
ReviewFilterLabel.grid(row=0, column=0, sticky=NW)
#create sort dropdown
ReviewFilterVariable = StringVar()
ReviewFilterVariable.set(ReviewFilterOptions[0])
FilterReviewDropdown = OptionMenu(ReviewFilterFrame, ReviewFilterVariable, *ReviewFilterOptions, command=FilterReviews)
FilterReviewDropdown.grid(row=0, column=1, sticky=NW)

#sort and filter main menu based on default values
FilterAndSortHotelDetails(SortVariable)
#show main menu widgets
DisplayMainMenu()
window.mainloop()


# https://www.acmicpc.net/problem/2477

# Unsolved

K = int(input())

field = [[] for _ in range(5)]
for _ in range(6):
    direc, dist = map(int, input().split())
    field[direc].append(dist)

# print(field)

# width = 0
# height = 0
for d in range(1, 5):
    if len(field[d]) == 1:
        if d <= 2:
            width = field[d][0]
            if d % 2:
                small_width = field[d+1][0]
            else:
                small_width = field[d-1][0]
        else:
            height = field[d][0]
            if d % 2:
                small_height = field[d+1][0]
            else:
                small_height = field[d-1][0]
    # else:
    #     if d <= 2:
    #         small_width = field[d][0]
    #     else:
    #         small_height = field[d][0]

# print(small_width, small_height)

# else:
#     if d <= 2:
#         small_width = min(field[d])
#     else:
#         small_height = min(field[d])

print((width*height - small_width*small_height)*K)


'''
This is the main.py module, and is essentially where the "backend"
portion of this project, namely, the server, is created and run.
'''
import sys

from flask import Flask, request, render_template
from flask_graphql import GraphQLView
from flask_cors import CORS
from jinja2 import Template

from pkg.schema import SCHEMA
from pkg.config import ARENA_USERNAME
from pkg.constants import REQUEST_LENGTH, DATA_CHECK
from pkg.html import JINJA_HTML
from pkg.blocks import get_random_blocks
from pkg.db import add_test_data, clear_database
from pkg.mail import create_content, send_email

# starting Flask application, adding graphiql url
APP = Flask('__main__')
APP.add_url_rule(
    '/graphql',
    view_func=GraphQLView.as_view(
        'graphql',
        schema=SCHEMA,
        graphiql=True
    )
)

@APP.before_request
def get_block_data() -> None:
    '''
    description:            this is where block data is downloaded via the
                            are.na API, using functions defined in pkg/

    return:                 None
    '''
    # only get blocks when specific request from UI is called
    if request.method == 'POST' and request.content_length == REQUEST_LENGTH:
        get_random_blocks(3, ARENA_USERNAME)

def get_blocks_for_email() -> None:
    '''
    description:            this is where block data is downloaded via the
                            are.na API, using functions defined in pkg/

    return:                 None
    '''
    get_random_blocks(3, ARENA_USERNAME)

    # execute query to database using GraphQL query
    data = SCHEMA.execute(DATA_CHECK).data['allBlocks']['edges'][1:]
    content = []

    # get data, parse for info on each block
    for block in data:

        block_id = block['node']['blockId']
        block_content = block['node']['blockContent']
        block_channel = block['node']['channelTitle']
        block_add_date = block['node']['blockCreateDate']

        if block['node']['blockType'] == 'Text':
            content.append(create_content(block_id, block_content, 'Text'))
        else:
            content.append(create_content(block_id, block_content, 'Media'))

        content.append(create_content(block_channel, block_add_date, 'Info'))

    # create email content with data
    email_template = Template(JINJA_HTML)
    email_content = email_template.render(
        block1=content[0],
        block1_info=content[1],
        block2=content[2],
        block2_info=content[3],
        block3=content[4],
        block3_info=content[5],
    )

    # send email with final html
    send_email(email_content)

@APP.route('/')
def my_index() -> str:
    '''
    description:            this function renders the index.html file created
                            via React, to the endpoint defined above

    return:                 the rendered index.html file
    '''
    return render_template("index.html")

@APP.teardown_appcontext
def shutdown_session(exception=None) -> None:  # pylint:disable=unused-argument
    '''
    description:            this function shuts the database in
                            DB_SESSION down as part of shutting down the
                            Flask application

    return:                 None
    '''
    clear_database()

if __name__ == '__main__':

    if len(sys.argv) > 1 and sys.argv[1] == '--email':
        # add test data to sqlite3 database
        add_test_data()

        # get data for email
        get_blocks_for_email()

    else:
        # add test data to sqlite3 database
        add_test_data()

        # enable cross origin resource sharing to allow front end access
        CORS(APP, resources={r'/graphql': {'origins': '*'}})

        # run Flask app
        APP.run(host='0.0.0.0', use_reloader=False)
        # APP.run(debug=True, use_reloader=False)


from App.Lib.Client.marina_api import MarinaAPI
from App.Lib.Treat.date_treat import (treat_datetime_to_string,
                                      treat_node_string,
                                      treat_string_to_datetime)
from App.Queues.Schedule.Search.search import Search

class CheckScheduleExists(Search):
    schedule_dates = []

    def handle(self) -> bool:
        if not self.has_schedule():
            return super().handle()

        schedule = self.find_schedule()
        self.set_schedule(schedule)
        return super().handle()

    def has_schedule(self):
        if not self.get_grade().get('schedules'):
            return False

        self.schedule_dates = self.get_schedule_dates()
        return self.get_deadline_to_compare() in self.schedule_dates

    def get_schedule_dates(self):
        schedule_dates = [schedule.get('scheduleDate')
                          for schedule in self.get_grade().get('schedules')]
        return [treat_node_string(date) for date in schedule_dates]

    def get_deadline_to_compare(self):
        deadline = treat_string_to_datetime(self.get_deadline())
        return deadline.replace(hour=0, minute=0)

    def find_schedule(self):
        schedule_date = self.get_deadline_to_compare()
        schedule_index = self.schedule_dates.index(schedule_date)
        return self.get_grade().get('schedules')[schedule_index]


#!/opt/rh/rh-python35/root/usr/bin/python

import sys
import re
from f5.bigip import ManagementRoot

def usage():
    print('\nusage: %s <mode> [ <environment> ] [ <ip> | <pattern> ]' % sys.argv[0])
    print('\n Modes:')
    print('\n  -n               node mode: the lookup is done by all nodes (or specified by IP address) and finds all virtuals servers that use the node(s) in the backend.')
    print('\n  -vs              VIP mode: the lookup is done by virtual server and lists all the nodes for all (or specified by pattern or IP address) virtual servers.')
    print('\n Environments:')
    print('\n                   Valid environments are prod, prod-b and appver. If no environment is specified - by default it is appver.\n')
    sys.exit(0)

env = 'appver'
ip = None
vs = None

options = [ opt for opt in sys.argv[1:] if opt != '-n' and opt != '-vs' ]
mode_list = [ opt for opt in sys.argv[1:] if opt == '-n' or opt == '-vs' ]

if len(options) > 2:
    print('Too many options')
    usage()
    sys.exit(2)

if len(options) > 0:
    for opt in options:
        if opt.lower() == 'prod' or opt.lower() == 'prod-b' or opt.lower() == 'appver':
            env = opt.lower()
        elif re.search('^\d+\.\d+\.\d+\.\d+$', opt):
            ip = opt
        elif opt.lower() == '-h':
            usage()
        else:
            vs = str(opt)

if len(mode_list) != 1:
    print('\n1 mode must be specified: "-n" - for node lookup, "-vs" for VIP lookup')
    usage()
    sys.exit(2)

mode = mode_list[0]

if env == 'appver':
    server = 'servername'
elif env == 'prod':
    server = 'servername'
elif env == 'prod-b':
    server = 'servername'

mgmt = ManagementRoot(server, 'user', 'password')

virtuals = mgmt.tm.ltm.virtuals.get_collection()

if mode == '-vs':
    print('\nEnvironment: ' + env + '\n')
results = {}
for virt in virtuals:
    partition = virt.partition
    try:
        pool_name = virt.pool.split('/')[2]
        pool = mgmt.tm.ltm.pools.pool.load(name = pool_name, partition = partition)
        members = pool.members_s.get_collection()
        if mode == '-vs':
            if vs:
                if re.search('.*%s.*' % vs.lower(), virt.name.lower()):
                    print('\n' + virt.name + ' (' + virt.destination.split('/')[2] + ')')
            elif ip:
                if re.search('^' + ip + '%.*', virt.destination.split('/')[2]):
                    print('\n' + virt.name + ' (' + virt.destination.split('/')[2] + ')')
            else:
                print('\n' + virt.name + ' (' + virt.destination.split('/')[2] + ')')
        for member in members:
            if mode == '-n':
                try:
                    results[member.name.split('%')[0]].append( virt.name + ' (' + virt.destination.split('/')[2] + ')' )
                except KeyError:
                    results[member.name.split('%')[0]] = [ virt.name + ' (' + virt.destination.split('/')[2] + ')' ]
                    continue
            if mode == '-vs':
                if vs:
                    if re.search('.*%s.*' % vs.lower(), virt.name.lower()):
                        print(' ' + member.name.split('%')[0])
                elif ip:
                    if re.search('^' + ip + '%.*', virt.destination.split('/')[2]):
                        print(' ' + member.name.split('%')[0])
                else:
                    print(' ' + member.name.split('%')[0])
    except:
        continue

if mode == '-vs':
    sys.exit()

print('\nEnvironment: ' + env + '\n')
for ke,va in results.items():
    if ip:
        if ip == ke:
            print(ke + ':')
            for vs in va:
                print(' ' + vs)
    else:
        print('\n' + ke + ':')
        for vs in va:
            print(' ' + vs)


from backend.graph_io import json_to_lkg
import networkx as nx

# Compute pagerank score
def set_pagerank_scores_for_all_cases(j=None):
    if not j:
        j = json_to_lkg("LKG.json")
    print("Imported lkg")
    j = j.query(judges=[], subjects=[], keywords=[], judgements=[], types=['case'], year_range=[])
    print("Filtered lkg")
    pagerank_dict = nx.pagerank(j)
    print("pageranked lkg")
    return(pagerank_dict)


#https://medium.com/@g.shevtsov1989/bellman-ford-algorithm-in-python-8f4cbca040ac

class Graph:
    def __init__(self, vertices):
        self.vertices = vertices
        self.edges = []

    def add_edge(self, u, v, weight):
        self.edges.append((u, v, weight))

class BellmanFord:
    def __init__(self, graph, source):
        self.graph = graph
        self.source = source
        self.distances = {vertex: float('inf') for vertex in graph.vertices}
        self.predecessors = {vertex: None for vertex in graph.vertices}

        self.distances[source] = 0

    def run(self):
        for i in range(len(self.graph.vertices) - 1):
            for u, v, weight in self.graph.edges:
                if self.distances[u] + weight < self.distances[v]:
                    self.distances[v] = self.distances[u] + weight
                    self.predecessors[v]=u

        for u, v, weight in self.graph.edges:
            if self.distances[u] + weight < self.distances[v]:
                print("Negative cycle detected")
                return

        print("Shortest distances:", self.distances)

    def get_shortest_path(self, destination):
        path = []
        while destination != self.source:
            path.append(destination)
            destination = self.predecessors[destination]
        path.append(self.source)
        return path[::-1]

if __name__ == '__main__':
    vertices = ['A', 'B', 'C', 'D', 'E']
    graph = Graph(vertices)
    graph.add_edge('A', 'B', 4)
    graph.add_edge('A', 'C', 2)
    graph.add_edge('B', 'C', 3)
    graph.add_edge('B', 'D', 2)
    graph.add_edge('B', 'E', 3)
    graph.add_edge('C', 'B', 1)
    graph.add_edge('C', 'D', 4)
    graph.add_edge('C', 'E', 5)
    graph.add_edge('E', 'D', 1)

    bf = BellmanFord(graph, 'A')
    bf.run()
    print("Shortest path from A to D:", bf.get_shortest_path('D'))