import gurobipy as grb
from gurobipy import quicksum
import time
import numpy as np
from functions import *
from scipy.stats import norm

from solvers.CLSP import CLSP

class CLSP_Det(CLSP):
    """
    Classe per la risoluzione del problema CLSP (Capacitated Lot-Sizing Problem) 
    con domanda deterministica e scorte di sicurezza
    """
    def __init__(self,**setting):
        self.name = "CLSP_Det"
        self.setting = setting

    def populate(self, instance, I0):
        """
        Questo metodo, a partire dai dati di input della classe, popola il modello definendone 
        variabili, vincoli e funzione obiettivo
        """

        #parametri del modello (costi, capacità massima)
        h=instance.h
        f=instance.f
        proc_time=instance.proc_time
        R=instance.R
        setup_times=instance.setup_times
        num_items=instance.num_items
        time_periods=instance.time_periods
        mean=instance.mean
        sd=instance.sd
        alpha=instance.alpha

        #CALCOLO DELLE SCORTE DI SICUREZZA PER OGNI PRODOTTO
        SS=[]
        for i in range(num_items):
            quant=norm.ppf(alpha, loc=mean[i], scale=sd[i])
            SS.append(quant-mean[i])
        
                
        model = grb.Model(self.name)
        
        #VARIABILI DEL MODELLO
        
        y = model.addMVar(
            shape=(num_items,time_periods),
            lb=0,
            vtype=grb.GRB.CONTINUOUS,
            name="y"
        )
        
        self.y=y
        
        
        I = model.addMVar(
            shape=(num_items,time_periods),
            vtype=grb.GRB.CONTINUOUS,
            name="I"
        )
        
        self.I=I
        
        s = model.addMVar(
            shape=(num_items,time_periods),
            vtype=grb.GRB.BINARY,
            name="s"
        )
        
        self.s=s
        
        #VINCOLI DEL MODELLO
        
        #BILANCIO MAGAZZINO
        model.addConstrs(I[(i,0)]==I0[i]+y[(i,0)]-mean[(i)] for i in range(num_items))
        model.addConstrs(I[(i,t)]==I[(i,t-1)]+y[(i,t)]-mean[(i)] for i in range(num_items) for t in range(1,time_periods))

        #MASSIMA QUANTITA' PRODUZIONE
        model.addConstrs(y[(i,t)]<=(time_periods-t)*(mean[i])*s[(i,t)] for i in range(num_items) for t in range(time_periods))

        #MASSIMA DISPONIBILITA' TEMPO
        model.addConstrs(quicksum(proc_time[i]*y[(i,t)] for i in range(num_items))+quicksum(setup_times[i]*s[(i,t)] for i in range(num_items))<=R for t in range(time_periods))

        #SCORTE DI SICUREZZA MAGAZZINO
        model.addConstrs(I[i,t]>=SS[i] for i in range(num_items) for t in range(time_periods))
        
        #FUNZIONE OBIETTIVO DEL MODELLO
        
        model.setObjective(quicksum(quicksum(h[i]*I[(i,t)]+f[i]*s[(i,t)] for t in range(time_periods)) for i in range(num_items)), grb.GRB.MINIMIZE)
        
        model.update()
        return model
    

    def populate2(self, instance, I0):
        """
        Questo metodo, a partire dai dati di input della classe, popola il modello definendone 
        variabili, vincoli e funzione obiettivo, nel caso in cui le scorte di
        sicurezza rendano il problema unfeasible. Si ridefinisce quindi il problema
        deterministico, richiedendo il livello di magazzino semplicemente maggiore
        di zero
        """

        #parametri del modello (costi, capacità massima)
        h=instance.h
        f=instance.f
        proc_time=instance.proc_time
        R=instance.R
        setup_times=instance.setup_times
        num_items=instance.num_items
        time_periods=instance.time_periods
        mean=instance.mean
        sd=instance.sd
        alpha=instance.alpha
        
                
        model = grb.Model(self.name)
        
        #VARIABILI DEL MODELLO
        
        y = model.addMVar(
            shape=(num_items,time_periods),
            lb=0,
            vtype=grb.GRB.CONTINUOUS,
            name="y"
        )
        
        self.y=y
        
        
        I = model.addMVar(
            shape=(num_items,time_periods),
            vtype=grb.GRB.CONTINUOUS,
            lb=0,
            name="I"
        )
        
        self.I=I
        
        s = model.addMVar(
            shape=(num_items,time_periods),
            vtype=grb.GRB.BINARY,
            name="s"
        )
        
        self.s=s
        
        #VINCOLI DEL MODELLO
        
        #BILANCIO MAGAZZINO
        model.addConstrs(I[(i,0)]==I0[i]+y[(i,0)]-mean[(i)] for i in range(num_items))
        model.addConstrs(I[(i,t)]==I[(i,t-1)]+y[(i,t)]-mean[(i)] for i in range(num_items) for t in range(1,time_periods))

        #MASSIMA QUANTITA' PRODUZIONE
        model.addConstrs(y[(i,t)]<=(time_periods-t)*(mean[i])*s[(i,t)] for i in range(num_items) for t in range(time_periods))

        #MASSIMA DISPONIBILITA' TEMPO
        model.addConstrs(quicksum(proc_time[i]*y[(i,t)] for i in range(num_items))+quicksum(setup_times[i]*s[(i,t)] for i in range(num_items))<=R for t in range(time_periods))
        
        #FUNZIONE OBIETTIVO DEL MODELLO
        
        model.setObjective(quicksum(quicksum(h[i]*I[(i,t)]+f[i]*s[(i,t)] for t in range(time_periods)) for i in range(num_items)), grb.GRB.MINIMIZE)
        
        model.update()
        return model

    def get_solution(self, instance, model, I0, time_limit=None, gap=None, verbose=False):
        """
        Questo metodo risolve un modello definito, restituendo il valore della funzione obiettivo,
        la decisione di produzione all'istante iniziale, le corrispondenti variabili di set up,
        il tempo di risoluzione del modello e il valore del gap della soluzione trovata"""
        
        #gap (per criterio di arresto) del problemo misto intero
        if gap:
            model.setParam('MIPgap', gap)
        #tempo limite per la risoluzione del problemo misto intero
        if time_limit:
            model.setParam(grb.GRB.Param.TimeLimit, time_limit)
        #opzione per avere un output che mostra i dettagli della risoluzione
        #del modello o meno
        if verbose:
            model.setParam('OutputFlag', 1)
        else:
            model.setParam('OutputFlag', 0)
        if verbose:
            print ('Solving a model with: '+str(model.NumConstrs)+' constraints')
            print ('    and: ' +str(model.NumVars)+ ' variables')
        
        #risoluzione del modello e tempo computazionale

        start = time.time()
        model.optimize()
        end = time.time()
        comp_time = end - start
        
        num_items = instance.num_items

        #NEL CASO IN CUI LE SCORTE DI SICUREZZA RENDANO IL PROBLEMA INFEASIBLE
        #SI RIDEFINISCE IL MODELLO SENZA DI ESSE (LIVELLO DI MAGAZZINO I>=0)

        if model.status == grb.GRB.INFEASIBLE:
            model.reset()
            model = self.populate2(instance, I0)


            start = time.time()
            model.optimize()
            end = time.time()
            comp_time = end - start

            #CALCOLO DELLE DECISIONI INIZIALI (QUANTITA' PRODOTTE PER 
            # OGNI ITEM AL TEMPO 0 Y E RELATIVE VARIABILI DI SETUP S)

            Y=[]
            S=[]

            for i in range(num_items):
                Y.append(self.y[i,0].X)
                if Y[i]>0:
                    S.append(1)
                else:
                    S.append(0)

            #VALORE DELLA FUNZIONE OBIETTIVO          
            of = model.getObjective().getValue()

            #VALORE DEL GAP ALLA SOLUZIONE TROVATA
            gap=model.MIPGap


        #CASO DI MODELLO CON SCORTE DI SICUREZZA FEASIBLE
        else:
            #CALCOLO DELLE DECISIONI INIZIALI (QUANTITA' PRODOTTE PER 
            # OGNI ITEM AL TEMPO 0 Y E RELATIVE VARIABILI DI SETUP S)

            Y=[]
            S=[]

            for i in range(num_items):
                Y.append(self.y[i,0].X)
                if Y[i]>0:
                    S.append(1)
                else:
                    S.append(0)

            #VALORE DELLA FUNZIONE OBIETTIVO          
            of = model.getObjective().getValue()

            #VALORE DEL GAP ALLA SOLUZIONE TROVATA
            gap=model.MIPGap
          
        model.reset()


        return of, Y, S, comp_time,gap


    def solve(
        self, instance, I0, time_limit=None, gap=None, verbose=False
    ):
        """
        Questo è il metodo dove il problema viene definito e risolto
        :param instance: dizionario contenente tutti i parametri necessari alla definizione del modello
        :I0: valore iniziale del magazzino
        :param time_limit: per interrompere il solver gurobi dopo "time_limit" secondi; se non è presente
        il solver si interrompe quando trova una soluzione 'distante meno di' "gap" dalla 
        soluzione ottima
        :param gap: il solver gurobi si ferma quando trova una soluzione 'distante meno di' "gap"
        dalla soluzione ottima; il valore standard è 0.0001
        :param verbose: parametro (True/False) da passare per avere un output che stampa i dettagli della 
        soluzione del modello o meno
        :return: valore della funzione obiettivo, decisioni al primo stadio (Y e S), tempo di calcolo e
        gap della soluzione trovata da quella ottima
        """
        model = self.populate(instance, I0)
        return self.get_solution(instance,model,I0,time_limit=time_limit, gap=gap, verbose=verbose)