"""
    Miscellaneous - useful functions
"""
from typing import Tuple, Union, List, Callable
import logging
import numpy as np
import matplotlib.pyplot as plt


def plot_func(
    interval: Tuple,
    func: Callable,
    num_x_vals: int = 1000,
    plot_title: str = None,
    plot_ylabel: str = None,
    savefig_path: str = None,
) -> None:
    """Plots a given 1D function (callable) within the provided interval.

    It plots a given 1-dimensional function (python Callable) within the provided interval.
    The x values are computed by generating `n_

    Args:
        interval (tuple): Delimiters (xmin, xmax) of the histogram.
        func (callable): Function to be evaluated. The image of the function builds
            the y-axis values that are plotted.
        num_x_vals (int, default=100): It defines the number of evenly spaced x values
            within the given interval or range in which the function (callable) is evaluated.
        plot_title (string, default=None): Title of the plot.
        plot_ylabel (string, default=None): Label of the y-axis.
        savefig_path (string, default=None): path to save the created figure. If it is not
            provided, the plot is shown at the end of the routine.

    """
    if not isinstance(interval, tuple):
        raise ValueError("interval argument must be a tuple")

    (xmin, xmax) = interval

    xx = np.linspace(xmin, xmax, num=num_x_vals)
    yy = func(xx)

    plt.plot(xx, yy)
    plt.xlabel("x")
    if plot_ylabel:
        plt.ylabel(plot_ylabel)

    if plot_title:
        plt.title(plot_title)

    if savefig_path:
        plt.savefig(savefig_path, dpi=800)
    else:
        plt.show()


def relu(x: Union[float,np.ndarray]):
    """Element-wise maximum between the value and zero.

    Args:
        x (ndarray): list of numbers to compute its element-wise maximum.

    Returns:
        array_like consisting in the element-wise maximum vector of the given values.
    """
    return np.maximum(x, np.zeros_like(x))


def indicator(
    x: float,
    start: float = None,
    stop: float = None,
    inclusive: str = "both"
) -> np.ndarray:
    r"""Element-wise indicator function within a real interval.
    The interval can be left-closed, right-closed, closed or open.
    Visit https://en.wikipedia.org/wiki/Indicator_function for more information.

    Args:
        x (ndarray): list of numbers to compute its element-wise indicator image.
        start (double, default=None): left value of the interval. If not provided,
            the left value is equivalent to :math:`- {\infty}`.
        stop (double, default=None): right value of the interval. If not provided,
            the right value is equivalent to :math:`+ {\infty}`.
        inclusive (string, default="both"): type of interval. For left-closed interval
            use "left", for right-closed interval use "right", for closed interval use
            "both" and for open interval use "neither".

    Returns:
        array_like consisting in the element-wise indicator function image of the given values.
    """
    if start is None and stop is None:
        raise ValueError("Error: provide start and/or stop for indicator function.")

    INCLUSIVE_OPTIONS = set(["both", "left", "right", "neither"])
    if inclusive not in INCLUSIVE_OPTIONS:
        raise ValueError(f"Error: invalid interval inclusive parameter: {inclusive}\n"
                         "\t inclusive has to be one of the following: {INCLUSIVE_OPTIONS}.")

    if start is not None:
        if inclusive in ["both", "left"]:
            condition = start <= x
        elif inclusive in ["neither", "right"]:
            condition = start < x

    if (start is not None) and (stop is not None):
        if inclusive in ["both", "right"]:
            condition = np.logical_and(condition, (x <= stop))
        elif inclusive in ["neither", "left"]:
            condition = np.logical_and(condition, (x < stop))
    elif stop:
        if inclusive in ["both", "right"]:
            condition = x <= stop
        elif inclusive in ["neither", "left"]:
            condition = x < stop

    return np.where(condition, 1.0, 0.0)


def get_bins_centers_and_contour(bin_edges: List[float]) -> List[float]:
    """Calculates the centers and contour of the given the bins edges.

    Computes the centers of the given the bins edges. Also, the smallest and the largest
    bin delimitiers are included to define the countour of the representation interval.

    Args:
        bin_edges (list): list of numbers (floats) that specify each bin delimiter.

    Returns:
        list of numbers (floats) consisting in the list of bin centers and contour.

    """
    centers = [bin_edges[0]] # Adding initial contour
    l = len(bin_edges)
    for i in range(l-1):
        centers.append((bin_edges[i]+bin_edges[i+1])/2) # Adding centers
    centers.append(bin_edges[-1]) # Adding final contour
    return centers


def get_logger(logger_name: str) -> logging.Logger:
    """Get a pre-configured logger
    """
    log_formatter = logging.Formatter(
        "[{levelname: <7} - {module: >10}]: {message}", style='{'
    )
    log_level = logging.INFO

    handler = logging.StreamHandler()
    handler.setFormatter(log_formatter)
    handler.setLevel(log_level)

    logger = logging.getLogger(logger_name)
    logger.setLevel(log_level)
    logger.addHandler(handler)

    return logger
