"""
freespace (:mod:`skrf.media.freespace`)
========================================

A plane-wave (TEM Mode) in Freespace.

Represents a plane-wave in a homogeneous freespace, defined by
the space's relative permittivity and relative permeability.

.. autosummary::
    :toctree: generated/

    Freespace


"""
from __future__ import annotations

from typing import TYPE_CHECKING

from numpy import ones, real, sqrt
from scipy.constants import epsilon_0, mu_0

from ..constants import NumberLike
from ..data import materials
from .media import Media

if TYPE_CHECKING:
    from ..frequency import Frequency


class Freespace(Media):
    r"""
    A plane-wave (TEM Mode) in Freespace.

    A Freespace media can be constructed in two ways:

    * from complex, relative permativity and permeability OR
    * from real relative permativity and permeability with loss tangents.

    There is also a method to initialize from a
    existing distributed circuit, appropriately named
    :func:`Freespace.from_distributed_circuit`


    Parameters
    ----------
    frequency : :class:`~skrf.frequency.Frequency` object
        frequency band of this transmission line medium
    z0_port : number, array-like, or None
        `z0_port` is the port impedance for networks generated by the media.
        If `z0_port` is not None, the networks generated by the media are
        renormalized (or in other words embedded) from the characteristic
        impedance z0 of the media to `z0_port`.
        Else if `z0_port` is None, the networks port impedances will be the raw
        characteristic impedance z0 of the media.
        (Default is None)
    z0_override : number, array-like, or None
        `z0_override` override the characteristic impedance for the media.
        If `z0_override` is not None, the networks generated by the media have
        their characteristic impedance `z0` overrided by `z0_override`.
        (Default is None)
    z0 : number, array-like, or None
        deprecated parameter, alias to `z0_override` if `z0_override` is None.
        Emmit a deprecation warning.
    ep_r : number, array-like
        complex relative permittivity. negative imaginary is lossy.
    mu_r : number, array-like
        complex relative permeability. negative imaginary is lossy.
    ep_loss_tan : None, number, array-like
        electric loss tangent (of the permativity).
        If not None, imag(ep_r) is ignored.
    mu_loss_tan : None, number, array-like
        magnetic loss tangent (of the permeability).
        If not None, imag(mu_r) is ignored.
    rho : number, array-like, string or None
        resistivity (ohm-m) of the conductor walls. If array-like
        must be same length as frequency. if str, it must be a key in
        :data:`skrf.data.materials`.
        Default is None (lossless).
    \*args, \*\*kwargs : arguments and keyword arguments


    Examples
    --------
    >>> from skrf.media.freespace import Freespace
    >>> from skrf.frequency import Frequency
    >>> f = Frequency(75,110,101,'ghz')
    >>> Freespace(frequency=f, ep_r=11.9)
    >>> Freespace(frequency=f, ep_r=11.9-1.1j)
    >>> Freespace(frequency=f, ep_r=11.9, ep_loss_tan=.1)
    >>> Freespace(frequency=f, ep_r=11.9-1.1j, mu_r = 1.1-.1j)

    """

    def __init__(self, frequency: Frequency | None = None,
                 z0_port: NumberLike | None = None,
                 z0_override: NumberLike | None = None,
                 z0: NumberLike | None = None,
                 ep_r: NumberLike = 1+0j, mu_r: NumberLike = 1+0j,
                 ep_loss_tan: NumberLike | None = None,
                 mu_loss_tan: NumberLike | None = None,
                 rho: NumberLike | str | None = None,
                 *args, **kwargs):
        Media.__init__(self, frequency = frequency,
                       z0_port = z0_port, z0_override = z0_override, z0 = z0)
        self.ep_r = ep_r
        self.mu_r = mu_r
        self.rho = rho

        self.ep_loss_tan = ep_loss_tan
        self.mu_loss_tan = mu_loss_tan

    def __str__(self) -> str:
        f = self.frequency
        output = 'Freespace  Media.  %i-%i %s.  %i points'%\
                (f.f_scaled[0], f.f_scaled[-1], f.unit, f.npoints)
        return output

    def __repr__(self) -> str:
        return self.__str__()

    @property
    def ep(self) -> NumberLike:
        r"""
        Complex dielectric permittivity.

        If :math:`\tan\delta_e` is not defined:

        .. math::

            \varepsilon = \varepsilon_0 \varepsilon_r

        otherwise,

        .. math::

            \varepsilon = \varepsilon_0 \Re[\varepsilon_r] (1 - j\tan\delta_e)

        where :math:`\tan\delta_e` is the electric loss tangent.

        Returns
        -------
        ep : number or array-like
            Complex dielectric permittivity in F/m.

        """

        if self.ep_loss_tan is not None:
            ep_r = real(self.ep_r)*(1 - 1j*self.ep_loss_tan)
        else:
            ep_r = self.ep_r
        return ep_r*epsilon_0

    @property
    def mu(self) -> NumberLike:
        r"""
        Complex dielectric permeability.

        If :math:`\tan\delta_m` is not defined:

        .. math::

            \mu = \mu_0 \mu_r

        otherwise,

        .. math::

            \mu = \mu_0 \Re[\mu_r] (1 - j\tan\delta_m)

        where :math:`\tan\delta_m` is the magnetic loss tangent.

        Returns
        -------
        mu : number
            Complex permeability in H/m.
        """
        if self.mu_loss_tan is not None:
            mu_r = real(self.mu_r)*(1 -1j*self.mu_loss_tan)
        else:
            mu_r = self.mu_r
        return mu_r*mu_0


    @classmethod
    def from_distributed_circuit(cls, dc, *args, **kwargs) -> Media:
        r"""
        Initialize a freespace from :class:`~skrf.media.distributedCircuit.DistributedCircuit`.

        Parameters
        ----------
        dc: :class:`~skrf.media.distributedCircuit.DistributedCircuit`
            a DistributedCircuit object
        \*args, \*\*kwargs :
            passed to `Freespace.__init__

        Notes
        -----
        Here are the details::

            w = dc.frequency.w
            z= dc.Z/(w*mu_0)
            y= dc.Y/(w*epsilon_0)
            ep_r = -1j*y
            mu_r = -1j*z

        See Also
        --------
        skrf.media.distributedCircuit.DistributedCircuit

        """
        w = dc.frequency.w
        z= dc.Z/(w*mu_0)
        y= dc.Y/(w*epsilon_0)


        kw={}
        kw['ep_r'] = -1j*y
        kw['mu_r'] = -1j*z

        kwargs.update(kw)
        return cls(frequency=dc.frequency, **kwargs)

    @property
    def rho(self) -> NumberLike:
        """
        Conductivity in ohm*m.

        Parameters
        ----------
        val : float, array-like or str
            the resistivity in ohm*m. If array-like must be same length
            as self.frequency. if str, it must be a key in
            :data:`~skrf.data.materials`.

        Examples
        --------
        >>> wg.rho = 2.8e-8
        >>> wg.rho = 2.8e-8 * ones(len(wg.frequency))
        >>> wg.rho = 'al'
        >>> wg.rho = 'aluminum'
        """
        return self._rho

    @rho.setter
    def rho(self, val):
        if isinstance(val, str):
            self._rho = materials[val.lower()]['resistivity(ohm*m)']
        else:
            self._rho=val

    @property
    def ep_with_rho(self) -> NumberLike:
        r"""
        Complex permittivity with resistivity absorbed into its imaginary component.

        .. math::

            \varepsilon - j \frac{1}{\rho\omega}

        See Also
        --------
        rho
        ep
        """
        if self.rho is not None:
            return self.ep -1j/(self.rho*self.frequency.w)
        else:
            return self.ep

    @property
    def gamma(self) -> NumberLike:
        r"""
        Propagation Constant, :math:`\gamma`.

        Defined as,

        .. math::

                \gamma =  \sqrt{ Z^{'}  Y^{'}}

        Returns
        -------
        gamma : np.ndarray
            Propagation Constant,

        Note
        ----
        The components of propagation constant are interpreted as follows:

        * positive real(gamma) = attenuation
        * positive imag(gamma) = forward propagation
        """
        ep = self.ep_with_rho
        return 1j*self.frequency.w * sqrt(ep*self.mu)

    @property
    def z0_characteristic(self) -> NumberLike:
        r"""
        Characteristic Impedance, :math:`z_0`.

        .. math::

                Z_0 = \sqrt{ \frac{Z^{'}}{Y^{'}}}

        Returns
        -------
        z0_characteristic : np.ndarray
            Characteristic Impedance in units of ohms
        """
        ep = self.ep_with_rho
        return sqrt(self.mu/ep)*ones(len(self))

    def plot_ep(self):
        """
        Plot the real and imaginary part of the complex permittivity.
        """
        self.plot(self.ep_r.real, label=r'ep_r real')
        self.plot(self.ep_r.imag, label=r'ep_r imag')

    def plot_mu(self):
        """
        Plot the real and imaginary part of the complex permeability.
        """
        self.plot(self.mu_r.real, label=r'mu_r real')
        self.plot(self.mu_r.imag, label=r'mu_r imag')

    def plot_ep_mu(self):
        """
        Plot the real and imaginary part of the complex permittivity with resistivity.
        """
        self.plot_ep()
        self.plot_mu()
