import numpy as np
import matplotlib.pyplot as plt
from tabulate import tabulate
from pandas import read_excel, read_csv

from .utils.exceptions import InputError, RockGradingError, ArmourUnitsError, user_warning


def read_grading(filepath, rho=2650, sheet_name=0):
    """ Load a rock grading from an excel or csv file

    Excel input file can be generated by using
    :obj:`bw.generate_excel <breakwater.utils.input_generator.generate_excel>`
    with input argument 'Grading'. Alternatively, when using your own
    Excel file it must, at least, have the following headers.

    +------------+-----------+-----------+-----+-----+
    | Rock Class | M50 Lower | M50 Upper | NLL | NUL |
    +------------+-----------+-----------+-----+-----+

    Parameters
    ----------
    filepath : str
        a valid filepath
    rho : float, optional, default: 2650
        density of the armourstone [kg/m³]
    sheet_name : str, optional, default: 0
        name of the sheet with the RockGrading to read, default value
        of 0 will result in reading the first sheet

    Returns
    -------
    bw.RockGrading
        a rock grading object
    """
    extension = filepath.split('.')[-1]

    if extension == 'xlsx':
        df = read_excel(filepath, sheet_name=sheet_name)

    elif extension == 'csv':
        df = read_csv(filepath, delimiter=";|,", engine='python')

    else:
        raise IOError(f'specified file has an invalid extension')

    # create custom_grading
    custom_grading = {}

    # lowercase all columns
    df.columns = map(str.lower, df.columns)

    # iterate over the rows in the df
    for i, row in df.iterrows():
        rock_class = row['Rock Class'.lower()]
        custom_grading[rock_class] = {

            'M50': [row['M50 Lower'.lower()], row['M50 Upper'.lower()]],

            'NLL': row['NLL'.lower()],

            'NUL': row['NUL'.lower()]
        }

    return RockGrading(grading=custom_grading, rho=rho)

def read_units(filepath, kd, name, rho=2400, sheet_name=0):
    """ Load ArmourUnits from an excel or csv file

    Excel input file can be generated by using
    :obj:`bw.generate_excel <breakwater.utils.input_generator.generate_excel>`
    with input argument 'ArmourUnits'. Alternatively, when using your
    own Excel file it must, at least, have the following headers.

    +-----------+-------+-------+--------+
    | Volume    |   D   |   h   |   Vc   |
    +-----------+-------+-------+--------+

    .. note::
       Do not use this function to read Xbloc or XblocPlus armour units
       from an Excel file. Use the predefined,
       :obj:`bw.Xbloc <breakwater.material.Xbloc>` or
       :obj:`bw.XblocPlus <breakwater.material.XblocPlus>` instead.


    Parameters
    ----------
    filepath : str
        a valid filepath
    kd : int
        Stability coefficient [-]
    name : str
        name of the ArmourUnit
    rho : float, optional, default: 2400
        density of the concrete [kg/m³]
    sheet_name : str
        name of the sheet with the armour units to read, default value
        of 0 will result in reading the first sheet

    Returns
    -------
    bw.ConcreteArmour
        an armour units object
    """
    extension = filepath.split('.')[-1]

    if extension == 'xlsx':
        df = read_excel(filepath, sheet_name=sheet_name)

    elif extension == 'csv':
        df = read_csv(filepath, delimiter=";|,", engine='python')

    else:
        raise IOError(f'specified file has an invalid extension')


    if name in ['Xbloc', 'XblocPlus']:
        user_warning(
            (f'The correction factor for {name} is not computed when '
             f'loading {name} from an Excel file.'))

    # create custom_grading
    units = {}

    # lowercase all columns
    df.columns = map(str.lower, df.columns)

    for i, row in df.iterrows():
        class_ = row['Volume'.lower()]
        units[class_] = {

            'D': row['D'.lower()],

            'h': row['h'.lower()],

            'Vc': row['Vc'.lower()]
        }

    return ConcreteArmour(kd=kd, units=units, name=name, rho=rho)

class RockGrading:
    """ Standard rock grading of the standard NEN-EN 13383-1 (2002)

    Create a Rock Grading to determine the rock class in
    :py:class:`RockRubbleMound`, :py:class:`ConcreteRubbleMound`,
    :py:class:`RubbleMound` or :py:class:`Caisson`. By default the
    standard rock grading of the NEN-EN 13383-1 (2002) is used.
    However, it is possible to change the grading to a user defined
    custom grading by giving one in the arguments.

    Parameters
    ----------
    grading : dict, optional, default: None
        If specified the custom grading will replace the default rock
        grading of the NEN-EN 13383-1. The custom grading must be
        formatted as {str: {'M50': [float, float], 'NLL': float, 'NUL':
        float}}, with str the rock class and a lower and upper bound
        for M50. Furthermore, the NLL and NUL mass must be specified.
    rho : float, optional, default: 2650
        density of the armourstone [kg/m³]
    y_NLL : float, optional, default: 0.06
        the fraction passing at the nominal lower limit mass (NLL) of a
        grading
    y_NUL : float, optional, default: 0.9
        the fraction passing at the nominal upper limit mass (NUL) of a
        grading

    Attributes
    ----------
    grading : dict
        dictionary of the chosen rock grading
    rho : float
        density of the armourstone
    y_NLL : float
        the fraction passing at the nominal lower limit mass (NLL)
    y_NUL : float
        the fraction passing at the nominal upper limit mass (NUL)
    """
    def __init__(self, grading=None, rho=2650, y_NLL=0.06, y_NUL=0.9):
        """ See help(RockGrading) for more info """
        self.name = 'Rock'
        self.rho = rho
        self.y_NLL = y_NLL
        self.y_NUL = y_NUL

        if grading is not None:
            # check if format of rock grading is a valid format
            msg = ('Invalid custom rock grading. Rock grading must be '
                   'formatted as {str: {\'M50\': [float, float], \'NLL\': '
                   'float, \'NUL\': float}}')
            if isinstance(grading, dict):
                # grading is a dict
                for class_, values in grading.items():
                    if isinstance(values, dict):
                        # grading is a nested dict
                        for i, (param, value) in enumerate(values.items()):
                            if isinstance(value, list) and len(value) == 2:
                                # mass is list with an upper and lower bound
                                if param == 'M50':
                                    # key is M50
                                    pass
                                else:
                                    # invalid key for M50
                                    raise RockGradingError(msg)
                            elif param == 'NLL':
                                pass
                            elif param == 'NUL':
                                pass
                            else:
                                raise RockGradingError(msg)
                    else:
                        # grading is not a nested dict
                        raise RockGradingError(msg)
            else:
                # grading is not a dict
                raise RockGradingError(msg)
        else:
            # no custom grading was specified, use NEN-EN 13383-1
            grading = {'LMA_5/40':
                           {'M50': [10, 20], 'NLL': 5, 'NUL': 40},
                       'LMA_10/60':
                           {'M50': [20, 35], 'NLL': 10, 'NUL': 60},
                       'LMA_40/200':
                           {'M50': [80, 120], 'NLL': 40,'NUL': 200},
                       'LMA_15/300':
                           {'M50': [45, 135], 'NLL': 15, 'NUL': 300},
                       'LMA_60/300':
                           {'M50': [120, 190], 'NLL': 60, 'NUL': 300},
                       'HMA_300/1000':
                           {'M50': [540, 690], 'NLL': 300, 'NUL': 1000},
                       'HMA_1000/3000':
                           {'M50': [1700, 2100], 'NLL': 1000, 'NUL': 3000},
                       'HMA_3000/6000':
                           {'M50': [4200, 4800], 'NLL': 3000, 'NUL': 6000},
                       'HMA_6000/10000':
                           {'M50': [7500, 8500], 'NLL': 6000, 'NUL': 10000},
                       'HMA_10000/15000':
                           {'M50': [12000, 13000], 'NLL': 10000, 'NUL': 15000}}

        # set the grading as an attribute
        self.grading = grading

    def __str__(self):
        table = []

        for i, (class_, values) in enumerate(self.grading.items()):
            if i == 0:
                # set table headers in first iteration
                headers = ['Rock Class']
                # get the keys of the nested dict and add to headers
                headers.extend(list(values.keys()))

            # add the rock class to a new row
            row = [class_]

            # add the values to the row
            row.extend(list(values.values()))

            # add row to the table
            table.append(row)

        # return the table as a string for printing
        return str(tabulate(table, headers, tablefmt='github'))

    def __setitem__(self, key, value):
        self.grading[key] = value

    def __getitem__(self, key):
        return self.grading[key]

    def add_cost(self, cost):
        """ Add cost per m³ of each Rock Class

        Add the cost per m³ of each Rock Class to the :py:attr:`grading`

        Parameters
        ----------
        cost : dict
            cost of each Rock Class, keys must be identical to the name
            of the rock class

        Raises
        ------
        KeyError
            if a rock class of the given cost is not in the grading
        InputError
            if a rock class of the :py:attr:`grading` has no price
        """
        rock_classes = list(self.grading.keys())
        # iterate over the prices in the given dict
        for class_, price in cost.items():
            # check if the rock class is in the grading
            if class_ in self.grading.keys():
                # class is in the grading so add price to the nested dict
                self.grading[class_]['price'] = price

                # delete from rock_classes to validate if all have a price
                rock_classes.remove(class_)
            else:
                # class not in the grading, raise KeyError
                valid_classes = ', '.join(self.grading.keys())
                raise KeyError(
                    (f'{class_} is not a rock class of the RockGrading, '
                     f'valid classes are {valid_classes}'))

        # check if pricing has been added for all rock_classes
        if any(rock_classes):
            # not all have been added, raise error
            no_pricing = ', '.join(rock_classes)
            raise InputError(f'No pricing has been given for {no_pricing}')

    def get_class(self, Dn50):
        """ Get the rock class for a given Dn50

        Parameters
        ----------
        Dn50 : float
            nominal diameter of the armourstone [m]

        Returns
        -------
        str
            Rock class

        Raises
        ------
        RockGradingError
            If the computed Dn50 of the armour layer is out of range for
            the specified rock grading
        """
        rock_class = None
        M_required = self.rho * Dn50**3

        for class_, characteristics in self.grading.items():
            if characteristics['M50'][1] >= M_required:
                rock_class = class_
                break

        if rock_class == None:
            max_class = list(self.grading.keys())[-1]
            max_mass = self.grading[max_class]['M50'][1]
            max_dn = np.round((max_mass/2650)**(1/3), 3)
            raise RockGradingError(
                f'Dn50 = {np.round(Dn50, 3)} is out of range for the specified'
                f' rock grading, {max_dn} m is the maximum possible Dn50.')
        else:
            return rock_class

    def get_class_dn50(self, class_):
        """ Get the average Dn50 of a rock class

        Parameters
        ----------
        class_ : str
            Rock class

        Returns
        -------
        Dn50 : float
            nominal diameter of the armourstone [m]

        Raises
        ------
        KeyError
            If the specified rock class is not in the rock grading
        """
        class_M = self.grading[class_]['M50']

        average_Dn50 = ((class_M[0] + class_M[1])/(2*self.rho))**(1/3)

        return average_Dn50

    def rosin_rammler(self, class_, y):
        """ Compute the Rosin-Rammler curve for an idealised gradings

        The Rosin-Rammler curve can be used to interpolate between the
        limits of standard gradings (CIRIA, CUR, CETMEF, 2007). Given
        that two fixed point on the curve are known, for instance the
        NLL and NUL limits, M50 and  the uniformity coefficient, n_RRM,
        can be computed. These values are subsequently used to compute
        My.

        .. math::
           M_{y} = M_{50} \\left(\\frac{\\ln{(1 - y)}}{\\ln{(0.5)}}
           \\right)^{1/n_{RRM}}

        Parameters
        ----------
        class_ : str
            Rock class
        y : float
            the fraction passing value

        Returns
        -------
        My : float
            the mass corresponding to that value using a percentage
            subscript to express that fraction [kg]

        Raises
        ------
        KeyError
            If the specified rock class is not in the rock grading
        """
        # set NLL and NUL for the given rock class
        NLL = self.grading[class_]['NLL']
        NUL = self.grading[class_]['NUL']

        # compute the uniformity index
        a = np.log(1 - self.y_NUL)/np.log(1 - self.y_NLL)
        n_RRM = np.log10(a)/np.log10(NUL/NLL)

        # compute M50 with NLL and the computed n_RRM, in accordance
        # with the advice from the Rock Manual (p113, bottom)
        M50 = NLL * (np.log(1 - self.y_NLL)/np.log(0.5))**(-1/n_RRM)

        # compute the mass passing
        My = M50 * (np.log(1- y)/np.log(0.5))**(1/n_RRM)

        return My

    def plot_rosin_rammler(self, class_):
        """ plot the Rosin-Rammler curve for an idealised gradings

        Parameters
        ----------
        class_ : str
            Rock class

        Raises
        ------
        KeyError
            If the specified rock class is not in the rock grading
        """
        y = np.linspace(0, 0.999999, 1000)
        My = self.rosin_rammler(class_=class_, y=y)

        # plot grading curve and NLL + NUL points
        plt.plot(My, y)
        plt.plot(self.grading[class_]['NLL'], self.y_NLL, 'ko')
        plt.plot(self.grading[class_]['NUL'], self.y_NUL, 'ko')

        # style plot
        xmax = self.rosin_rammler(class_=class_, y=0.9999)
        xmin = self.rosin_rammler(class_=class_, y=0.0001)
        plt.xlim(xmin=xmin, xmax=xmax)
        plt.ylim(ymin=0, ymax=1)

        plt.xlabel('My, Mass of armourstone [kg]')
        plt.ylabel('y, fraction lighter')

        plt.title(f'Rosin-Rammler curve for {class_}')

        plt.grid('minor')
        plt.show()


class ConcreteArmour:
    """ Define concrete armour units

    Define a type of concrete armour unit, :py:class:`Xbloc` and
    :py:class:`XblocPlus` have been predefined.

    Parameters
    ----------
    kd : int
        Stability coefficient [-]
    units : dict
        Dictionary of the defined concrete armour units. Format must
        be {V: dict}, where V is the volume of the armour unit (float)
        and dict = {'D': float, 'h': float, 'Vc': float} in which D is
        the diameter, h is the thickness of the armour layers and Vc is
        the volume of concrete.
    name : str, optional, default: None
        name of the ArmourUnit, by default the name of the armour unit
        is derived from the name of the class.
    rho : float, optional, default: 2400
        density of the concrete used to make the armour units [kg/m³]

    Attributes
    ----------
    kd : int
        Stability coefficient [-]
    units : dict
        Dictionary of the defined concrete armour units. Format must
        be {V: dict}, where V is the volume of the armour unit (float)
        and dict = {'D': float, 'h': float, 'Vc': float} in which D is
        the diameter, h is the thickness of the armour layers and Vc is
        the volume of concrete.
    rho : float, optional, default: 2400
        density of the concrete used to make the armour units [kg/m³]
    """

    def __init__(self, kd, units, name=None, rho=2400):
        """ See help(ConcreteArmour) for more info """
        self.kd = kd
        self.units = units
        self.rho = rho

        # set name of the armour unit
        if name is None:
            self.name = self.__class__.__name__
        else:
            self.name = name

    def __str__(self):
        table = []

        for i, (volume, dimensions) in enumerate(self.units.items()):
            if i == 0:
                # set headers of the table
                headers = list(dimensions.keys())
                headers.insert(0, 'V')
            row = []
            row.append(volume)

            for parameter, dimension in dimensions.items():
                row.append(dimension)
            table.append(row)

        table = tabulate(table, headers, tablefmt='github')
        return str(table)

    def __setitem__(self, key, value):
        self.units[key] = value

    def __getitem__(self, key):
        return self.units[key]

    def get_class(self, d):
        """ Get the volume of a standard concrete armour unit

        Parameters
        ----------
        d : float
            minimal required diameter [m]

        Returns
        -------
        float
            Volume of a standard armour unit

        Raises
        ------
        ArmourUnitsError
            If the computed Dn of the armour layer is out of range for
            the specified armour units
        """
        computed_volume = d**3
        class_volume = None

        for volume in self.units:
            if volume >= computed_volume:
                class_volume = volume
                break

        if class_volume == None:
            max_class = list(self.units.keys())[-1]
            max_dn = np.round(max_class**(1/3), 3)
            msg = ('given dn is out of range for the specified armour units'
                   f', {max_dn} m (V = {max_class} m^3) is the maximum '
                   'possible dn.')
            raise ArmourUnitsError(msg)

        return class_volume


class Xbloc(ConcreteArmour):
    """ Xbloc concrete armour units

    .. figure:: _figures/Xbloc.png
       :align: center
       :alt: Xbloc armour unit

    Xbloc is an armour unit developed by Delta Marine Consultants (DMC).
    All units have been predefined in :py:attr:`units`, with the values
    from table 1 from Delta Marine Consultants (2018).

    Parameters
    ----------
    rho : float, optional, default: 2400
        density of the concrete used to make the armour units

    Attributes
    ----------
    kd : int
        Stability coefficient [-]
    units : dict
        Dictionary of the defined concrete armour units. Format is
        {V: dict}, where V is the volume of the Xbloc (float) and
        dict = {'D': float, 'h': float, 'Vc': float} in
        which D is the diameter, h is the thickness of the armour
        layers and Vc is the volume of concrete.
    rho : float, optional, default: 2400
        density of the concrete used to make the armour units [kg/m³]
    """

    def __init__(self, rho=2400):
        """ See help(Xbloc) for more info """
        units = {0.75: {'D': 1.31, 'h': 1.3, 'Vc': 0.53},
                 1: {'D': 1.44, 'h': 1.4, 'Vc': 0.58},
                 1.5: {'D': 1.65, 'h': 1.6, 'Vc': 0.66},
                 2: {'D': 1.82, 'h': 1.8, 'Vc': 0.73},
                 2.5: {'D': 1.96, 'h': 1.9, 'Vc': 0.78},
                 3: {'D': 2.08, 'h': 2, 'Vc': 0.83},
                 4: {'D': 2.29, 'h': 2.2, 'Vc': 0.92},
                 5: {'D': 2.47, 'h': 2.4, 'Vc': 0.99},
                 6: {'D': 2.62, 'h': 2.5, 'Vc': 1.05},
                 7: {'D': 2.76, 'h': 2.7, 'Vc': 1.11},
                 8: {'D': 2.88, 'h': 2.8, 'Vc': 1.16},
                 9: {'D': 3, 'h': 2.9, 'Vc': 1.2},
                 10: {'D': 3.11, 'h': 3, 'Vc': 1.25},
                 12: {'D': 3.3, 'h': 3.2, 'Vc': 1.32},
                 14: {'D': 3.48, 'h': 3.4, 'Vc': 1.39},
                 16: {'D': 3.63, 'h': 3.5, 'Vc': 1.46},
                 18: {'D': 3.78, 'h': 3.7, 'Vc': 1.52},
                 20: {'D': 3.91, 'h': 3.8, 'Vc': 1.57}}
        super().__init__(kd=16, units=units, rho=rho)

    def correction_factor(
            self, Hs, h, Rc, occurrence_hs, slope, slope_foreshore,
            permeability, logger=None, **kwargs):
        """ Determine correction factor for Xbloc

        Correction factors for phenomena which require an increase in
        the Xbloc unit sizes. For the conceptual design of structures
        the correction factor must be multiplied with the volume
        (Delta Marine Consultants, 2018).

        Parameters
        ----------
        Hs : float
            significant wave height [m]
        h : float
            water depth [m]
        Rc : float
            Crest freeboard [m]
        occurrence_hs : bool
            True if frequent occurrence of the near-design wave height
            during the lifetime of the structure. False if no frequent
            occurrence
        slope : float
            slope of the armour layer [rad]
        slope_foreshore : float
            slope of the foreshore [rad]
        permeability : {'permeable', 'low', 'impermeable'}
            Permeability of the core
        logger : dict, optional, default: None
            dict to log messages, must have keys 'INFO' and 'WARNINGS'

        Returns
        -------
        float
            The correction factor to be applied on the volume. Returns
            1 if no correction factor is needed.
        """
        correction = [1]
        log = [None]

        # frequent occurrence of near-design wave height
        if occurrence_hs:
            correction.append(1.25)
            log.append('Xbloc: frequent occurrence of Hs')

        # steep foreshore
        if np.arctan(1/30) <= slope_foreshore:
            if np.arctan(1/20) <= slope_foreshore:
                if np.arctan(1/15) <= slope_foreshore:
                    if np.arctan(1/10) <= slope_foreshore:
                        correction.append(2) # steeper than 1:10
                        log.append('Xbloc: steepness of foreshore is greater '
                                   'than 1:10')
                    else:
                        correction.append(1.5) # between 1:15-1:10
                        log.append('Xbloc: steepness of foreshore is between '
                                   '1:15 and 1:10')
                else:
                    correction.append(1.25) # between 1:20-1:15
                    log.append('Xbloc: steepness of foreshore is between '
                               '1:20 and 1:15')
            else:
                correction.append(1.1) # between 1:30-1:20
                log.append('Xbloc: steepness of foreshore is between 1:30 and'
                           ' 1:20')

        # Check if structure is low crested
        if Rc/Hs < 1:
            if Rc/Hs < 0.5:
                correction.append(2)
                log.append('Xbloc: low crested structure, Rc/Hs < 0.5')
            else:
                correction.append(1.5)
                log.append('Xbloc: low crested structure, Rc/Hs < 1.0')

        # Check if water depth is large
        if h > 2.5*Hs:
            if h > 3.5*Hs:
                correction.append(2)
                log.append('Xbloc: large water depth, h > 3.5 x Hs')
            else:
                correction.append(1.5)
                log.append('Xbloc: large water depth, h > 2.5 x Hs')


        # low core permeability
        if permeability == 'low':
            correction.append(1.5)
            log.append('Xbloc: low core permeability')
        elif permeability == 'impermeable':
            correction.append(2)
            log.append('Xbloc: impermeable core')

        # Check if armour slope is mild
        if slope < np.arctan(2/3):
            if slope < np.arctan(1/2):
                correction.append(1.5)
                log.append('Xbloc: armour slope is milder than 1:2')
            else:
                correction.append(1.25)
                log.append('Xbloc: armour slope is milder than 2:3')

        max_correction = max(correction)
        max_index = correction.index(max_correction)
        msg = (f'Xbloc: used correction factor: {max_correction}, because '
               f'{log[max_index]} was the maximum value')

        if logger is not None:
            if len(log) > 1:
                correction.pop(0)
                log.pop(0)
                logger['INFO'].extend(log)
                logger['INFO'].append(msg)

        return max_correction


class XblocPlus(ConcreteArmour):
    """ XblocPlus concrete armour units

    .. figure:: _figures/XblocPlus.png
       :align: center
       :alt: XblocPlus armour unit

    XblocPlus is an armour unit developed by Delta Marine Consultants
    (DMC). All units have been predefined in :py:attr:`units`, with the
    values from table 2 from Delta Marine Consultants (2018).

    Parameters
    ----------
    rho : float, optional, default: 2400
        density of the concrete used to make the armour units

    Attributes
    ----------
    kd : int
        Stability coefficient [-]
    units : dict
        Dictionary of the defined concrete armour units. Format is
        {V: dict}, where V is the volume of the XblocPlus unit (float)
        and dict = {'L1': float, 'L2': float, 'L3': float, 'h': float
        'Vc': float} in which L1, L2 and L3 are the dimensions, h is
        the thickness of the armour layers and Vc is the volume of
        concrete.
    rho : float, optional, default: 2400
        density of the concrete used to make the armour units [kg/m³]
    """
    def __init__(self, rho=2400):
        """ See help(XblocPlus) for more info """
        units = {0.75: {'L1': 0.75, 'L2': 1.51, 'L3': 1.91, 'h': 1.2, 'Vc': 0.48},
                 1: {'L1': 0.83, 'L2': 1.66, 'L3': 2.1, 'h': 1.3, 'Vc': 0.53},
                 1.5: {'L1': 0.95, 'L2': 1.9, 'L3': 2.41, 'h': 1.5, 'Vc': 0.6},
                 2: {'L1': 1.04, 'L2': 2.09, 'L3': 2.65, 'h': 1.7, 'Vc': 0.66},
                 2.5: {'L1': 1.12, 'L2': 2.25, 'L3': 2.85, 'h': 1.8, 'Vc': 0.71},
                 3: {'L1': 1.19, 'L2': 2.39, 'L3': 3.03, 'h': 1.9, 'Vc': 0.76},
                 4: {'L1': 1.31, 'L2': 2.63, 'L3': 3.34, 'h': 2.1, 'Vc': 0.83},
                 5: {'L1': 1.42, 'L2': 2.83, 'L3': 3.59, 'h': 2.3, 'Vc': 0.9},
                 6: {'L1': 1.5, 'L2': 3.01, 'L3': 3.82, 'h': 2.4, 'Vc': 0.96},
                 7: {'L1': 1.58, 'L2': 3.17, 'L3': 4.02, 'h': 2.5, 'Vc': 1.01},
                 8: {'L1': 1.66, 'L2': 3.31, 'L3': 4.2, 'h': 2.7, 'Vc': 1.05},
                 9: {'L1': 1.72, 'L2': 3.45, 'L3': 4.37, 'h': 2.8, 'Vc': 1.09},
                 10: {'L1': 1.78, 'L2': 3.57, 'L3': 4.53, 'h': 2.9, 'Vc': 1.13},
                 12: {'L1': 1.89, 'L2': 3.79, 'L3': 4.81, 'h': 3.0, 'Vc': 1.2},
                 14: {'L1': 1.99, 'L2': 3.99, 'L3': 5.06, 'h': 3.2, 'Vc': 1.27},
                 16: {'L1': 2.09, 'L2': 4.17, 'L3': 5.29, 'h': 3.3, 'Vc': 1.33},
                 18: {'L1': 2.17, 'L2': 4.34, 'L3': 5.5, 'h': 3.5, 'Vc': 1.38},
                 20: {'L1': 2.65, 'L2': 4.5, 'L3': 5.7, 'h': 3.6, 'Vc': 1.43}}
        super().__init__(kd=12, units=units, rho=rho)

    def correction_factor(
            self, Hs, Rc, slope_foreshore, permeability, logger=None,
            **kwargs):
        """ Determine correction factor for XblocPlus

        Correction factors for phenomena which require an increase in
        the XblocPlus unit sizes. For the conceptual design of
        structures the correction factor must be multiplied with the
        volume (Delta Marine Consultants, 2018).

        Parameters
        ----------
        Hs : float
            significant wave height [m]
        Rc : float
            Crest freeboard [m]
        slope_foreshore : float
            slope of the foreshore [rad]
        permeability : str
            Permeability of the core {'permeable', 'low', 'impermeable'}
        logger : dict, optional, default: None
            dict to log messages, must have keys 'INFO' and 'WARNINGS'

        Returns
        -------
        float
            The correction factor to be applied on the volume. Returns
            1 if no correction factor is needed.
        """
        correction = [1]
        log = [None]

        # steep foreshore
        if np.arctan(1/30) <= slope_foreshore:
            if np.arctan(1/20) <= slope_foreshore:
                if np.arctan(1/15) <= slope_foreshore:
                    if np.arctan(1/10) <= slope_foreshore:
                        correction.append(2) # steeper than 1:10
                        log.append('XblocPlus: steepness of foreshore is '
                                   'greater than 1:10')
                    else:
                        correction.append(1.5) # between 1:15-1:10
                        log.append('XblocPlus: steepness of foreshore is '
                                   'between 1:15 and 1:10')
                else:
                    correction.append(1.25) # between 1:20-1:15
                    log.append('XblocPlus: steepness of foreshore is between '
                               '1:20 and 1:15')
            else:
                correction.append(1.1) # between 1:30-1:20
                log.append('XblocPlus: steepness of foreshore is between 1:30'
                           ' and 1:20')

        # Check if structure is low crested
        if Rc/Hs < 1:
            if Rc/Hs < 0.5:
                correction.append(1.5)
                log.append('XblocPlus: low crested structure, Rc/Hs < 0.5')
            else:
                correction.append(1.25)
                log.append('XblocPlus: low crested structure, Rc/Hs < 1.0')

        # low core permeability
        if permeability == 'low':
            correction.append(1.25)
            log.append('XblocPlus: low core permeability')
        elif permeability == 'impermeable':
            correction.append(1.5)
            log.append('XblocPlus: impermeable core')

        max_correction = max(correction)
        max_index = correction.index(max_correction)
        msg = (f'XblocPlus: used correction factor: {max_correction}, because '
               f'{log[max_index]} was the maximum value')

        if logger is not None:
            if len(log) > 1:
                correction.pop(0)
                log.pop(0)
                logger['INFO'].extend(log)
                logger['INFO'].append(msg)

        return max_correction
