from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Union

import numpy


class NotAvailable:
    """Use when the selected fit method does not provide a certain results."""

    pass


UNITS = {
    "Time": "s",
    "Flux": "Hz",
    "DetectorDistance": "cm",
    "DetectorArea": "cm^2",
    "SolidAngle": "sr",
    "energies": "keV",
    "raw_energy": "keV",
}


class QuantificationParameters(NamedTuple):
    I0: float  # incident photon count
    Time: float  # sec
    Flux: float  # I0/Time (ph/sec)
    DetectorDistance: float  # cm
    DetectorArea: float  # cm^2
    SolidAngle: float  # steradian
    ReferenceElement: Optional[str]
    ReferenceTransitions: Optional[str]


class XRFBatchFitResult(NamedTuple):
    # Nparameters: number of fit parameters
    # Narea_parameters: number of peak area parameters
    # Nlayers: number of layers in a multilayer matrix
    # Nspectra: number of XRF spectra
    # Nchan: original number of MCA channels
    # Nfit: fit number of MCA channels

    parameters: numpy.ndarray  # (Nparameters, Nspectra)
    uncertainties: numpy.ndarray  # (Nparameters, Nspectra)
    derivatives: Optional[numpy.ndarray]  # (Nparameters, Nchan)
    parameter_names: List[str]  # (Nparameters,)
    channels: numpy.ndarray  # (Nfit,)
    energies: numpy.ndarray  # (Nfit,)
    raw_channels: numpy.ndarray  # (Nchan,)
    raw_energies: numpy.ndarray  # (Nchan,)
    spectra: Union[numpy.ndarray, None, NotAvailable]  # (Nspectra, Nchan)
    model: Union[numpy.ndarray, None, NotAvailable]  # (Nspectra, Nchan)
    residuals: Union[numpy.ndarray, None, NotAvailable]  # (Nspectra, Nchan)
    mass_fractions: Optional[
        numpy.ndarray
    ]  # (Narea_parameters, Nspectra); None when no quantification
    mass_fractions_per_layer: Optional[
        List[numpy.ndarray]
    ]  # (Nlayers, Narea_parameters, Nspectra); None when no quantification
    mass_fraction_names: Optional[
        List[str]
    ]  # (Narea_parameters,); None when no quantification
    quantification_parameters: Union[QuantificationParameters, NotAvailable]

    @property
    def has_diagnostics(self) -> bool:
        return all(self.has_data(value) for value in ["spectra", "model", "residuals"])

    @property
    def num_spectra(self) -> int:
        if self.parameters.ndim == 2:
            return self.parameters.shape[1]
        return 0

    def has_data(self, name) -> bool:
        value = getattr(self, name)
        return value is not None and not isinstance(value, NotAvailable)
