import torch
from typing import Tuple


def marginal_pdf(values: torch.Tensor, bins: torch.Tensor, sigma: torch.Tensor,
                 epsilon: float = 1e-10) -> Tuple[torch.Tensor, torch.Tensor]:
    """
    Function that calculates the marginal probability distribution function of the input tensor based on the number
    of histogram bins.
    Args:
                    values: (torch.Tensor), shape [BxNx1]
                    bins: (torch.Tensor), shape [NUM_BINS]
                    sigma: (torch.Tensor), shape [1], gaussian smoothing factor
                    epsilon: (float), scalar, for numerical stability
    Returns:
                    pdf: (torch.Tensor), shape [BxN]
                    kernel_values: (torch.Tensor), shape [BxNxNUM_BINS]"""

    if not torch.is_tensor(values):
        raise TypeError("Input values type is not a torch.Tensor. Got {}"
                        .format(type(values)))
    if not torch.is_tensor(bins):
        raise TypeError("Input bins type is not a torch.Tensor. Got {}"
                        .format(type(bins)))
    if not torch.is_tensor(sigma):
        raise TypeError("Input sigma type is not a torch.Tensor. Got {}"
                        .format(type(sigma)))

    if not values.dim() == 3:
        raise ValueError("Input values must be a of the shape BxNx1."
                         " Got {}".format(values.shape))
    if not bins.dim() == 1:
        raise ValueError("Input bins must be a of the shape NUM_BINS"
                         " Got {}".format(bins.shape))
    if not sigma.dim() == 0:
        raise ValueError("Input sigma must be a of the shape 1"
                         " Got {}".format(sigma.shape))

    residuals = values - bins.unsqueeze(0).unsqueeze(0)
    kernel_values = torch.exp(-0.5 * (residuals / sigma).pow(2))

    pdf = torch.mean(kernel_values, dim=1)
    normalization = torch.sum(pdf, dim=1).unsqueeze(1) + epsilon
    pdf = pdf / normalization

    return (pdf, kernel_values)