'''
This file contains the EV class.
'''

import numpy as np
import warnings
import math
from typing import Tuple, Union


class EV():
    '''
     which is used to represent the EVs in the environment.
    The two-stage battery model was adapted from https://github.com/zach401/acnportal/blob/master/acnportal/acnsim/models/battery.py#L186


    Attributes:
        - id: unique identifier of the EV (uniquep per charging station)
        - location: the location of the EV (charging station id)
        - battery_capacity_at_arrival: the battery capacity of the EV at arrival time in kWh
        - time_of_arrival: the time of arrival of the EV in the charging station in simulation timesteps
        - time_of_departure: the earliest time of departure of the EV in the charging station in simulation timesteps (if use_probabilistic_time_of_departure is False, then time_of_departure is equal to time_of_departure)
        - desired_capacity: the desired capacity of the EV at departure time in kWh
        - use_probabilistic_time_of_departure: whether the EV will use a probabilistic time of departure or not
        - battery_capacity: the battery capacity of the EV in kWh
        - min_desired_capacity: the minimum desired capacity of the EV in kWh to maximize battery life
        - max_desired_capacity: the maximum desired capacity of the EV in kWh to maximize battery life
        - charge_efficiency: the efficiency of the EV when charging
        - discharge_efficiency: the efficiency of the EV when discharging        
        - timescale: the timescale of the simulation (useful for determining the charging speed)

    Status variables:
        - current_capacity: the current battery capacity of the EV in kWh
        - current_energy: the current power input of the EV in kW (positive for charging, negative for discharging)
        - charging_cycles: the number of charging/discharging cycles of the EV (useful for determining battery life parameters)
        - previous_power: the power input of the EV in the previous timestep in kW (positive for charging, negative for discharging)

    Methods:
        - step: updates the EV status according to the actions taken by the EV charger
        - _charge: charges the EV
        - _discharge: discharges the EV        

    '''

    def __init__(self,
                 id,
                 location,
                 battery_capacity_at_arrival,
                 time_of_arrival,
                 time_of_departure,
                 desired_capacity=None,  # kWh
                 battery_capacity=50,  # kWh
                 min_battery_capacity=10,  # kWh
                 min_emergency_battery_capacity=25,  # kWh
                 max_ac_charge_power=22,  # kW
                 min_ac_charge_power=0,  # kW
                 max_dc_charge_power=50,  # kW
                 max_discharge_power=-22,  # kW
                 min_discharge_power=0,  # kW
                 ev_phases=3,
                 transition_soc=0.8,
                 transition_soc_multiplier=1,
                 charge_efficiency=1, # can be a list of charge efficiencies for different current levels
                 discharge_efficiency=1, # can be a list of discharge efficiencies for different current levels
                 timescale=5,
                 ):

        self.id = id
        self.location = location
        self.timescale = timescale

        # EV simulation characteristics
        self.time_of_arrival = time_of_arrival
        self.time_of_departure = time_of_departure
        self.desired_capacity = battery_capacity if desired_capacity is None else desired_capacity
        self.battery_capacity_at_arrival = battery_capacity_at_arrival  # kWh

        # EV technical characteristics
        self.battery_capacity = battery_capacity  # kWh
        self.min_battery_capacity = min_battery_capacity  # kWh
        self.min_emergency_battery_capacity = min_emergency_battery_capacity  # kWh
        self.max_ac_charge_power = max_ac_charge_power  # kW
        self.min_ac_charge_power = min_ac_charge_power  # kW
        self.max_discharge_power = max_discharge_power  # kW
        self.min_discharge_power = min_discharge_power  # kW
        self.max_dc_charge_power = max_dc_charge_power  # kW
        self.transition_soc = transition_soc
        self.transition_soc_multiplier = transition_soc_multiplier
        self.ev_phases = ev_phases

        self.charge_efficiency = charge_efficiency
        self.discharge_efficiency = discharge_efficiency

        # EV status
        self.current_capacity = battery_capacity_at_arrival  # kWh
        self.prev_capacity = self.current_capacity
        self.current_energy = 0  # kW
        self.actual_current = 0  # A
        self.charging_cycles = 0
        self.previous_power = 0
        self.required_energy = self.battery_capacity - self.battery_capacity_at_arrival
        self.total_energy_exchanged = 0
        self.max_energy_AFAP = 0
        # timesteps that the EV is discharged below the minimum emergency battery capacity
        self.min_emergency_battery_capacity_metric = 0

        # Baterry degradation
        self.abs_total_energy_exchanged = 0
        self.historic_soc = []
        self.active_steps = []

        self.calendar_loss = 0
        self.cyclic_loss = 0

    def reset(self):
        '''
        The reset method is used to reset the EV's status to the initial state.
        '''
        self.current_capacity = self.battery_capacity_at_arrival
        self.prev_capacity = self.current_capacity
        self.current_energy = 0
        self.actual_current = 0
        self.charging_cycles = 0
        self.previous_power = 0
        self.required_energy = self.battery_capacity - self.battery_capacity_at_arrival
        self.total_energy_exchanged = 0
        self.c_lost = 0
        self.max_energy_AFAP = 0
        self.min_emergency_battery_capacity_metric = 0

        self.abs_total_energy_exchanged = 0
        self.historic_soc = []
        self.active_steps = []

        self.calendar_loss = 0
        self.cyclic_loss = 0

    def step(self, amps, voltage, phases=1, type='AC') -> Tuple[float, float]:
        '''
        The step method is used to update the EV's status according to the actions taken by the EV charger.
        Inputs:
            - action: the power input in kW (positive for charging, negative for discharging)
        Outputs:
            - self.current_energy: the current power input of the EV in kW (positive for charging, negative for discharging)
            - self.actual_curent: the actual current input of the EV in A (positive for charging, negative for discharging)
        '''

        if type == 'DC':
            raise NotImplementedError

        if amps > 0 and amps < self.min_ac_charge_power*1000/(voltage*math.sqrt(phases)):
            amps = 0
        elif amps < 0 and amps > self.min_discharge_power*1000/(voltage*math.sqrt(phases)):
            amps = 0

        self.historic_soc.append(self.get_soc())

        if amps == 0:
            self.current_energy = 0
            self.actual_current = 0

            self.active_steps.append(0)
            return 0, 0

        # If the action is different than the previous action, then increase the charging cycles
        if self.previous_power == 0 or (self.previous_power/amps) < 0:
            self.charging_cycles += 1

        phases = min(phases, self.ev_phases)

        if amps > 0:
            self.actual_current = self._charge(amps, voltage, phases)
        elif amps < 0:
            self.actual_current = self._discharge(amps, voltage, phases)

        self.previous_power = self.current_energy

        self.total_energy_exchanged += self.current_energy  # * self.timescale / 60
        # * self.timescale / 60
        self.abs_total_energy_exchanged += abs(self.current_energy)

        # round up to the nearest 0.01 the current capacity
        self.current_capacity = self.my_ceil(self.current_capacity, 2)

        self.active_steps.append(1 if self.actual_current != 0 else 0)
        return self.current_energy, self.actual_current

    def my_ceil(self, a, precision=2):
        return np.true_divide(np.ceil(a * 10**precision), 10**precision)

    def is_departing(self, timestep) -> Union[float, None]:
        '''
        The is_departing method is used to determine whether the EV is departing or not.
        Inputs:
            - timestep: the current timestep of the simulation
        Outputs:
            - Returns the user satisfaction of the EV in departing else None
        '''
        if timestep < self.time_of_departure:
            return None

        return self.get_user_satisfaction()

    def get_user_satisfaction(self) -> float:
        '''
        A function that returns the user satisfaction of the EV when departing.
        Outputs: 
            - Score: a value between 0 and 1
        '''

        if self.current_capacity < self.desired_capacity - 0.001:
            return self.current_capacity / self.desired_capacity
        else:
            return 1

    def min_SoC_when_discharging_metric(self) -> float:
        """
        This metric monitors how often the EV is discharged below the minimum state of charge (SoC) threshold.        
        It is calculated in every step of the simulation and is used to determine the EV's satisfaction metric.
        """
        return 1 if self.current_capacity >= self.min_emergency_battery_capacity else 0

    def get_soc(self) -> float:
        '''
        A function that returns the state of charge of the EV.
        Outputs: 
            - SoC: the state of charge of the EV in [0,1]
        '''
        return (self.current_capacity/self.battery_capacity)

    def __str__(self):
        return f' {self.current_energy*60/self.timescale :5.1f} kWh |' + \
            f' {(self.current_capacity/self.battery_capacity)*100:5.1f} % |' + \
            f't_stay: {self.time_of_arrival}-' + \
            f'{self.time_of_departure} |' + \
            f' {self.max_ac_charge_power}/' + \
            f'{self.max_discharge_power} kWh|' + \
            f' {self.battery_capacity} kW |'

    def _charge(self, amps, voltage, phases=1) -> float:

        assert (amps > 0)
        # given_power = (amps * voltage / 1000) * \
        #     self.charge_efficiency * self.timescale / 60  # KW

        # if self.current_capacity + given_power > self.battery_capacity:
        #     self.current_energy = self.battery_capacity - self.current_capacity
        #     self.current_capacity = self.battery_capacity
        # else:
        #     self.current_energy = given_power
        #     self.current_capacity += given_power

        """ Method to "charge" the battery based on a two-stage linear
        battery model.
        
        Battery model based on a piecewise linear approximation of battery behavior. The battery will charge at the
        minimum of max_rate and the pilot until it reaches _transition_soc. After this, the maximum charging rate of the
        battery will decrease linearly to 0 at 100% state of charge.

        For more info on model: https://www.sciencedirect.com/science/article/pii/S0378775316317396

        All calculations are done in terms fo battery state of charge
        (SoC). Results are converted back to power units at the end.
        Code adapted from: https://github.com/zach401/acnportal/blob/master/acnportal/acnsim/models/battery.py#L186

        Args:
            pilot (float): Pilot signal passed to the battery. [A]
            voltage (float): AC voltage provided to the battery
                charger. [V]
            period (float): Length of the charging period. [minutes]

        Returns:
            float: average charging rate of the battery over this single
                period.

        """

        pilot = amps
        voltage = voltage * math.sqrt(phases)
        period = self.timescale
        # All calculations are done in terms of battery SoC, so we
        # convert pilot signal and max power into pilot and max rate of
        # change of SoC.
        
        # if charge efficeincy is dict, then get the charge efficiency based on the current
        # current level
        if isinstance(self.charge_efficiency, dict):
            charge_efficiency = self.charge_efficiency.get(np.round(amps), 1)/100
        else:
            charge_efficiency = self.charge_efficiency
        
        pilot_dsoc = charge_efficiency * pilot * voltage / 1000 / \
            self.battery_capacity / (60 / period)
        max_dsoc = charge_efficiency * self.max_ac_charge_power / \
            self.battery_capacity / (60 / period)

        if pilot_dsoc > max_dsoc:
            pilot_dsoc = max_dsoc

        if self.transition_soc == 1:
            curr_soc = pilot_dsoc + self.get_soc()
            if curr_soc > 1:
                curr_soc = 1

        else:

            # The pilot SoC rate of change has a new transition SoC at
            # which decreasing of max charging rate occurs.
            pilot_transition_soc = self.transition_soc + (
                pilot_dsoc - max_dsoc
            ) / max_dsoc * (self.transition_soc - 1)

            # The charging equation depends on whether the current SoC of
            # the battery is above or below the new transition SoC.
            if self.get_soc() < pilot_transition_soc:
                # In the pre-rampdown region, the charging equation changes
                # depending on whether charging the battery over this
                # time period causes the battery to transition between
                # charging regions.
                if 1 <= (pilot_transition_soc - self.get_soc()) / pilot_dsoc:
                    new_soc = pilot_dsoc + self.get_soc()
                else:
                    new_soc = 1 + np.exp(
                        self.transition_soc_multiplier *
                        (pilot_dsoc + self.get_soc() - pilot_transition_soc)
                        / (pilot_transition_soc - 1)
                    ) * (pilot_transition_soc - 1)
            else:
                new_soc = 1 + np.exp(self.transition_soc_multiplier*pilot_dsoc / (pilot_transition_soc - 1)) * (
                    self.get_soc() - 1
                )

            if max_dsoc > pilot_dsoc:
                dsoc_limit = pilot_dsoc
            else:
                dsoc_limit = max_dsoc

            if new_soc-self.get_soc() > dsoc_limit:
                curr_soc = dsoc_limit + self.get_soc()
            else:
                curr_soc = new_soc

        dsoc = curr_soc - self.get_soc()
        self.prev_capacity = self.current_capacity
        self.current_capacity = curr_soc * self.battery_capacity

        # For charging power and charging rate (current), we use the
        # the average over this time period.
        self.current_energy = dsoc * self.battery_capacity  # / (period / 60)
        self.required_energy = self.required_energy - \
            self.current_energy  # * period / 60
        return self.current_energy / (period / 60) * 1000 / voltage

    def _discharge(self, amps, voltage, phases) -> float:
        '''
        The _discharge method is used to discharge the EV's battery.
        Inputs:
            - power: the power input in kW (it is negative because of the discharge)
        '''
        assert (amps < 0)

        voltage = voltage * math.sqrt(phases)

        given_power = (amps * voltage / 1000)
        prev_capacity = self.current_capacity

        if abs(given_power) > abs(self.max_discharge_power):
            given_power = self.max_discharge_power
            
        # if discharge efficeincy is dict, then get the discharge efficiency based on the current
        # current level
        if isinstance(self.charge_efficiency, dict):
            discharge_efficiency = self.discharge_efficiency.get(np.abs(np.round(amps)), 1)/100            
            assert discharge_efficiency > 0
        else:
            discharge_efficiency = self.discharge_efficiency
        
        given_energy = given_power * discharge_efficiency * self.timescale / 60
        if self.current_capacity + given_energy < self.min_battery_capacity:
            if self.current_capacity > self.min_battery_capacity:
                self.current_energy = - \
                    (self.current_capacity - self.min_battery_capacity)
                given_energy = self.current_energy
                self.prev_capacity = self.current_capacity
                self.current_capacity = self.min_battery_capacity
            else:
                self.current_energy = 0
                given_energy = 0
                self.prev_capacity = self.current_capacity
                self.current_capacity = self.min_battery_capacity
        else:
            self.current_energy = given_energy  # * 60 / self.timescale
            self.prev_capacity = self.current_capacity
            self.current_capacity += given_energy

        self.required_energy = self.required_energy + self.current_energy

        if prev_capacity > self.min_emergency_battery_capacity and self.current_capacity < self.min_emergency_battery_capacity:
            self.min_emergency_battery_capacity_metric += 1        

        assert given_energy <= 0
        return given_energy*60/self.timescale * 1000 / voltage

    def calculate_max_energy_with_AFAP(self,
                                       max_cs_power
                                       ) -> None:
        '''
        The calculate_max_energy_with_AFAP method is used to calculate the maximum energy that the EV can receive
        when charging as fast as possible (AFAP). This value is used to determine the user_satifaction metric of the EV.
        '''

        if abs(max_cs_power) > abs(self.max_ac_charge_power):
            max_power = self.max_ac_charge_power
        else:
            max_power = max_cs_power

        self.max_energy_AFAP = self.battery_capacity_at_arrival
        
        # if charge efficeincy is dict, then get the charge efficiency based on the current
        if isinstance(self.charge_efficiency, dict):
            # iterate over all values of charge efficiency and get the maximum
            # charge efficiency
            max_charge_efficiency = 0
            for key, value in self.charge_efficiency.items():
                if value > max_charge_efficiency:
                    max_charge_efficiency = value
            charge_efficiency = max_charge_efficiency/100                
            
        else:
            charge_efficiency = self.charge_efficiency
            
        for _ in range(self.time_of_arrival, self.time_of_departure+1):
            self.max_energy_AFAP += max_power * charge_efficiency * self.timescale / 60
            self.max_energy_AFAP = self.my_ceil(self.max_energy_AFAP, 2)
            if self.max_energy_AFAP > self.battery_capacity:
                self.max_energy_AFAP = self.battery_capacity
                break

    def get_battery_degradation(self) -> Tuple[float, float]:
        '''
        A function that returns the capacity loss of the EV.

        Qacc := Accumulated battery cell throughput (Ah)
        Qsim := Battery cell throughput during simulation (Ah)        
        Tacc := Battery age (days)
        Tsim := Simulation time (days)
        theta := Battery temperature (K)

        Outputs: 
            - Capacity loss: the capacity loss
        '''

        # Degradation modelling parameters
        e0 = 7.543e6
        e1 = 23.75e6
        e2 = 6976

        z0 = 7.348e-3
        z1 = 3.667
        z2 = 7.6e-4
        z3 = 4.081e-3

        b_cap_ah = 2.05  # ah
        b_cap_kwh = 78  # kwh

        d_dist = 15000  # km
        b_age = 2*365  # days
        G = 0.186  # kwh/km

        # Age of the battery in days
        T_acc = b_age

        # Simulation time in days
        T_sim = (self.time_of_departure - self.time_of_arrival + 1) * \
            self.timescale / (60*24)  # days

        theta = 298.15  # Kelvin
        k = 0.8263  # Volts

        v_min = 3.3324  # Volts
        # Add the final soc to the historic soc
        self.historic_soc.append(self.get_soc())
        avg_soc = np.mean(self.historic_soc)
        v_avg = v_min + k * avg_soc

        # alpha(v_avg)
        alpha = (e0 * v_avg - e1) * math.exp(-e2 / theta)
        d_cal = alpha * 0.75 * T_sim / (T_acc)**0.25

        # beta(v_avg, soc_avg)
        # print(f'avg_soc: {avg_soc}')
        self.active_steps.append(1)

        # get historic soc that self.active_steps == 1
        filtered_historic_soc = [soc for i, soc in enumerate(
            self.historic_soc) if self.active_steps[i] == 1]
        # print(f'filtered soc {filtered_historic_soc}')
        avg_filtered_soc = np.mean(filtered_historic_soc)

        delta_DoD = 2 * \
            abs(avg_filtered_soc.repeat(len(filtered_historic_soc)) -
                filtered_historic_soc).mean()
        # print(f'delta_DoD: {delta_DoD}')
        v_half_soc = v_min + k * 0.5
        beta = z0 * (v_half_soc - z1)**2 + z2 + z3 * delta_DoD

        Q_sim = (self.abs_total_energy_exchanged / b_cap_kwh) * b_cap_ah

        # accumulated throughput
        Q_acc = 2 * (b_age * (d_dist / 365) * G * b_cap_ah) / b_cap_kwh
        # print(f'Q_acc: {Q_acc}')

        d_cyc = beta * 0.5 * Q_sim / (Q_acc)**0.5

        self.calendar_loss = d_cal
        self.cyclic_loss = d_cyc

        return d_cal, d_cyc