### ----------- External Packages -----------
from gurobipy import *
import itphelper
import datetime
import time
import numpy
import math
import xlsxwriter
import csv
from model_writer import ModelWriter
from model_creation import Model
from price_generation import PriceGenerator
from limit_order_generation import CPRIndicatorGenerator

class ITP_Solver(object):
	"""docstring for Gurobi_Controller"""
	def __init__(self, generate_scenarios=False, generate_random_variables=False, printing_output=False, output_solutions=False, parameter_file_name="Data/104_case_instance.txt"):
		super(ITP_Solver, self).__init__()


		### ----------- Model Initialization -----------
		self.model 									= Model('itp')
		self.model.setParam('OutputFlag', 			1 if output_solutions else 0)
		self.model.params.threads 					= 1 
		self.model.update()
		self.model_id								= "104"

		### ----------- Set System Parameters -----------
		self.generate_scenarios 					= generate_scenarios 																															# If scenarios should be autogenerated or not
		self.generate_random_variables				= generate_random_variables																														# if random variables should be loaded from file or not (only possible to load from models with identical dimensions)
		self.printing_output						= printing_output																																# If optimal variables should be printed in terminal or not
		self.parameter_file_name 					= parameter_file_name	                                            		              														# Directory of the input parameter file if not autogenerated scenarios
		self.scenario_generation_file_name			= "Data/ID" 						+ self.model_id + "_" + "scenario_generation_input.txt"														# Directory of the input parameter file for autogenerated scenarios
		self.variables_file_name					= "Output/VariableFiles/ID" 		+ self.model_id + "_" + datetime.datetime.now().strftime("%Y-%m-%d_%H.%M") + "variables.xlsx"				# Directory of the output file with all the optimal variable values
		self.random_variable_file_name				= "Output/RandomVariableFiles/ID" 	+ self.model_id + "_" + datetime.datetime.now().strftime("%Y-%m-%d_%H.%M") + "random_variables.csv"			# Directory of the folder of the random variables 
		self.printing_mode							= False 																																		# If program progression should be printed or not
		self.start 									= time.time()	
		self.asynchronous_gcs						= True							# Indicates whether we are running in asynchronous gate closure mode or not. Can be set in scenario_generation_file
		self.stages_per_hour                    	= 6
		self.time_of_first_gc						= 0								# Parameter used in the asynchronous gate closure cases
		self.gate_closures							= [self.number_of_trading_stages - self.stages_per_hour * dp for dp in range(self.first_dp, self.last_dp + 1)]
		
		### ----------- Set Model Parameters -----------
		self.number_of_trading_stages 				= 100 
		self.number_of_scenarios					= 20
		self.number_of_production_units 			= 2
		self.number_of_dps 							= 5
		self.number_of_order_levels 				= 5
		self.first_dp 								= 11
		self.last_dp 								= 15

		### ----------- Set Case Parameters -----------
		self.setup_case_parameters()

		### ----------- Set Market Parameters -----------
		self.setup_market_parameters()


		### ----------- Validate The Parameters -----------
		self.validate_parameters()											


	def set_parameters(self, load_parameters=False):
		if(load_parameters == True):
			raise ValueError("Not implemented parameter loading")
		else:
			
			self.INTRAZONAL_ORDER_LEVEL_PRICES_BUY 		= []
			self.INTERZONAL_ORDER_LEVEL_PRICES_BUY 		= []
			
			self.INTRAZONAL_ORDER_LEVEL_PRICES_SELL 	= []
			self.INTERZONAL_ORDER_LEVEL_PRICES_SELL 	= []

			self.INTRAZONAL_ORDER_LEVEL_VOLUMES_BUY 	= []
			self.INTERZONAL_ORDER_LEVEL_VOLUMES_BUY 	= []
			
			self.INTRAZONAL_ORDER_LEVEL_VOLUMES_SELL 	= []
			self.INTERZONAL_ORDER_LEVEL_VOLUMES_SELL 	= []
			
			self.INTERZONAL_SPREADS 					= [0.7, 0.8, 0.8, 0.9, 1.0]
			self.INTRAZONAL_SPREADS 					= [1.4, 1.6, 1.6, 0.9, 1.0]

			self.BM_SPREAD_PLUS 						= 100 
			self.BM_SPREAD_MINUS 						= -100
			



        

	# To do
	def validate_parameters(self):
		return 


    def setup_case_parameters(self):
    	self.INFLOW 								= [100 for x in range(self.number_of_production_units)]
		self.MIN_Q									= 0
		self.MAX_Q 									= 500
		self.R_0			 						= 250
		self.PRODUCTION_COST 						= 0
		self.MIN_STORAGE	 						= 0
		self.MAX_STORAGE	 						= 0


	def is_intrazonal(self,dp,t):
		return dp < t

    def setup_market_parameters(self):
		self.TRANSACTION_COST 						= 0.1
		self.SCENARIO_PROBABILITIES					= [float(1.0/self.number_of_scenarios) for s in range(self.number_of_scenarios)]
		self.SPREAD									= [[self.INTRAZONAL_SPREADS[dp][t] if self.is_intrazonal(dp,t) else self.INTERZONAL_SPREADS[dp][t] if t <= self.gate_closures[dp] else 0 for t in range(self.number_of_trading_stages)] for dp in range(self.first_dp, self.last_dp + 1)]

		
		
		### Stochasticities
		# The computation is done in separate classes; PriceGenerator and CPRIndicatorGenerator
		self.pg 									= PriceGenerator(self.number_of_dps, self.number_of_scenarios, self.number_of_trading_stages, gate_closures=self.gate_closures)
		CPRig_buy 									= CPRIndicatorGenerator(self.number_of_dps, self.number_of_scenarios, self.number_of_trading_stages, "buy")
		CPRig_sell 									= CPRIndicatorGenerator(self.number_of_dps, self.number_of_scenarios, self.number_of_trading_stages, "sell")
		
		# Price processes
		self.PRICE_PROCESSES 						= self.pg.generate_price_processes()

		# Limit order indicators
		self.CPR_BUY_INDICATORS 					= CPRig_buy.get_indicators()
		self.CPR_SELL_INDICATORS 					= CPRig_sell.get_indicators()

		# To do: cross the stochastic parameters to generate complete scenarios
		pass
		
		# Setup price dependent market parameters
		self.ORDER_LEVEL_PRICES_BUY					= [[self.INTRAZONAL_ORDER_LEVEL_PRICES_BUY[dp][t] 	if self.is_intrazonal(dp,t) else self.INTERZONAL_ORDER_LEVEL_PRICES_BUY[dp][t] 		if t <= self.gate_closures[dp] else 0 for t in range(self.number_of_trading_stages)] for dp in range(self.first_dp, self.last_dp + 1)]
		self.ORDER_LEVEL_VOLUMES_BUY				= [[self.INTRAZONAL_ORDER_LEVEL_VOLUMES_BUY[dp][t] 	if self.is_intrazonal(dp,t) else self.INTERZONAL_ORDER_LEVEL_VOLUMES_BUY[dp][t] 	if t <= self.gate_closures[dp] else 0 for t in range(self.number_of_trading_stages)] for dp in range(self.first_dp, self.last_dp + 1)]
		self.ORDER_LEVEL_PRICES_SELL				= [[self.INTRAZONAL_ORDER_LEVEL_PRICES_SELL[dp][t] 	if self.is_intrazonal(dp,t) else self.INTERZONAL_ORDER_LEVEL_PRICES_SELL[dp][t] 	if t <= self.gate_closures[dp] else 0 for t in range(self.number_of_trading_stages)] for dp in range(self.first_dp, self.last_dp + 1)]
		self.ORDER_LEVEL_VOLUMES_SELL				= [[self.INTRAZONAL_ORDER_LEVEL_VOLUMES_SELL[dp][t] if self.is_intrazonal(dp,t) else self.INTERZONAL_ORDER_LEVEL_VOLUMES_SELL[dp][t] 	if t <= self.gate_closures[dp] else 0 for t in range(self.number_of_trading_stages)] for dp in range(self.first_dp, self.last_dp + 1)]
		self.PLUS_IMBALANCE_PRICES					= self.compute_imbalance_prices("plus")
		self.MINUS_IMBALANCE_PRICES					= self.compute_imbalance_prices("minus")


	def compute_imbalance_prices(mode):
		if(mode == "plus"):
			return [[self.price_processes[s][dp][-1] + self.BM_SPREAD_PLUS for dp in range(self.first_dp, self.last_dp + 1)] for s in range(self.number_of_scenarios)]
		else:
			return [[self.price_processes[s][dp][-1] + self.BM_SPREAD_MINUS for dp in range(self.first_dp, self.last_dp + 1)] for s in range(self.number_of_scenarios)]

	def write_variables_to_file(self, filename="ID104_variables"):
		mw = ModelWriter(filename, self.model.getVars())

	def setup_model(self):
		self.create_variables()
		self.setup_constraints()
		self.setup_objective_function()
	
	def create_variables(self):
		# Variable description:
		# pqs: production quantities
		# rls: storage volume levels
		# bbm: buy imbalance market volume
		# sbm: sell imbalance market volume
		# ofs: overflow volumes
		# bvs: buy order volumes
		# svs: sell order volumes
		# bts: buy order transaction volumes
		# sts: sell order transaction volumes

		self.pqs 									= [[[	self.model.addVar(vtype=GRB.CONTINUOUS,		name="productionQuantity_"		+str(dp)+"_"+str(s)+"_X_X_"					+str(p), 	lb=0.0	) for p in range(self.number_of_production_units)] for s in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]  
		self.rls 									= [[	self.model.addVar(vtype=GRB.CONTINUOUS,		name="storageVolume_"			+str(dp)+"_"+str(s)+"_X_X_X", 							lb=0.0	) for s in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]
		self.bbm									= [[	self.model.addVar(vtype=GRB.CONTINUOUS,		name="imbalanceVolumeBuy_"		+str(dp)+"_"+str(s)+"_X_X_X", 							lb=0.0	) for s in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]
		self.sbm									= [[	self.model.addVar(vtype=GRB.CONTINUOUS,		name="imbalanceVolumeSell_"		+str(dp)+"_"+str(s)+"_X_X_X", 							lb=0.0	) for s in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]
		self.ofs 									= [[	self.model.addVar(vtype=GRB.CONTINUOUS,		name="overflow_"				+str(dp)+"_"+str(s)+"_X_X_X",							lb=0.0	) for s in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]
		self.bvs 									= [[[[	self.model.addVar(vtype=GRB.CONTINUOUS, 	name="orderVolumeB_"			+str(dp)+"_"+str(s)+"_"+str(i)+"_"+str(p)	+"_X",		lb=0.0	) for p in range(self.number_of_price_levels)] for j in range(self.number_of_trading_stages)] for i in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]
		self.svs 									= [[[[	self.model.addVar(vtype=GRB.CONTINUOUS, 	name="orderVolumeA_"			+str(dp)+"_"+str(s)+"_"+str(i)+"_"+str(p)	+"_X", 		lb=0.0	) for p in range(self.number_of_price_levels)] for j in range(self.number_of_trading_stages)] for i in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]
		self.bts 									= [[[[	self.model.addVar(vtype=GRB.CONTINUOUS, 	name="transactionVolumeB_"		+str(dp)+"_"+str(s)+"_"+str(i)+"_"+str(p)	+"_X",		lb=0.0	) for p in range(self.number_of_price_levels)] for k in range(self.number_of_trading_stages)] for i in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]   
		self.sts 									= [[[[	self.model.addVar(vtype=GRB.CONTINUOUS, 	name="transactionVolumeA_"		+str(dp)+"_"+str(s)+"_"+str(i)+"_"+str(p)	+"_X",		lb=0.0	) for p in range(self.number_of_price_levels)] for k in range(self.number_of_trading_stages)] for i in range(self.number_of_scenarios)] for dp in range(self.number_of_dps)]   

		

	def setup_constraints(self):
		# Constraint 1

	### ----------- Objective Function -----------
	def setup_objective_function(self):
		# The objective function can be divided into:
		# - Transaction revenues
		# - Production costs
		# - Transaction costs
		# - Imbalance costs

		self.model.setObjective((quicksum(self.scenario_probabilities[s]*self.transaction_volumes[dp][s][i][j][p] * self.price_levels[dp][i][p][s] for p in range(self.number_of_price_levels) for j in range(self.number_of_trading_stages) for i in range(self.number_of_trading_stages) for s in range(self.number_of_scenarios) for dp in range(self.number_of_dps))) 
			- sum(self.scenario_production_costs[x][s]*self.scenario_probabilities[s] * self.production_quantities[dp][s][x] for x in range(self.number_of_production_units) for s in range(self.number_of_scenarios) for dp in range(self.number_of_dps)) 
			- self.transaction_cost*sum(self.cpr[dp][i][p][s] * self.bid_volume[dp][s][i][p] for p in range(self.number_of_price_levels) for i in range(self.number_of_trading_stages) for s in range(self.number_of_scenarios) for dp in range(self.number_of_dps))
			- sum(self.scenario_probabilities[s] * sum(self.bms[dp][s] * self.imbalance_volume[dp][s] for dp in range(self.number_of_dps)) for s in range(self.number_of_scenarios)), GRB.MAXIMIZE)




	def run_model(self):
		self.model.optimize()
	

