"""
IES TM-27-14 Data Input / Output
================================

Define the :class:`colour.SpectralDistribution_IESTM2714` class handling *IES
TM-27-14* spectral data *XML* files.

References
----------
-   :cite:`IESComputerCommittee2014a` : IES Computer Committee, & TM-27-14
    Working Group. (2014). IES Standard Format for the Electronic Transfer of
    Spectral Data Electronic Transfer of Spectral Data. Illuminating
    Engineering Society. ISBN:978-0-87995-295-2
"""

from __future__ import annotations

import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from xml.dom import minidom
from xml.etree import ElementTree

from colour.colorimetry import SpectralDistribution
from colour.hints import Any, Callable, Literal
from colour.utilities import (
    Structure,
    as_float_array,
    as_float_scalar,
    attest,
    is_numeric,
    multiline_repr,
    multiline_str,
    optional,
    tstack,
)

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "VERSION_IESTM2714",
    "NAMESPACE_IESTM2714",
    "Element_Specification_IESTM2714",
    "Header_IESTM2714",
    "SpectralDistribution_IESTM2714",
]

VERSION_IESTM2714: str = "1.0"

NAMESPACE_IESTM2714: str = "http://www.ies.org/iestm2714"


@dataclass
class Element_Specification_IESTM2714:
    """
    *IES TM-27-14* spectral data *XML* file element specification.

    Parameters
    ----------
    element
        Element name.
    attribute
        Associated attribute name.
    type_
        Element type.
    required
        Is element required.
    read_conversion
        Method to convert from *XML* to type on reading.
    write_conversion
        Method to convert from type to *XML* on writing.
    """

    element: str
    attribute: str
    type_: Any = field(default_factory=str)
    required: bool = field(default_factory=lambda: False)
    read_conversion: Callable = field(
        default_factory=lambda: lambda x: None if x == "None" else str(x)
    )
    write_conversion: Callable = field(default_factory=lambda: str)


class Header_IESTM2714:
    """
    Define the header object for a *IES TM-27-14* spectral distribution.

    Parameters
    ----------
    manufacturer
        Manufacturer of the device under test.
    catalog_number
        Manufacturer's product catalog number.
    description
        Description of the spectral data in the spectral data *XML* file.
    document_creator
        Creator of the spectral data *XML* file, which may be a test lab, a
        research group, a standard body, a company or an individual.
    unique_identifier
        Unique identifier to the product under test or the spectral data in the
        document.
    measurement_equipment
        Description of the equipment used to measure the spectral data.
    laboratory
        Testing laboratory name that performed the spectral data measurements.
    report_number
        Testing laboratory report number.
    report_date
        Testing laboratory report date using the *XML DateTime Data Type*,
        *YYYY-MM-DDThh:mm:ss*.
    document_creation_date
        Spectral data *XML* file creation date using the
        *XML DateTime Data Type*, *YYYY-MM-DDThh:mm:ss*.
    comments
        Additional information relating to the tested and reported data.

    Attributes
    ----------
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.mapping`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.manufacturer`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.catalog_number`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.description`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.document_creator`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.unique_identifier`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.measurement_equipment`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.laboratory`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.report_number`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.report_date`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.document_creation_date`
    -   :attr:`~colour.io.ies_tm2714.Header_IESTM2714.comments`

    Methods
    -------
    -   :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__init__`
    -   :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__str__`
    -   :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__repr__`
    -   :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__hash__`
    -   :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__eq__`
    -   :meth:`~colour.io.ies_tm2714.Header_IESTM2714.__ne__`

    Examples
    --------
    >>> Header_IESTM2714("colour-science")  # doctest: +ELLIPSIS
    Header_IESTM2714('colour-science',
                     None,
                     None,
                     None,
                     None,
                     None,
                     None,
                     None,
                     None,
                     None,
                     None)
    >>> Header_IESTM2714("colour-science").manufacturer  # doctest: +SKIP
    'colour-science'
    """

    def __init__(
        self,
        manufacturer: str | None = None,
        catalog_number: str | None = None,
        description: str | None = None,
        document_creator: str | None = None,
        unique_identifier: str | None = None,
        measurement_equipment: str | None = None,
        laboratory: str | None = None,
        report_number: str | None = None,
        report_date: str | None = None,
        document_creation_date: str | None = None,
        comments: str | None = None,
    ) -> None:
        self._mapping: Structure = Structure(
            **{
                "element": "Header",
                "elements": (
                    Element_Specification_IESTM2714("Manufacturer", "manufacturer"),
                    Element_Specification_IESTM2714("CatalogNumber", "catalog_number"),
                    Element_Specification_IESTM2714(
                        "Description", "description", required=True
                    ),
                    Element_Specification_IESTM2714(
                        "DocumentCreator", "document_creator", required=True
                    ),
                    Element_Specification_IESTM2714(
                        "UniqueIdentifier", "unique_identifier"
                    ),
                    Element_Specification_IESTM2714(
                        "MeasurementEquipment", "measurement_equipment"
                    ),
                    Element_Specification_IESTM2714("Laboratory", "laboratory"),
                    Element_Specification_IESTM2714("ReportNumber", "report_number"),
                    Element_Specification_IESTM2714("ReportDate", "report_date"),
                    Element_Specification_IESTM2714(
                        "DocumentCreationDate",
                        "document_creation_date",
                        required=True,
                    ),
                    Element_Specification_IESTM2714("Comments", "comments", False),
                ),
            }
        )

        self._manufacturer: str | None = None
        self.manufacturer = manufacturer
        self._catalog_number: str | None = None
        self.catalog_number = catalog_number
        self._description: str | None = None
        self.description = description
        self._document_creator: str | None = None
        self.document_creator = document_creator
        self._unique_identifier: str | None = None
        self.unique_identifier = unique_identifier
        self._measurement_equipment: str | None = None
        self.measurement_equipment = measurement_equipment
        self._laboratory: str | None = None
        self.laboratory = laboratory
        self._report_number: str | None = None
        self.report_number = report_number
        self._report_date: str | None = None
        self.report_date = report_date
        self._document_creation_date: str | None = None
        self.document_creation_date = document_creation_date
        self._comments: str | None = None
        self.comments = comments

    @property
    def mapping(self) -> Structure:
        """
        Getter property for the mapping structure.

        Returns
        -------
        :class:`colour.utilities.Structure`
            Mapping structure.
        """

        return self._mapping

    @property
    def manufacturer(self) -> str | None:
        """
        Getter and setter property for the manufacturer.

        Parameters
        ----------
        value
            Value to set the manufacturer with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Manufacturer.
        """

        return self._manufacturer

    @manufacturer.setter
    def manufacturer(self, value: str | None):
        """Setter for the **self.manufacturer** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"manufacturer" property: "{value}" type is not "str"!',
            )

        self._manufacturer = value

    @property
    def catalog_number(self) -> str | None:
        """
        Getter and setter property for the catalog number.

        Parameters
        ----------
        value
            Value to set the catalog number with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Catalog number.
        """

        return self._catalog_number

    @catalog_number.setter
    def catalog_number(self, value: str | None):
        """Setter for the **self.catalog_number** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"catalog_number" property: "{value}" type is not "str"!',
            )

        self._catalog_number = value

    @property
    def description(self) -> str | None:
        """
        Getter and setter property for the description.

        Parameters
        ----------
        value
            Value to set the description with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Description.
        """

        return self._description

    @description.setter
    def description(self, value: str | None):
        """Setter for the **self.description** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"description" property: "{value}" type is not "str"!',
            )

        self._description = value

    @property
    def document_creator(self) -> str | None:
        """
        Getter and setter property for the document creator.

        Parameters
        ----------
        value
            Value to set the document creator with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Document creator.
        """

        return self._document_creator

    @document_creator.setter
    def document_creator(self, value: str | None):
        """Setter for the **self.document_creator** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"document_creator" property: "{value}" type is not "str"!',
            )

        self._document_creator = value

    @property
    def unique_identifier(self) -> str | None:
        """
        Getter and setter property for the unique identifier.

        Parameters
        ----------
        value
            Value to set the unique identifier with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Unique identifier.
        """

        return self._unique_identifier

    @unique_identifier.setter
    def unique_identifier(self, value: str | None):
        """Setter for the **self.unique_identifier** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"unique_identifier" property: "{value}" type is not "str"!',
            )

        self._unique_identifier = value

    @property
    def measurement_equipment(self) -> str | None:
        """
        Getter and setter property for the measurement equipment.

        Parameters
        ----------
        value
            Value to set the measurement equipment with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Measurement equipment.
        """

        return self._measurement_equipment

    @measurement_equipment.setter
    def measurement_equipment(self, value: str | None):
        """Setter for the **self.measurement_equipment** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"measurement_equipment" property: "{value}" type is not "str"!',
            )

        self._measurement_equipment = value

    @property
    def laboratory(self) -> str | None:
        """
        Getter and setter property for the laboratory.

        Parameters
        ----------
        value
            Value to set the laboratory with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Laboratory.
        """

        return self._laboratory

    @laboratory.setter
    def laboratory(self, value: str | None):
        """Setter for the **self.measurement_equipment** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"laboratory" property: "{value}" type is not "str"!',
            )

        self._laboratory = value

    @property
    def report_number(self) -> str | None:
        """
        Getter and setter property for the report number.

        Parameters
        ----------
        value
            Value to set the report number with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Report number.
        """

        return self._report_number

    @report_number.setter
    def report_number(self, value: str | None):
        """Setter for the **self.report_number** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"report_number" property: "{value}" type is not "str"!',
            )

        self._report_number = value

    @property
    def report_date(self) -> str | None:
        """
        Getter and setter property for the report date.

        Parameters
        ----------
        value
            Value to set the report date with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Report date.
        """

        return self._report_date

    @report_date.setter
    def report_date(self, value: str | None):
        """Setter for the **self.report_date** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"report_date" property: "{value}" type is not "str"!',
            )

        self._report_date = value

    @property
    def document_creation_date(self) -> str | None:
        """
        Getter and setter property for the document creation date.

        Parameters
        ----------
        value
            Value to set the document creation date with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Document creation date.
        """

        return self._document_creation_date

    @document_creation_date.setter
    def document_creation_date(self, value: str | None):
        """Setter for the **self.document_creation_date** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"document_creation_date" property: "{value}" type is not "str"!',
            )

        self._document_creation_date = value

    @property
    def comments(self) -> str | None:
        """
        Getter and setter property for the comments.

        Parameters
        ----------
        value
            Value to set the comments with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Comments.
        """

        return self._comments

    @comments.setter
    def comments(self, value: str | None):
        """Setter for the **self.comments** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"comments" property: "{value}" type is not "str"!',
            )

        self._comments = value

    def __str__(self) -> str:
        """
        Return a formatted string representation of the header.

        Returns
        -------
        :class:`str`
            Formatted string representation.

        Examples
        --------
        >>> print(Header_IESTM2714("colour-science"))
        Manufacturer           : colour-science
        Catalog Number         : None
        Description            : None
        Document Creator       : None
        Unique Identifier      : None
        Measurement Equipment  : None
        Laboratory             : None
        Report Number          : None
        Report Date            : None
        Document Creation Date : None
        Comments               : None
        """

        return multiline_str(
            self,
            [
                {"name": "_manufacturer", "label": "Manufacturer"},
                {"name": "_catalog_number", "label": "Catalog Number"},
                {"name": "_description", "label": "Description"},
                {"name": "_document_creator", "label": "Document Creator"},
                {"name": "_unique_identifier", "label": "Unique Identifier"},
                {
                    "name": "_measurement_equipment",
                    "label": "Measurement Equipment",
                },
                {"name": "_laboratory", "label": "Laboratory"},
                {"name": "_report_number", "label": "Report Number"},
                {"name": "_report_date", "label": "Report Date"},
                {
                    "name": "_document_creation_date",
                    "label": "Document Creation Date",
                },
                {"name": "_comments", "label": "Comments"},
            ],
        )

    def __repr__(self) -> str:
        """
        Return an evaluable string representation of the header.

        Returns
        -------
        :class:`str`
            Evaluable string representation.

        Examples
        --------
        >>> Header_IESTM2714("colour-science")
        Header_IESTM2714('colour-science',
                         None,
                         None,
                         None,
                         None,
                         None,
                         None,
                         None,
                         None,
                         None,
                         None)
        """

        return multiline_repr(
            self,
            [
                {"name": "_manufacturer"},
                {"name": "_catalog_number"},
                {"name": "_description"},
                {"name": "_document_creator"},
                {"name": "_unique_identifier"},
                {"name": "_measurement_equipment"},
                {"name": "_laboratory"},
                {"name": "_report_number"},
                {"name": "_report_date"},
                {"name": "_document_creation_date"},
                {"name": "_comments"},
            ],
        )

    def __hash__(self) -> int:
        """
        Return the header hash.

        Returns
        -------
        :class:`int`
            Object hash.
        """

        return hash(
            (
                self._manufacturer,
                self._catalog_number,
                self._description,
                self._document_creator,
                self._unique_identifier,
                self._measurement_equipment,
                self._laboratory,
                self._report_number,
                self._report_date,
                self._document_creation_date,
                self._comments,
            )
        )

    def __eq__(self, other: Any) -> bool:
        """
        Return whether the header is equal to given other object.

        Parameters
        ----------
        other
            Object to test whether it is equal to the header.

        Returns
        -------
        :class:`bool`
            Whether given object is equal to the header.

        Examples
        --------
        >>> Header_IESTM2714("Foo") == Header_IESTM2714("Foo")
        True
        >>> Header_IESTM2714("Foo") == Header_IESTM2714("Bar")
        False
        """

        if isinstance(other, Header_IESTM2714):
            return all(
                [
                    self._manufacturer == other.manufacturer,
                    self._catalog_number == other.catalog_number,
                    self._description == other.description,
                    self._document_creator == other.document_creator,
                    self._unique_identifier == other.unique_identifier,
                    self._measurement_equipment == other.measurement_equipment,
                    self._laboratory == other.laboratory,
                    self._report_number == other.report_number,
                    self._report_date == other.report_date,
                    self._document_creation_date == other.document_creation_date,
                    self._comments == other.comments,
                ]
            )
        return False

    def __ne__(self, other: Any) -> bool:
        """
        Return whether the header is not equal to given other object.

        Parameters
        ----------
        other
            Object to test whether it is not equal to the header.

        Returns
        -------
        :class:`bool`
            Whether given object is not equal to the header.

        Examples
        --------
        >>> Header_IESTM2714("Foo") != Header_IESTM2714("Foo")
        False
        >>> Header_IESTM2714("Foo") != Header_IESTM2714("Bar")
        True
        """

        return not (self == other)


class SpectralDistribution_IESTM2714(SpectralDistribution):
    """
    Define a *IES TM-27-14* spectral distribution.

    This class can read and write *IES TM-27-14* spectral data *XML* files.

    Parameters
    ----------
    path
        Spectral data *XML* file path.
    header
        *IES TM-27-14* spectral distribution header.
    spectral_quantity
        Quantity of measurement for each element of the spectral data.
    reflection_geometry
        Spectral reflectance factors geometric conditions.
    transmission_geometry
        Spectral transmittance factors geometric conditions.
    bandwidth_FWHM
        Spectroradiometer full-width half-maximum bandwidth in nanometers.
    bandwidth_corrected
        Specifies if bandwidth correction has been applied to the measured
        data.

    Other Parameters
    ----------------
    data
        Data to be stored in the spectral distribution.
    domain
        Values to initialise the
        :attr:`colour.SpectralDistribution.wavelength` property with.
        If both ``data`` and ``domain`` arguments are defined, the latter will
        be used to initialise the
        :attr:`colour.SpectralDistribution.wavelength` property.
    extrapolator
        Extrapolator class type to use as extrapolating function.
    extrapolator_kwargs
        Arguments to use when instantiating the extrapolating function.
    interpolator
        Interpolator class type to use as interpolating function.
    interpolator_kwargs
        Arguments to use when instantiating the interpolating function.
    name
        Spectral distribution name.
    display_name
        Spectral distribution name for figures, default to
        :attr:`colour.SpectralDistribution.name` property value.

    Notes
    -----
    *Reflection Geometry*

    -   di:8: Diffuse / eight-degree, specular component included.
    -   de:8: Diffuse / eight-degree, specular component excluded.
    -   8:di: Eight-degree / diffuse, specular component included.
    -   8:de: Eight-degree / diffuse, specular component excluded.
    -   d:d: Diffuse / diffuse.
    -   d:0: Alternative diffuse.
    -   45a:0: Forty-five degree annular / normal.
    -   45c:0: Forty-five degree circumferential / normal.
    -   0:45a: Normal / forty-five degree annular.
    -   45x:0: Forty-five degree directional / normal.
    -   0:45x: Normal / forty-five degree directional.
    -   other: User-specified in comments.

    *Transmission Geometry*

    -   0:0: Normal / normal.
    -   di:0: Diffuse / normal, regular component included.
    -   de:0: Diffuse / normal, regular component excluded.
    -   0:di: Normal / diffuse, regular component included.
    -   0:de: Normal / diffuse, regular component excluded.
    -   d:d: Diffuse / diffuse.
    -   other: User-specified in comments.

    Attributes
    ----------
    -   :attr:`~colour.SpectralDistribution_IESTM2714.mapping`
    -   :attr:`~colour.SpectralDistribution_IESTM2714.path`
    -   :attr:`~colour.SpectralDistribution_IESTM2714.header`
    -   :attr:`~colour.SpectralDistribution_IESTM2714.spectral_quantity`
    -   :attr:`~colour.SpectralDistribution_IESTM2714.reflection_geometry`
    -   :attr:`~colour.SpectralDistribution_IESTM2714.transmission_geometry`
    -   :attr:`~colour.SpectralDistribution_IESTM2714.bandwidth_FWHM`
    -   :attr:`~colour.SpectralDistribution_IESTM2714.bandwidth_corrected`

    Methods
    -------
    -   :meth:`~colour.SpectralDistribution_IESTM2714.__init__`
    -   :meth:`~colour.SpectralDistribution_IESTM2714.__str__`
    -   :meth:`~colour.SpectralDistribution_IESTM2714.__repr__`
    -   :meth:`~colour.SpectralDistribution_IESTM2714.read`
    -   :meth:`~colour.SpectralDistribution_IESTM2714.write`

    References
    ----------
    :cite:`IESComputerCommittee2014a`

    Examples
    --------
    >>> from os.path import dirname, join
    >>> directory = join(dirname(__file__), "tests", "resources")
    >>> sd = SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx"))
    >>> sd.name  # doctest: +SKIP
    'Unknown - N/A - Rare earth fluorescent lamp'
    >>> sd.header.comments
    'Ambient temperature 25 degrees C.'
    >>> sd[501.7]  # doctest: +ELLIPSIS
    0.0950000...
    """

    def __init__(
        self,
        path: str | Path | None = None,
        header: Header_IESTM2714 | None = None,
        spectral_quantity: (
            Literal[
                "absorptance",
                "exitance",
                "flux",
                "intensity",
                "irradiance",
                "radiance",
                "reflectance",
                "relative",
                "transmittance",
                "R-Factor",
                "T-Factor",
                "other",
            ]
            | None
        ) = None,
        reflection_geometry: (
            Literal[
                "di:8",
                "de:8",
                "8:di",
                "8:de",
                "d:d",
                "d:0",
                "45a:0",
                "45c:0",
                "0:45a",
                "45x:0",
                "0:45x",
                "other",
            ]
            | None
        ) = None,
        transmission_geometry: (
            Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None
        ) = None,
        bandwidth_FWHM: float | None = None,
        bandwidth_corrected: bool | None = None,
        **kwargs,
    ) -> None:
        super().__init__(**kwargs)

        self._mapping: Structure = Structure(
            **{
                "element": "SpectralDistribution",
                "elements": (
                    Element_Specification_IESTM2714(
                        "SpectralQuantity", "spectral_quantity", required=True
                    ),
                    Element_Specification_IESTM2714(
                        "ReflectionGeometry", "reflection_geometry"
                    ),
                    Element_Specification_IESTM2714(
                        "TransmissionGeometry", "transmission_geometry"
                    ),
                    Element_Specification_IESTM2714(
                        "BandwidthFWHM",
                        "bandwidth_FWHM",
                        read_conversion=(
                            lambda x: (None if x == "None" else as_float_scalar(x))
                        ),
                    ),
                    Element_Specification_IESTM2714(
                        "BandwidthCorrected",
                        "bandwidth_corrected",
                        read_conversion=(lambda x: bool(x == "true")),
                        write_conversion=(lambda x: "true" if x is True else "false"),
                    ),
                ),
                "data": Element_Specification_IESTM2714(
                    "SpectralData", "wavelength", required=True
                ),
            }
        )

        self._path: str | None = None
        self.path = path
        self._header: Header_IESTM2714 = Header_IESTM2714()
        self.header = optional(header, self._header)
        self._spectral_quantity: (
            Literal[
                "absorptance",
                "exitance",
                "flux",
                "intensity",
                "irradiance",
                "radiance",
                "reflectance",
                "relative",
                "transmittance",
                "R-Factor",
                "T-Factor",
                "other",
            ]
            | None
        ) = None
        self.spectral_quantity = spectral_quantity
        self._reflection_geometry: (
            Literal[
                "di:8",
                "de:8",
                "8:di",
                "8:de",
                "d:d",
                "d:0",
                "45a:0",
                "45c:0",
                "0:45a",
                "45x:0",
                "0:45x",
                "other",
            ]
            | None
        ) = None
        self.reflection_geometry = reflection_geometry
        self._transmission_geometry: (
            Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None
        ) = None
        self.transmission_geometry = transmission_geometry
        self._bandwidth_FWHM: float | None = None
        self.bandwidth_FWHM = bandwidth_FWHM
        self._bandwidth_corrected: bool | None = None
        self.bandwidth_corrected = bandwidth_corrected

        if self.path is not None and os.path.exists(
            self.path  # pyright: ignore
        ):
            self.read()

    @property
    def mapping(self) -> Structure:
        """
        Getter property for the mapping structure.

        Returns
        -------
        :class:`colour.utilities.Structure`
            Mapping structure.
        """

        return self._mapping

    @property
    def path(self) -> str | None:
        """
        Getter and setter property for the path.

        Parameters
        ----------
        value
            Value to set the path with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Path.
        """

        return self._path

    @path.setter
    def path(self, value: str | Path | None):
        """Setter for the **self.path** property."""

        if value is not None:
            attest(
                isinstance(value, (str, Path)),
                f'"path" property: "{value}" type is not "str" or "Path"!',
            )

            value = str(value)

        self._path = value

    @property
    def header(self) -> Header_IESTM2714:
        """
        Getter and setter property for the header.

        Parameters
        ----------
        value
            Value to set the header with.

        Returns
        -------
        :class:`colour.io.tm2714.Header_IESTM2714`
            Header.
        """

        return self._header

    @header.setter
    def header(self, value: Header_IESTM2714):
        """Setter for the **self.header** property."""

        attest(
            isinstance(value, Header_IESTM2714),
            f'"header" property: "{value}" type is not "Header_IESTM2714"!',
        )

        self._header = value

    @property
    def spectral_quantity(
        self,
    ) -> (
        Literal[
            "absorptance",
            "exitance",
            "flux",
            "intensity",
            "irradiance",
            "radiance",
            "reflectance",
            "relative",
            "transmittance",
            "R-Factor",
            "T-Factor",
            "other",
        ]
        | None
    ):
        """
        Getter and setter property for the spectral quantity.

        Parameters
        ----------
        value
            Value to set the spectral quantity with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Spectral quantity.
        """

        return self._spectral_quantity

    @spectral_quantity.setter
    def spectral_quantity(
        self,
        value: (
            Literal[
                "absorptance",
                "exitance",
                "flux",
                "intensity",
                "irradiance",
                "radiance",
                "reflectance",
                "relative",
                "transmittance",
                "R-Factor",
                "T-Factor",
                "other",
            ]
            | None
        ),
    ):
        """Setter for the **self.spectral_quantity** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"spectral_quantity" property: "{value}" type is not "str"!',
            )

        self._spectral_quantity = value

    @property
    def reflection_geometry(
        self,
    ) -> (
        Literal[
            "di:8",
            "de:8",
            "8:di",
            "8:de",
            "d:d",
            "d:0",
            "45a:0",
            "45c:0",
            "0:45a",
            "45x:0",
            "0:45x",
            "other",
        ]
        | None
    ):
        """
        Getter and setter property for the reflection geometry.

        Parameters
        ----------
        value
            Value to set the reflection geometry with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Reflection geometry.
        """

        return self._reflection_geometry

    @reflection_geometry.setter
    def reflection_geometry(
        self,
        value: (
            Literal[
                "di:8",
                "de:8",
                "8:di",
                "8:de",
                "d:d",
                "d:0",
                "45a:0",
                "45c:0",
                "0:45a",
                "45x:0",
                "0:45x",
                "other",
            ]
            | None
        ),
    ):
        """Setter for the **self.reflection_geometry** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"reflection_geometry" property: "{value}" type is not "str"!',
            )

        self._reflection_geometry = value

    @property
    def transmission_geometry(
        self,
    ) -> Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None:
        """
        Getter and setter property for the transmission geometry.

        Parameters
        ----------
        value
            Value to set the transmission geometry with.

        Returns
        -------
        :py:data:`None` or :class:`str`
            Transmission geometry.
        """

        return self._transmission_geometry

    @transmission_geometry.setter
    def transmission_geometry(
        self,
        value: (Literal["0:0", "di:0", "de:0", "0:di", "0:de", "d:d", "other"] | None),
    ):
        """Setter for the **self.transmission_geometry** property."""

        if value is not None:
            attest(
                isinstance(value, str),
                f'"transmission_geometry" property: "{value}" type is not "str"!',
            )

        self._transmission_geometry = value

    @property
    def bandwidth_FWHM(self) -> float | None:
        """
        Getter and setter property for the full-width half-maximum bandwidth.

        Parameters
        ----------
        value
            Value to set the full-width half-maximum bandwidth with.

        Returns
        -------
        :py:data:`None` or :class:`float`
            Full-width half-maximum bandwidth.
        """

        return self._bandwidth_FWHM

    @bandwidth_FWHM.setter
    def bandwidth_FWHM(self, value: float | None):
        """Setter for the **self.bandwidth_FWHM** property."""

        if value is not None:
            attest(
                is_numeric(value),
                f'"bandwidth_FWHM" property: "{value}" is not a "number"!',
            )

            value = as_float_scalar(value)

        self._bandwidth_FWHM = value

    @property
    def bandwidth_corrected(self) -> bool | None:
        """
        Getter and setter property for whether bandwidth correction has been
        applied to the measured data.

        Parameters
        ----------
        value
            Whether bandwidth correction has been applied to the measured data.

        Returns
        -------
        :py:data:`None` or :class:`bool`
            Whether bandwidth correction has been applied to the measured data.
        """

        return self._bandwidth_corrected

    @bandwidth_corrected.setter
    def bandwidth_corrected(self, value: bool | None):
        """Setter for the **self.bandwidth_corrected** property."""

        if value is not None:
            attest(
                isinstance(value, bool),
                f'"bandwidth_corrected" property: "{value}" type is not "bool"!',
            )

        self._bandwidth_corrected = value

    def __str__(self) -> str:
        """
        Return a formatted string representation of the *IES TM-27-14*
        spectral distribution.

        Returns
        -------
        :class:`str`
            Formatted string representation.

        Examples
        --------
        >>> from os.path import dirname, join
        >>> directory = join(dirname(__file__), "tests", "resources")
        >>> print(SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx")))
        ... # doctest: +ELLIPSIS
        IES TM-27-14 Spectral Distribution
        ==================================
        <BLANKLINE>
        Path                  : ...
        Spectral Quantity     : relative
        Reflection Geometry   : other
        Transmission Geometry : other
        Bandwidth (FWHM)      : 2.0
        Bandwidth Corrected   : True
        <BLANKLINE>
        Header
        ------
        <BLANKLINE>
        Manufacturer           : Unknown
        Catalog Number         : N/A
        Description            : Rare earth fluorescent lamp
        Document Creator       : byHeart Consultants
        Unique Identifier      : C3567553-C75B-4354-961E-35CEB9FEB42C
        Measurement Equipment  : None
        Laboratory             : N/A
        Report Number          : N/A
        Report Date            : N/A
        Document Creation Date : 2014-06-23
        Comments               : Ambient temperature 25 degrees C.
        <BLANKLINE>
        Spectral Data
        -------------
        <BLANKLINE>
        [[  4.00000000e+02   3.40000000e-02]
         [  4.03100000e+02   3.70000000e-02]
         [  4.05500000e+02   6.90000000e-02]
         [  4.07500000e+02   3.70000000e-02]
         [  4.20600000e+02   4.20000000e-02]
         [  4.31000000e+02   4.90000000e-02]
         [  4.33700000e+02   6.00000000e-02]
         [  4.37000000e+02   3.57000000e-01]
         [  4.38900000e+02   6.00000000e-02]
         [  4.60000000e+02   6.80000000e-02]
         [  4.77000000e+02   7.50000000e-02]
         [  4.81000000e+02   8.50000000e-02]
         [  4.88200000e+02   2.04000000e-01]
         [  4.92600000e+02   1.66000000e-01]
         [  5.01700000e+02   9.50000000e-02]
         [  5.07600000e+02   7.80000000e-02]
         [  5.17600000e+02   7.10000000e-02]
         [  5.29900000e+02   7.60000000e-02]
         [  5.35400000e+02   9.90000000e-02]
         [  5.39900000e+02   4.23000000e-01]
         [  5.43200000e+02   8.02000000e-01]
         [  5.44400000e+02   7.13000000e-01]
         [  5.47200000e+02   9.99000000e-01]
         [  5.48700000e+02   5.73000000e-01]
         [  5.50200000e+02   3.40000000e-01]
         [  5.53800000e+02   2.08000000e-01]
         [  5.57300000e+02   1.39000000e-01]
         [  5.63700000e+02   1.29000000e-01]
         [  5.74800000e+02   1.31000000e-01]
         [  5.78000000e+02   1.98000000e-01]
         [  5.79200000e+02   1.90000000e-01]
         [  5.80400000e+02   2.05000000e-01]
         [  5.84800000e+02   2.44000000e-01]
         [  5.85900000e+02   2.36000000e-01]
         [  5.87500000e+02   2.56000000e-01]
         [  5.90300000e+02   1.80000000e-01]
         [  5.93500000e+02   2.18000000e-01]
         [  5.95500000e+02   1.59000000e-01]
         [  5.97000000e+02   1.47000000e-01]
         [  5.99400000e+02   1.70000000e-01]
         [  6.02200000e+02   1.34000000e-01]
         [  6.04600000e+02   1.21000000e-01]
         [  6.07400000e+02   1.40000000e-01]
         [  6.09400000e+02   2.29000000e-01]
         [  6.10200000e+02   4.65000000e-01]
         [  6.12000000e+02   9.52000000e-01]
         [  6.14600000e+02   4.77000000e-01]
         [  6.16900000e+02   2.08000000e-01]
         [  6.18500000e+02   1.35000000e-01]
         [  6.22100000e+02   1.50000000e-01]
         [  6.25600000e+02   1.55000000e-01]
         [  6.28400000e+02   1.34000000e-01]
         [  6.31200000e+02   1.68000000e-01]
         [  6.33200000e+02   8.70000000e-02]
         [  6.35600000e+02   6.80000000e-02]
         [  6.42700000e+02   5.80000000e-02]
         [  6.48700000e+02   5.80000000e-02]
         [  6.50700000e+02   7.40000000e-02]
         [  6.52600000e+02   6.30000000e-02]
         [  6.56200000e+02   5.30000000e-02]
         [  6.57000000e+02   5.60000000e-02]
         [  6.60600000e+02   4.90000000e-02]
         [  6.62600000e+02   5.90000000e-02]
         [  6.64200000e+02   4.80000000e-02]
         [  6.86000000e+02   4.10000000e-02]
         [  6.87600000e+02   4.80000000e-02]
         [  6.89200000e+02   3.90000000e-02]
         [  6.92400000e+02   3.80000000e-02]
         [  6.93500000e+02   4.40000000e-02]
         [  6.95500000e+02   3.40000000e-02]
         [  7.02300000e+02   3.60000000e-02]
         [  7.06700000e+02   4.20000000e-02]
         [  7.07100000e+02   6.10000000e-02]
         [  7.10200000e+02   6.10000000e-02]
         [  7.11000000e+02   4.10000000e-02]
         [  7.12200000e+02   5.20000000e-02]
         [  7.14200000e+02   3.30000000e-02]
         [  7.48400000e+02   3.40000000e-02]
         [  7.57900000e+02   3.10000000e-02]
         [  7.60700000e+02   3.90000000e-02]
         [  7.63900000e+02   2.90000000e-02]
         [  8.08800000e+02   2.90000000e-02]
         [  8.10700000e+02   3.90000000e-02]
         [  8.12700000e+02   3.00000000e-02]
         [  8.50100000e+02   3.00000000e-02]]
        """

        try:
            str_parent = super().__str__()

            return multiline_str(
                self,
                [
                    {
                        "label": "IES TM-27-14 Spectral Distribution",
                        "header": True,
                    },
                    {"line_break": True},
                    {"name": "path", "label": "Path"},
                    {
                        "name": "spectral_quantity",
                        "label": "Spectral Quantity",
                    },
                    {
                        "name": "reflection_geometry",
                        "label": "Reflection Geometry",
                    },
                    {
                        "name": "transmission_geometry",
                        "label": "Transmission Geometry",
                    },
                    {"name": "bandwidth_FWHM", "label": "Bandwidth (FWHM)"},
                    {
                        "name": "bandwidth_corrected",
                        "label": "Bandwidth Corrected",
                    },
                    {"line_break": True},
                    {"label": "Header", "section": True},
                    {"line_break": True},
                    {"formatter": lambda x: str(self.header)},  # noqa: ARG005
                    {"line_break": True},
                    {"label": "Spectral Data", "section": True},
                    {"line_break": True},
                    {"formatter": lambda x: str_parent},  # noqa: ARG005
                ],
            )
        except TypeError:  # pragma: no cover
            return super().__str__()

    def __repr__(self) -> str:
        """
        Return an evaluable string representation of the *IES TM-27-14*
        spectral distribution.

        Returns
        -------
        :class:`str`
            Evaluable string representation.

        Examples
        --------
        >>> from os.path import dirname, join
        >>> directory = join(dirname(__file__), "tests", "resources")
        >>> SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx"))
        ... # doctest: +ELLIPSIS
        SpectralDistribution_IESTM2714('...',
                                       Header_IESTM2714('Unknown',
                                                        'N/A',
                                                        'Rare earth ...',
                                                        'byHeart Consultants',
                                                        'C3567553-C75B-...',
                                                        None,
                                                        'N/A',
                                                        'N/A',
                                                        'N/A',
                                                        '2014-06-23',
                                                        'Ambient ...'),
                                       'relative',
                                       'other',
                                       'other',
                                       2.0,
                                       True,
                                       [[  4.00000000e+02,   3.40000000e-02],
                                        [  4.03100000e+02,   3.70000000e-02],
                                        [  4.05500000e+02,   6.90000000e-02],
                                        [  4.07500000e+02,   3.70000000e-02],
                                        [  4.20600000e+02,   4.20000000e-02],
                                        [  4.31000000e+02,   4.90000000e-02],
                                        [  4.33700000e+02,   6.00000000e-02],
                                        [  4.37000000e+02,   3.57000000e-01],
                                        [  4.38900000e+02,   6.00000000e-02],
                                        [  4.60000000e+02,   6.80000000e-02],
                                        [  4.77000000e+02,   7.50000000e-02],
                                        [  4.81000000e+02,   8.50000000e-02],
                                        [  4.88200000e+02,   2.04000000e-01],
                                        [  4.92600000e+02,   1.66000000e-01],
                                        [  5.01700000e+02,   9.50000000e-02],
                                        [  5.07600000e+02,   7.80000000e-02],
                                        [  5.17600000e+02,   7.10000000e-02],
                                        [  5.29900000e+02,   7.60000000e-02],
                                        [  5.35400000e+02,   9.90000000e-02],
                                        [  5.39900000e+02,   4.23000000e-01],
                                        [  5.43200000e+02,   8.02000000e-01],
                                        [  5.44400000e+02,   7.13000000e-01],
                                        [  5.47200000e+02,   9.99000000e-01],
                                        [  5.48700000e+02,   5.73000000e-01],
                                        [  5.50200000e+02,   3.40000000e-01],
                                        [  5.53800000e+02,   2.08000000e-01],
                                        [  5.57300000e+02,   1.39000000e-01],
                                        [  5.63700000e+02,   1.29000000e-01],
                                        [  5.74800000e+02,   1.31000000e-01],
                                        [  5.78000000e+02,   1.98000000e-01],
                                        [  5.79200000e+02,   1.90000000e-01],
                                        [  5.80400000e+02,   2.05000000e-01],
                                        [  5.84800000e+02,   2.44000000e-01],
                                        [  5.85900000e+02,   2.36000000e-01],
                                        [  5.87500000e+02,   2.56000000e-01],
                                        [  5.90300000e+02,   1.80000000e-01],
                                        [  5.93500000e+02,   2.18000000e-01],
                                        [  5.95500000e+02,   1.59000000e-01],
                                        [  5.97000000e+02,   1.47000000e-01],
                                        [  5.99400000e+02,   1.70000000e-01],
                                        [  6.02200000e+02,   1.34000000e-01],
                                        [  6.04600000e+02,   1.21000000e-01],
                                        [  6.07400000e+02,   1.40000000e-01],
                                        [  6.09400000e+02,   2.29000000e-01],
                                        [  6.10200000e+02,   4.65000000e-01],
                                        [  6.12000000e+02,   9.52000000e-01],
                                        [  6.14600000e+02,   4.77000000e-01],
                                        [  6.16900000e+02,   2.08000000e-01],
                                        [  6.18500000e+02,   1.35000000e-01],
                                        [  6.22100000e+02,   1.50000000e-01],
                                        [  6.25600000e+02,   1.55000000e-01],
                                        [  6.28400000e+02,   1.34000000e-01],
                                        [  6.31200000e+02,   1.68000000e-01],
                                        [  6.33200000e+02,   8.70000000e-02],
                                        [  6.35600000e+02,   6.80000000e-02],
                                        [  6.42700000e+02,   5.80000000e-02],
                                        [  6.48700000e+02,   5.80000000e-02],
                                        [  6.50700000e+02,   7.40000000e-02],
                                        [  6.52600000e+02,   6.30000000e-02],
                                        [  6.56200000e+02,   5.30000000e-02],
                                        [  6.57000000e+02,   5.60000000e-02],
                                        [  6.60600000e+02,   4.90000000e-02],
                                        [  6.62600000e+02,   5.90000000e-02],
                                        [  6.64200000e+02,   4.80000000e-02],
                                        [  6.86000000e+02,   4.10000000e-02],
                                        [  6.87600000e+02,   4.80000000e-02],
                                        [  6.89200000e+02,   3.90000000e-02],
                                        [  6.92400000e+02,   3.80000000e-02],
                                        [  6.93500000e+02,   4.40000000e-02],
                                        [  6.95500000e+02,   3.40000000e-02],
                                        [  7.02300000e+02,   3.60000000e-02],
                                        [  7.06700000e+02,   4.20000000e-02],
                                        [  7.07100000e+02,   6.10000000e-02],
                                        [  7.10200000e+02,   6.10000000e-02],
                                        [  7.11000000e+02,   4.10000000e-02],
                                        [  7.12200000e+02,   5.20000000e-02],
                                        [  7.14200000e+02,   3.30000000e-02],
                                        [  7.48400000e+02,   3.40000000e-02],
                                        [  7.57900000e+02,   3.10000000e-02],
                                        [  7.60700000e+02,   3.90000000e-02],
                                        [  7.63900000e+02,   2.90000000e-02],
                                        [  8.08800000e+02,   2.90000000e-02],
                                        [  8.10700000e+02,   3.90000000e-02],
                                        [  8.12700000e+02,   3.00000000e-02],
                                        [  8.50100000e+02,   3.00000000e-02]],
                                       CubicSplineInterpolator,
                                       {},
                                       Extrapolator,
                                       {...})
        """

        try:
            return multiline_repr(
                self,
                [
                    {"name": "path"},
                    {"name": "header"},
                    {"name": "spectral_quantity"},
                    {"name": "reflection_geometry"},
                    {"name": "transmission_geometry"},
                    {"name": "bandwidth_FWHM"},
                    {"name": "bandwidth_corrected"},
                    {
                        "formatter": lambda x: repr(  # noqa: ARG005
                            tstack([self.domain, self.range])
                        ),
                    },
                    {
                        "name": "interpolator",
                        "formatter": lambda x: (  # noqa: ARG005
                            self.interpolator.__name__
                        ),
                    },
                    {"name": "interpolator_kwargs"},
                    {
                        "name": "extrapolator",
                        "formatter": lambda x: (  # noqa: ARG005
                            self.extrapolator.__name__
                        ),
                    },
                    {"name": "extrapolator_kwargs"},
                ],
            )
        except TypeError:  # pragma: no cover
            return super().__repr__()

    def read(self) -> SpectralDistribution_IESTM2714:
        """
        Read and parses the spectral data *XML* file path.

        Returns
        -------
        :class:`colour.SpectralDistribution_IESTM2714`
            *IES TM-27-14* spectral distribution.

        Raises
        ------
        ValueError
            If the *IES TM-27-14* spectral distribution path is undefined.

        Examples
        --------
        >>> from os.path import dirname, join
        >>> directory = join(dirname(__file__), "tests", "resources")
        >>> sd = SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx"))
        >>> sd.name  # doctest: +SKIP
        'Unknown - N/A - Rare earth fluorescent lamp'
        >>> sd.header.comments
        'Ambient temperature 25 degrees C.'
        >>> sd[400]  # doctest: +ELLIPSIS
        0.0340000...
        """

        if self._path is not None:
            formatter = "./{{{0}}}{1}/{{{0}}}{2}"

            tree = ElementTree.parse(self._path)  # noqa: S314
            root = tree.getroot()

            match = re.match("{(.*)}", root.tag)
            if match:
                namespace = match.group(1)
            else:
                raise ValueError(
                    'The "IES TM-27-14" spectral distribution namespace '
                    "was not found!"
                )

            self.name = os.path.splitext(os.path.basename(self._path))[0]

            iterator = root.iter

            for header_element in (self.header, self):
                mapping = header_element.mapping
                for specification in mapping.elements:
                    element = root.find(
                        formatter.format(
                            namespace, mapping.element, specification.element
                        )
                    )
                    if element is not None:
                        setattr(
                            header_element,
                            specification.attribute,
                            specification.read_conversion(element.text),
                        )

            # Reading spectral data.
            wavelengths = []
            values = []
            for spectral_data in iterator(
                f"{{{namespace}}}{self.mapping.data.element}"
            ):
                wavelengths.append(spectral_data.attrib[self.mapping.data.attribute])
                values.append(spectral_data.text)

            components = [
                component
                for component in (
                    self.header.manufacturer,
                    self.header.catalog_number,
                    self.header.description,
                )
                if component is not None
            ]
            self.name = "Undefined" if len(components) == 0 else " - ".join(components)

            self.wavelengths = as_float_array(wavelengths)
            self.values = as_float_array(values)

            return self
        else:
            raise ValueError(
                'The "IES TM-27-14" spectral distribution path is undefined!'
            )

    def write(self) -> bool:
        """
        Write the spectral distribution spectral data to *XML* file path.

        Returns
        -------
        :class:`bool`
            Definition success.

        Examples
        --------
        >>> from os.path import dirname, join
        >>> from shutil import rmtree
        >>> from tempfile import mkdtemp
        >>> directory = join(dirname(__file__), "tests", "resources")
        >>> sd = SpectralDistribution_IESTM2714(join(directory, "Fluorescent.spdx"))
        >>> temporary_directory = mkdtemp()
        >>> sd.path = join(temporary_directory, "Fluorescent.spdx")
        >>> sd.write()
        True
        >>> rmtree(temporary_directory)
        """

        if self._path is not None:
            root = ElementTree.Element("IESTM2714")
            root.attrib = {
                "xmlns": NAMESPACE_IESTM2714,
                "version": VERSION_IESTM2714,
            }

            spectral_distribution = ElementTree.Element("")
            for header_element in (self.header, self):
                mapping = header_element.mapping
                element = ElementTree.SubElement(root, mapping.element)
                for specification in mapping.elements:
                    element_child = ElementTree.SubElement(
                        element, specification.element
                    )
                    value = getattr(header_element, specification.attribute)
                    element_child.text = specification.write_conversion(value)

                if header_element is self:
                    spectral_distribution = element

            # Writing spectral data.
            for wavelength, value in tstack([self.wavelengths, self.values]):
                element_child = ElementTree.SubElement(
                    spectral_distribution, mapping.data.element
                )
                element_child.text = mapping.data.write_conversion(value)
                element_child.attrib = {
                    mapping.data.attribute: mapping.data.write_conversion(wavelength)
                }

            xml = minidom.parseString(  # noqa: S318
                ElementTree.tostring(root)
            ).toprettyxml()

            with open(self._path, "w") as file:
                file.write(xml)

            return True
        else:
            raise ValueError(
                'The "IES TM-27-14" spectral distribution path is undefined!'
            )
