#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
gnpy.core.parameters
====================

This module contains all parameters to configure standard network elements.

"""

from scipy.constants import c, pi
from numpy import squeeze, log10, exp


from gnpy.core.utils import db2lin, convert_length
from gnpy.core.exceptions import ParametersError


class Parameters:
    def asdict(self):
        class_dict = self.__class__.__dict__
        instance_dict = self.__dict__
        new_dict = {}
        for key in class_dict:
            if isinstance(class_dict[key], property):
                new_dict[key] = instance_dict['_' + key]
        return new_dict


class PumpParams(Parameters):
    def __init__(self, power, frequency, propagation_direction):
        self._power = power
        self._frequency = frequency
        self._propagation_direction = propagation_direction

    @property
    def power(self):
        return self._power

    @property
    def frequency(self):
        return self._frequency

    @property
    def propagation_direction(self):
        return self._propagation_direction


class RamanParams(Parameters):
    def __init__(self, **kwargs):
        self._flag_raman = kwargs['flag_raman']
        self._space_resolution = kwargs['space_resolution'] if 'space_resolution' in kwargs else None
        self._tolerance = kwargs['tolerance'] if 'tolerance' in kwargs else None

    @property
    def flag_raman(self):
        return self._flag_raman

    @property
    def space_resolution(self):
        return self._space_resolution

    @property
    def tolerance(self):
        return self._tolerance


class NLIParams(Parameters):
    def __init__(self, **kwargs):
        self._nli_method_name = kwargs['nli_method_name']
        self._wdm_grid_size = kwargs['wdm_grid_size']
        self._dispersion_tolerance = kwargs['dispersion_tolerance']
        self._phase_shift_tolerance = kwargs['phase_shift_tolerance']
        self._f_cut_resolution = None
        self._f_pump_resolution = None
        self._computed_channels = kwargs['computed_channels'] if 'computed_channels' in kwargs else None

    @property
    def nli_method_name(self):
        return self._nli_method_name

    @property
    def wdm_grid_size(self):
        return self._wdm_grid_size

    @property
    def dispersion_tolerance(self):
        return self._dispersion_tolerance

    @property
    def phase_shift_tolerance(self):
        return self._phase_shift_tolerance

    @property
    def f_cut_resolution(self):
        return self._f_cut_resolution

    @f_cut_resolution.setter
    def f_cut_resolution(self, f_cut_resolution):
        self._f_cut_resolution = f_cut_resolution

    @property
    def f_pump_resolution(self):
        return self._f_pump_resolution

    @f_pump_resolution.setter
    def f_pump_resolution(self, f_pump_resolution):
        self._f_pump_resolution = f_pump_resolution

    @property
    def computed_channels(self):
        return self._computed_channels


class SimParams(Parameters):
    def __init__(self, **kwargs):
        try:
            if 'nli_parameters' in kwargs:
                self._nli_params = NLIParams(**kwargs['nli_parameters'])
            else:
                self._nli_params = None
            if 'raman_parameters' in kwargs:
                self._raman_params = RamanParams(**kwargs['raman_parameters'])
            else:
                self._raman_params = None
        except KeyError as e:
            raise ParametersError(f'Simulation parameters must include {e}. Configuration: {kwargs}')

    @property
    def nli_params(self):
        return self._nli_params

    @property
    def raman_params(self):
        return self._raman_params


class FiberParams(Parameters):
    def __init__(self, **kwargs):
        try:
            self._length = convert_length(kwargs['length'], kwargs['length_units'])
            # fixed attenuator for padding
            self._att_in = kwargs['att_in'] if 'att_in' in kwargs else 0
            # if not defined in the network json connector loss in/out
            # the None value will be updated in network.py[build_network]
            # with default values from eqpt_config.json[Spans]
            self._con_in = kwargs['con_in'] if 'con_in' in kwargs else None
            self._con_out = kwargs['con_out'] if 'con_out' in kwargs else None
            if 'ref_wavelength' in kwargs:
                self._ref_wavelength = kwargs['ref_wavelength']
                self._ref_frequency = c / self.ref_wavelength
            elif 'ref_frequency' in kwargs:
                self._ref_frequency = kwargs['ref_frequency']
                self._ref_wavelength = c / self.ref_frequency
            else:
                self._ref_wavelength = 1550e-9
                self._ref_frequency = c / self.ref_wavelength
            self._dispersion = kwargs['dispersion']  # s/m/m
            self._dispersion_slope = kwargs['dispersion_slope'] if 'dispersion_slope' in kwargs else \
                -2 * self._dispersion/self.ref_wavelength  # s/m/m/m
            self._beta2 = -(self.ref_wavelength ** 2) * self.dispersion / (2 * pi * c)  # 1/(m * Hz^2)
            # Eq. (3.23) in  Abramczyk, Halina. "Dispersion phenomena in optical fibers." Virtual European University
            # on Lasers. Available online: http://mitr.p.lodz.pl/evu/lectures/Abramczyk3.pdf
            # (accessed on 25 March 2018) (2005).
            self._beta3 = ((self.dispersion_slope - (4*pi*c/self.ref_wavelength**3) * self.beta2) /
                           (2*pi*c/self.ref_wavelength**2)**2)
            self._gamma = kwargs['gamma']  # 1/W/m
            self._pmd_coef = kwargs['pmd_coef']  # s/sqrt(m)
            if type(kwargs['loss_coef']) == dict:
                self._loss_coef = squeeze(kwargs['loss_coef']['loss_coef_power']) * 1e-3  # lineic loss dB/m
                self._f_loss_ref = squeeze(kwargs['loss_coef']['frequency'])  # Hz
            else:
                self._loss_coef = kwargs['loss_coef'] * 1e-3  # lineic loss dB/m
                self._f_loss_ref = 193.5e12  # Hz
            self._lin_attenuation = db2lin(self.length * self.loss_coef)
            self._lin_loss_exp = self.loss_coef / (10 * log10(exp(1)))  # linear power exponent loss Neper/m
            self._effective_length = (1 - exp(- self.lin_loss_exp * self.length)) / self.lin_loss_exp
            self._asymptotic_length = 1 / self.lin_loss_exp
            # raman parameters (not compulsory)
            self._raman_efficiency = kwargs['raman_efficiency'] if 'raman_efficiency' in kwargs else None
            self._pumps_loss_coef = kwargs['pumps_loss_coef'] if 'pumps_loss_coef' in kwargs else None
        except KeyError as e:
            raise ParametersError(f'Fiber configurations json must include {e}. Configuration: {kwargs}')

    @property
    def length(self):
        return self._length

    @length.setter
    def length(self, length):
        """length must be in m"""
        self._length = length

    @property
    def att_in(self):
        return self._att_in

    @att_in.setter
    def att_in(self, att_in):
        self._att_in = att_in

    @property
    def con_in(self):
        return self._con_in

    @con_in.setter
    def con_in(self, con_in):
        self._con_in = con_in

    @property
    def con_out(self):
        return self._con_out

    @con_out.setter
    def con_out(self, con_out):
        self._con_out = con_out

    @property
    def dispersion(self):
        return self._dispersion

    @property
    def dispersion_slope(self):
        return self._dispersion_slope

    @property
    def gamma(self):
        return self._gamma

    @property
    def pmd_coef(self):
        return self._pmd_coef

    @property
    def ref_wavelength(self):
        return self._ref_wavelength

    @property
    def ref_frequency(self):
        return self._ref_frequency

    @property
    def beta2(self):
        return self._beta2

    @property
    def beta3(self):
        return self._beta3

    @property
    def loss_coef(self):
        return self._loss_coef

    @property
    def f_loss_ref(self):
        return self._f_loss_ref

    @property
    def lin_loss_exp(self):
        return self._lin_loss_exp

    @property
    def lin_attenuation(self):
        return self._lin_attenuation

    @property
    def effective_length(self):
        return self._effective_length

    @property
    def asymptotic_length(self):
        return self._asymptotic_length

    @property
    def raman_efficiency(self):
        return self._raman_efficiency

    @property
    def pumps_loss_coef(self):
        return self._pumps_loss_coef

    def asdict(self):
        dictionary = super().asdict()
        dictionary['loss_coef'] = self.loss_coef * 1e3
        dictionary['length_units'] = 'm'
        return dictionary
