# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
# 
# This module is under the UIUC open-source license. See 
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
"""
import numpy as np
import biosteam as bst
from .. import Unit
from .mixing import Mixer
from .heat_exchange import HXutility
from ._flash import Flash, Evaporator
from .design_tools import (
    compute_vacuum_system_power_and_cost,
    compute_heat_transfer_area
)    
from thermosteam import MultiStream, settings
import flexsolve as flx
from warnings import warn
import numpy as np
from .design_tools import heat_transfer as ht

__all__ = ('MultiEffectEvaporator',)

log = np.log
exp = np.exp

# Table 22.32 Product process and design (pg 592)
# Name: ('Area range (m2)', 'Cost(A) (USD)', 'U (kJ/(hr*m2*K)))', 'Material')
evaporators = {'Horizontal tube': 
                    ((9.29, 743.224),
                     lambda A, CE: CE*2.304*A**0.53,
                     4906.02,
                     'Carbon steel'),
               'Long-tube vertical':
                   ((9.29, 743.224),
                    lambda A, CE: CE*3.086*A**0.55,
                    8176.699,
                    'Carbon steel'),
               'Forced circulation': 
                   ((13.935, 8000),
                    lambda A, CE: CE/500*exp(8.2986 + 0.5329*log(A*0.0929)-0.000196*log(A*0.0929)**2),
                    10731.918,
                    'Carbon steel'),
               'Falling film': 
                   ((13.935, 371.612),
                    lambda A, CE: CE*7.416*A**0.55,
                    10220.874,
                    'Stainless steel tubes/Carbon steel shell')}


class MultiEffectEvaporator(Unit):
    """
    Creates evaporatorators with pressures given by P (a list of pressures). 
    Adjusts first evaporator vapor fraction to satisfy an overall fraction
    evaporated. All evaporators after the first have zero duty. Condenses
    the vapor coming out of the last evaporator. Pumps all liquid streams
    to prevent back flow in later parts. All liquid evaporated is ultimately
    recondensed. Cost is based on required heat transfer area. Vacuum system
    is based on air leakage. Air leakage is based on volume, as given by
    residence time `tau` and flow rate to each evaporator.

    Parameters
    ----------
    ins : 
        Inlet.
    outs : 
        * [0] Solid-rich stream.
        * [1] Condensate stream.
    P : tuple[float]
        Pressures describing each evaporator (Pa).
    V : float
        Molar fraction evaporated as specified in `V_definition` 
        (either overall or in the first effect).
    V_definition : str, optional
        * 'Overall' - `V` is the overall molar fraction evaporated.
        * 'First-effect' - `V` is the molar fraction evaporated in the first effect.
    
    Examples
    --------
    Concentrate sugar setting vapor fraction at the first effect:
    
    >>> import biosteam as bst
    >>> bst.process_tools.default()
    >>> from biorefineries.cellulosic import create_cellulosic_ethanol_chemicals
    >>> bst.settings.set_thermo(create_cellulosic_ethanol_chemicals())
    >>> feed = bst.Stream('feed', Water=1000, Glucose=100, 
    ...                   AceticAcid=0.5, HMF=0.1, Furfural=0.1,
    ...                   units='kg/hr')
    >>> E1 = bst.MultiEffectEvaporator('E1', ins=feed, outs=('solids', 'liquid'), 
    ...                                V=0.1, V_definition='First-effect',
    ...                                P=(101325, 73581, 50892, 32777, 20000))
    >>> E1.simulate()
    >>> E1.show()
    MultiEffectEvaporator: E1
    ins...
    [0] feed
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Water       55.5
                        AceticAcid  0.00833
                        Furfural    0.00104
                        HMF         0.000793
                        Glucose     0.555
    outs...
    [0] solids
        phase: 'l', T: 333.21 K, P: 20000 Pa
        flow (kmol/hr): Water       20.5
                        AceticAcid  0.00183
                        Furfural    5.96e-05
                        HMF         0.000793
                        Glucose     0.555
    [1] liquid
        phase: 'l', T: 352.11 K, P: 20000 Pa
        flow (kmol/hr): Water       35
                        AceticAcid  0.0065
                        Furfural    0.000981
    
    >>> E1.results()
    Multi-effect evaporator                                    Units       E1
    Electricity         Power                                     kW     5.72
                        Cost                                  USD/hr    0.447
    Low pressure steam  Duty                                   kJ/hr 5.83e+05
                        Flow                                 kmol/hr     15.1
                        Cost                                  USD/hr     3.58
    Cooling water       Duty                                   kJ/hr -3.5e+05
                        Flow                                 kmol/hr      239
                        Cost                                  USD/hr    0.117
    Design              Area                                     m^2       11
                        Volume                                   m^3     3.51
    Purchase cost       Evaporators (x5)                         USD 9.56e+03
                        Condenser - Double pipe                  USD 5.36e+03
                        Vacuum system - Liquid-ring pump...      USD 1.24e+04
    Total purchase cost                                          USD 2.73e+04
    Utility cost                                              USD/hr     4.15
    
    Concentrate sugar setting overall vapor fraction:
    
    >>> import biosteam as bst
    >>> from biorefineries.cellulosic import create_cellulosic_ethanol_chemicals
    >>> bst.settings.set_thermo(create_cellulosic_ethanol_chemicals())
    >>> feed = bst.Stream('feed', Water=1000, Glucose=100, 
    ...                   AceticAcid=0.5, HMF=0.1, Furfural=0.1,
    ...                   units='kg/hr')
    >>> E1 = bst.MultiEffectEvaporator('E1', ins=feed, outs=('solids', 'liquid'), 
    ...                                V=0.1, V_definition='Overall',
    ...                                P=(101325, 73581, 50892, 32777, 20000))
    >>> E1.simulate()
    >>> E1.show()
    MultiEffectEvaporator: E1
    ins...
    [0] feed
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Water       55.5
                        AceticAcid  0.00833
                        Furfural    0.00104
                        HMF         0.000793
                        Glucose     0.555
    outs...
    [0] solids
        phase: 'l', T: 354.91 K, P: 50892 Pa
        flow (kmol/hr): Water       50
                        AceticAcid  0.00683
                        Furfural    0.000535
                        HMF         0.000793
                        Glucose     0.555
    [1] liquid
        phase: 'l', T: 361.13 K, P: 50892 Pa
        flow (kmol/hr): Water       5.55
                        AceticAcid  0.0015
                        Furfural    0.000506
    
    >>> E1.results()
    Multi-effect evaporator                                    Units        E1
    Electricity         Power                                     kW      5.72
                        Cost                                  USD/hr     0.447
    Low pressure steam  Duty                                   kJ/hr  3.84e+05
                        Flow                                 kmol/hr      9.94
                        Cost                                  USD/hr      2.36
    Cooling water       Duty                                   kJ/hr -1.15e+05
                        Flow                                 kmol/hr      78.8
                        Cost                                  USD/hr    0.0384
    Design              Area                                     m^2      1.62
                        Volume                                   m^3      3.07
    Purchase cost       Evaporators (x3)                         USD  2.74e+03
                        Condenser - Double pipe                  USD   3.9e+03
                        Vacuum system - Liquid-ring pump...      USD  1.24e+04
    Total purchase cost                                          USD  1.91e+04
    Utility cost                                              USD/hr      2.85
    
    Concentrate sugar setting overall vapor fraction:
    
    >>> import biosteam as bst
    >>> from biorefineries.cellulosic import create_cellulosic_ethanol_chemicals
    >>> bst.settings.set_thermo(create_cellulosic_ethanol_chemicals())
    >>> feed = bst.Stream('feed', Water=1000, Glucose=100, 
    ...                   AceticAcid=0.5, HMF=0.1, Furfural=0.1,
    ...                   units='kg/hr')
    >>> E1 = bst.MultiEffectEvaporator('E1', ins=feed, outs=('solids', 'liquid'), 
    ...                                V=0.1, V_definition='Overall',
    ...                                P=(101325, 73581, 50892, 32777, 20000))
    >>> E1.simulate()
    >>> E1.show()
    MultiEffectEvaporator: E1
    ins...
    [0] feed
        phase: 'l', T: 298.15 K, P: 101325 Pa
        flow (kmol/hr): Water       55.5
                        AceticAcid  0.00833
                        Furfural    0.00104
                        HMF         0.000793
                        Glucose     0.555
    outs...
    [0] solids
        phase: 'l', T: 354.91 K, P: 50892 Pa
        flow (kmol/hr): Water       50
                        AceticAcid  0.00683
                        Furfural    0.000535
                        HMF         0.000793
                        Glucose     0.555
    [1] liquid
        phase: 'l', T: 361.13 K, P: 50892 Pa
        flow (kmol/hr): Water       5.55
                        AceticAcid  0.0015
                        Furfural    0.000506
    
    >>> E1.results()
    Multi-effect evaporator                                    Units        E1
    Electricity         Power                                     kW      5.72
                        Cost                                  USD/hr     0.447
    Low pressure steam  Duty                                   kJ/hr  3.84e+05
                        Flow                                 kmol/hr      9.94
                        Cost                                  USD/hr      2.36
    Cooling water       Duty                                   kJ/hr -1.15e+05
                        Flow                                 kmol/hr      78.8
                        Cost                                  USD/hr    0.0384
    Design              Area                                     m^2      1.62
                        Volume                                   m^3      3.07
    Purchase cost       Evaporators (x3)                         USD  2.74e+03
                        Condenser - Double pipe                  USD   3.9e+03
                        Vacuum system - Liquid-ring pump...      USD  1.24e+04
    Total purchase cost                                          USD  1.91e+04
    Utility cost                                              USD/hr      2.85
    
    """
    line = 'Multi-effect evaporator'
    auxiliary_unit_names = ('condenser', 'mixer', 'vacuum_system', 'evaporators')
    _units = {'Area': 'm^2',
              'Volume': 'm^3'}
    _F_BM_default = {'Evaporators': 2.45,
                     'Vacuum system': 1.0,
                     'Condenser': 3.17}
    _N_outs = 2

    # Evaporator type
    _Type = 'Forced circulation'
    
    # Data for simmulation and costing
    _evap_data = evaporators[_Type]

    @property
    def Type(self):
        """Evaporation type."""
        return self._Type
    @Type.setter
    def Type(self, evap_type):
        try:
            self._evap_data = evaporators[evap_type]
        except KeyError:
            dummy = str(evaporators.keys())[11:-2]
            raise ValueError(f"Type must be one of the following: {dummy}")
        self._Type = evap_type

    @property
    def V_definition(self):
        """[str] Must be one of the following:
        * 'Overall' - Defines attribute `V` as the overall molar fraction evaporated.
        * 'First-effect' - Defines attribute `V` as the molar fraction evaporated in the first effect.
        * 'First-effect duty' - Defines attribute `V` as the supplied duty over the total duty required to achived a vapor fraction of 1 at the first effect .
        """
        return self._V_definition
    @V_definition.setter
    def V_definition(self, V_definition):
        V_definition = V_definition.capitalize()
        if V_definition in ('Overall', 'First-effect', 'First-effect duty'):
            self._reload_components = True
            self._V_definition = V_definition
        else:
            raise ValueError("V_definition must be either 'Overall', 'First-effect', or 'First-effect duty'")

    def __init__(self, ID='', ins=None, outs=(), thermo=None, *, P, V, V_definition='Overall',
                 flash=True, chemical='7732-18-5'):
        Unit.__init__(self, ID, ins, outs, thermo)
        self.P = P #: tuple[float] Pressures describing each evaporator (Pa).
        self.V = V #: [float] Molar fraction evaporated.
        self.V_definition = V_definition
        self.flash = flash #: [bool] Whether to perform a flash calculation to account for volatile components.
        self._V_first_effect = None
        self.chemical = chemical
        
    def reset_cache(self, isdynamic=None):
        self._reload_components = True
        
    def _load_components(self):
        P = self.P
        self._N_evap = n = len(P) # Number of evaporators
        self.evaporators = []
        if self.flash:
            evaporator = self.auxiliary(
                'evaporators', Flash,
                ins=self.ins, 
                outs=(None, None), P=P[0],
            )
        else:
            evaporator = self.auxiliary(
                'evaporators', Evaporator,
                ins=self.ins,
                outs=(None, None, None), P=P[0],
                chemical=self.chemical,
            )
        for i in range(1, n):
            evaporator = self.auxiliary(
                'evaporators', Evaporator, 
                # Put liquid first, then vapor side stream
                ins=(evaporator.outs[1], evaporator.outs[0]), 
                outs=(None, self.outs[0] if i == n-1 else None, None), 
                P=P[i], chemical=self.chemical,
            )
        condenser = self.auxiliary(
            'condenser', HXutility, ins=evaporator.outs[0], outs=[None], V=0
        )
        self.auxiliary(
            'mixer', Mixer, 
            ins=[condenser.outs[0], *[i.outs[2] for i in self.evaporators[1:]]], 
            outs=self.outs[1]
        )
        
    def _V_overall(self, V_first_effect):
        first_evaporator, *other_evaporators = self.evaporators
        V_definition = self.V_definition
        chemical = self.chemical
        feed = first_evaporator.ins[0]
        if V_definition in ('First-effect', 'Overall'):
            first_evaporator.V = V_overall = V_first_effect
        elif V_definition == 'First-effect duty':
            if isinstance(first_evaporator, Flash):
                stream = feed.copy()
                stream.vle(P=first_evaporator.P, V=1)
                Hvap = stream.H - feed.H
                Q = V_first_effect * Hvap
            else:
                Hvap = first_evaporator.Hvap * first_evaporator.ins[0].imol[chemical]
                Q = V_first_effect * Hvap 
            first_evaporator.Q = Q
            
        first_evaporator._run()
        for evap in other_evaporators: evap._run()
        evaporated = self.evaporators[-1].outs[1]
        V_overall = 1. - evaporated.imol[chemical] / feed.imol[chemical]
        return V_overall
        
    def _V_overall_objective_function(self, V_first_effect):
        return self._V_overall(V_first_effect) - self.V
    
    def _run(self):
        out_wt_solids, liq = self.outs
        ins = self.ins
        if self.V == 0:
            out_wt_solids.copy_like(ins[0])
            liq.empty()
            self._reload_components = True
            return
        
        if self._reload_components:
            self._load_components()
            self._reload_components = False
        
        if self.V_definition == 'Overall':
            P = tuple(self.P)
            self.P = list(P)
            for i in range(self._N_evap - 1):
                if self._V_overall(0.) > self.V:
                    self.P.pop()
                    self._load_components()
                    self._reload_components = True
                else:
                    break
            
            self.P = P
            self._V_first_effect = flx.IQ_interpolation(self._V_overall_objective_function,
                                                        0., 1., None, None, self._V_first_effect, 
                                                        xtol=1e-9, ytol=1e-6,
                                                        checkiter=False)
            V_overall = self.V
        else: 
            V_overall = self._V_overall(self.V)
            
        evaporators = self.evaporators
        condenser = self.condenser
        mixer = self.mixer
        last_evaporator = evaporators[-1]
    
        # Condense vapor from last effector
        condenser._run()
        
        # Mix liquid streams
        liq = mixer.outs[0]
        liq.P = self.ins[0].P
        liq.mix_from(mixer.ins, conserve_phases=True)
        if self.flash:
            mixed_stream = MultiStream(None, thermo=self.thermo)
            mixed_stream.copy_flow(self.ins[0])
            mixed_stream.vle(P=last_evaporator.P, V=V_overall)
            out_wt_solids.mol = mixed_stream.imol['l']
            if liq.phase == 'l':
                liq.phase = 'l'
                liq.mol = mixed_stream.imol['g']
            else:
                H = liq.H
                liq.copy_like(mixed_stream['g'])
                liq.vle(H=H, P=self.ins[0].P)
        liq.P = out_wt_solids.P
        
    def _design(self):
        if self.V == 0: 
            for i in self.auxiliary_units: i._setup()
            return
        
        # This functions also finds the cost
        A_range, C_func, U, _ = self._evap_data
        evaporators = self.evaporators
        Design = self.design_results
        Cost = self.baseline_purchase_costs
        CE = bst.CE
        
        first_evaporator = evaporators[0]
        if self.flash:
            duty = first_evaporator.H_out - first_evaporator.H_in
        else:
            duty = first_evaporator.design_results['Heat transfer']
        
        # Cost first evaporators
        Q = abs(duty)
        Tci = first_evaporator.ins[0].T
        Tco = first_evaporator.outs[0].T
        hu = self.create_heat_utility()
        hu(duty, Tci, Tco)
        Th = hu.inlet_utility_stream.T
        LMTD = ht.compute_LMTD(Th, Th, Tci, Tco)
        ft = 1
        A = abs(compute_heat_transfer_area(LMTD, U, Q, ft))
        self._evap_costs = evap_costs = [C_func(A, CE)]
        
        # Find condenser requirements
        condenser = self.condenser
        condenser.simulate(run=False)
        
        # Find area and cost of evaporators
        As = [A]
        evap = evaporators[-1]
        bounds_warning = bst.exceptions.bounds_warning
        for evap in evaporators[1:]:
            Q = evap.design_results['Heat transfer']
            if Q <= 1e-12: 
                As.append(0.)
                evap_costs.append(0.)
            else:
                Tc = evap.outs[0].T
                Th = evap.outs[2].T
                LMTD = Th - Tc
                A = compute_heat_transfer_area(LMTD, U, Q, 1.)
                As.append(A)
                bounds_warning(self, 'heat transfer area requirement', A, 'ft2', A_range, 'cost')
                evap_costs.append(C_func(A, CE))
        self._As = As
        Design['Area'] = A = sum(As)
        total_volume = 0
        for evap in evaporators:
            if evap.outs[0].isempty(): continue
            evap._size_flash_vessel()
            vapor_sep_design = evap.design_results
            L = vapor_sep_design['Length']
            D = vapor_sep_design['Diameter']
            R = D / 2.
            total_volume += 0.0283168466 * np.pi * L * R * R # m3
        Design['Volume'] = total_volume
        Cost['Evaporators'] = sum(evap_costs)
        
        self.vacuum_system = bst.VacuumSystem(
            self, 'Steam-jet ejector', vessel_volume=total_volume, P_suction=self.outs[0].P,
        )
            
        
        
