from typing import Any
from typing import Dict
from typing import List
from typing import Literal
from typing import Optional
from typing import Union

from pydantic import BaseModel
from pydantic import Field
from pydantic import PrivateAttr


class PyMcaBaseModel(BaseModel, extra="allow", validate_assignment=True):
    pass


EnergyType = Union[float, None, Literal["None", "NONE"]]


class Fit(PyMcaBaseModel):
    deltaonepeak: Optional[float] = None
    strategy: Optional[str] = None
    strategyflag: Optional[int] = None
    fitfunction: Optional[int] = None
    continuum: Optional[int] = None
    fitweight: Optional[int] = None
    stripalgorithm: Optional[int] = Field(default=None, description="0: STRIP, 1: SNIP")
    linpolorder: Optional[int] = None
    exppolorder: Optional[int] = None
    stripconstant: Optional[float] = None
    snipwidth: Optional[int] = None
    stripiterations: Optional[int] = None
    stripwidth: Optional[int] = None
    stripfilterwidth: Optional[int] = None
    stripanchorsflag: Optional[int] = None
    maxiter: Optional[int] = None
    deltachi: Optional[float] = None
    xmin: Optional[int] = None
    xmax: Optional[int] = None
    linearfitflag: Optional[int] = None
    use_limit: Optional[int] = None
    stripflag: Optional[int] = None
    escapeflag: Optional[int] = None
    sumflag: Optional[int] = None
    scatterflag: Optional[int] = None
    hypermetflag: Optional[int] = None

    stripanchorslist: List[int] = Field(default_factory=list)

    energy: Union[List[EnergyType], EnergyType] = Field(default_factory=list)
    energyweight: Union[List[float], float] = Field(default_factory=list)
    energyflag: Union[List[int], int] = Field(default_factory=list)
    energyscatter: Union[List[int], int] = Field(default_factory=list)


class Concentrations(PyMcaBaseModel):
    usematrix: Optional[int] = None
    useattenuators: Optional[int] = None
    usemultilayersecondary: Optional[int] = None
    usexrfmc: Optional[int] = None
    mmolarflag: Optional[int] = None
    flux: Optional[float] = None
    time: Optional[float] = None
    area: Optional[float] = None
    distance: Optional[float] = None
    reference: Optional[str] = None
    useautotime: Optional[int] = None


class Detector(PyMcaBaseModel):
    detele: str
    nthreshold: int

    zero: float
    deltazero: float
    fixedzero: int

    gain: float
    deltagain: float
    fixedgain: int

    noise: float
    deltanoise: float
    fixednoise: int

    fano: float
    deltafano: float
    fixedfano: int

    sum: float
    deltasum: float
    fixedsum: int

    ignoreinputcalibration: Optional[int] = None

    ethreshold: Optional[float] = None
    ithreshold: Optional[float] = None


class PeakShape(PyMcaBaseModel):
    st_arearatio: float
    deltast_arearatio: float
    fixedst_arearatio: int

    st_sloperatio: float
    deltast_sloperatio: float
    fixedst_sloperatio: int

    lt_arearatio: float
    deltalt_arearatio: float
    fixedlt_arearatio: int

    lt_sloperatio: float
    deltalt_sloperatio: float
    fixedlt_sloperatio: int

    step_heightratio: float
    deltastep_heightratio: float
    fixedstep_heightratio: int

    eta_factor: Optional[float] = None
    deltaeta_factor: Optional[float] = None
    fixedeta_factor: Optional[int] = None


class Material(PyMcaBaseModel):
    Comment: Optional[str] = None
    Density: float
    Thickness: Optional[float] = None
    CompoundFraction: Union[float, Literal[""], List[float]]
    CompoundList: Union[str, List[str]] = Field(
        ..., description="key in materials or chemical formula"
    )


class UserAttenuator(PyMcaBaseModel):
    use: int
    name: str
    comment: Optional[str]
    energy: Union[float, List[float]]
    transmission: Union[float, List[float]]


class Tube(PyMcaBaseModel):
    transmission: int
    voltage: float
    anode: str
    anodethickness: float
    anodedensity: float
    window: str
    windowthickness: float
    windowdensity: float
    filter1: str
    filter1thickness: float
    filter1density: float
    alphax: float
    alphae: float
    deltaplotting: float


class XRFMCSetup(PyMcaBaseModel):
    p_polarisation: float
    source_sample_distance: float
    slit_distance: float
    slit_width_x: float
    slit_width_y: float
    source_size_x: float
    source_size_y: float
    source_diverg_x: float
    source_diverg_y: float
    nmax_interaction: int
    layer: int
    collimator_height: float
    collimator_diameter: float
    histories: int


class XRFMC(PyMcaBaseModel):
    program: Optional[str] = None
    setup: XRFMCSetup


class Fisx(PyMcaBaseModel):
    pass


class AttenuatorView(BaseModel):
    enabled: int
    material: Optional[str] = Field(
        ..., description="key in materials or chemical formula"
    )
    density: float = Field(..., description="g/cm^3")
    thickness: float = Field(..., description="cm")
    incidence_angle: Optional[float] = Field(
        None, description="Angle between incident beam and sample surface (degrees)"
    )
    exit_angle: Optional[float] = Field(
        None, description="Angle between outgoing beam and sample surface (degrees)"
    )
    scattering_flag: Optional[bool] = None
    scattering_angle: Optional[float] = Field(
        None, description="Angle between incident beam and outgoing beam (degrees)"
    )

    _raw: List[Any] = PrivateAttr()

    @classmethod
    def from_list(cls, data: List[Any]) -> "AttenuatorView":
        padded = (data + [None] * 8)[:8]
        obj = cls(
            enabled=padded[0],
            material=padded[1],
            density=padded[2],
            thickness=padded[3],
            incidence_angle=padded[4],
            exit_angle=padded[5],
            scattering_flag=padded[6],
            scattering_angle=padded[7],
        )
        obj._raw = data
        return obj

    def _sync(self) -> None:
        new = [
            self.enabled,
            self.material,
            self.density,
            self.thickness,
            self.incidence_angle,
            self.exit_angle,
            self.scattering_flag,
            self.scattering_angle,
        ]
        self._raw[:] = new[: len(self._raw)]

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        if name in type(self).model_fields:
            if hasattr(self, "_raw"):
                self._sync()


class MultilayerView(BaseModel):
    use: int
    material: str = Field(..., description="key in materials or chemical formula")
    density: float
    thickness: float

    _raw: List[Any] = PrivateAttr()

    @classmethod
    def from_list(cls, data: List[Any]) -> "MultilayerView":
        padded = (data + [None] * 4)[:4]
        obj = cls(
            use=padded[0],
            material=padded[1],
            density=padded[2],
            thickness=padded[3],
        )
        obj._raw = data
        return obj

    def _sync(self) -> None:
        self._raw[:] = [
            self.use,
            self.material,
            self.density,
            self.thickness,
        ]

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        if name in type(self).model_fields:
            if hasattr(self, "_raw"):
                self._sync()


class PyMcaXrfConfiguration(PyMcaBaseModel):
    peaks: Dict[str, Union[str, List[str]]] = Field(default_factory=dict)
    fit: Fit = Field(default_factory=dict)
    concentrations: Concentrations = Field(default_factory=dict)
    detector: Detector = Field(default_factory=dict)
    peakshape: PeakShape = Field(default_factory=dict)
    materials: Dict[str, Material] = Field(default_factory=dict)
    attenuators: Dict[str, List[Any]] = Field(default_factory=dict)
    multilayer: Dict[str, List[Any]] = Field(default_factory=dict)
    userattenuators: Dict[str, UserAttenuator] = Field(default_factory=dict)
    tube: Optional[Tube] = None
    xrfmc: Optional[XRFMC] = None
    fisx: Optional[Fisx] = None

    @property
    def attenuator_views(self) -> Dict[str, AttenuatorView]:
        return {
            name: AttenuatorView.from_list(values)
            for name, values in self.attenuators.items()
        }

    @property
    def multilayer_views(self) -> Dict[str, MultilayerView]:
        return {
            name: MultilayerView.from_list(values)
            for name, values in self.multilayer.items()
        }
