#  Copyright (c) European Space Agency, 2020.
#
#  This file is subject to the terms and conditions defined in file 'LICENCE.txt', which
#  is part of this Pyxel package. No part of the package, including
#  this file, may be copied, modified, propagated, or distributed except according to
#  the terms contained in the file ‘LICENCE.txt’.

"""Geometry class for detector."""

from collections.abc import Mapping
from typing import Optional

import numpy as np

from pyxel.util import get_size


def get_vertical_pixel_center_pos(
    num_rows: int,
    num_cols: int,
    pixel_vertical_size: float,
) -> np.ndarray:
    """Generate vertical position list of all pixel centers in detector imaging area."""
    init_ver_position = np.arange(0.0, num_rows, 1.0) * pixel_vertical_size
    init_ver_position += pixel_vertical_size / 2.0

    return np.repeat(init_ver_position, num_cols)


def get_horizontal_pixel_center_pos(
    num_rows: int,
    num_cols: int,
    pixel_horizontal_size: float,
) -> np.ndarray:
    """Generate horizontal position list of all pixel centers in detector imaging area."""
    init_hor_position = np.arange(0.0, num_cols, 1.0) * pixel_horizontal_size
    init_hor_position += pixel_horizontal_size / 2.0

    return np.tile(init_hor_position, reps=num_rows)


class Geometry:
    """Geometrical attributes of the detector.

    Parameters
    ----------
    row : int
        Number of pixel rows.
    col : int
        Number of pixel columns.
    total_thickness : float, optional
        Thickness of detector. Unit: um
    pixel_vert_size : float, optional
        Vertical dimension of pixel. Unit: um
    pixel_horz_size : float, optional
        Horizontal dimension of pixel. Unit: um
    pixel_scale : float, optional
        Dimension of how much of the sky is covered by one pixel. Unit: arcsec/pixel
    """

    def __init__(
        self,
        row: int,
        col: int,
        total_thickness: Optional[float] = None,  # unit: um
        pixel_vert_size: Optional[float] = None,  # unit: um
        pixel_horz_size: Optional[float] = None,  # unit: um
        pixel_scale: Optional[float] = None,  # unit: arcsec/pixel
    ):
        if row <= 0:
            raise ValueError("'row' must be strictly greater than 0.")

        if col <= 0:
            raise ValueError("'col' must be strictly greater than 0.")

        if total_thickness and not (0.0 <= total_thickness <= 10000.0):
            raise ValueError("'total_thickness' must be between 0.0 and 10000.0.")

        if pixel_vert_size and not (0.0 <= pixel_vert_size <= 1000.0):
            raise ValueError("'pixel_vert_size' must be between 0.0 and 1000.0.")

        if pixel_horz_size and not (0.0 <= pixel_horz_size <= 1000.0):
            raise ValueError("'pixel_horz_size' must be between 0.0 and 1000.0.")

        self._row = row
        self._col = col
        self._total_thickness = total_thickness
        self._pixel_vert_size = pixel_vert_size
        self._pixel_horz_size = pixel_horz_size
        self._pixel_scale = pixel_scale

        self._numbytes = 0

    def __repr__(self) -> str:
        cls_name: str = self.__class__.__name__
        return (
            f"{cls_name}(row={self._row!r}, col={self._col!r}, "
            f"total_thickness={self._total_thickness!r}, "
            f"pixel_vert_size={self._pixel_vert_size!r}, "
            f"pixel_horz_size={self._pixel_horz_size}), "
            f"pixel_scale={self._pixel_scale})"
        )

    def __eq__(self, other) -> bool:
        return type(self) is type(other) and (
            self.row,
            self.col,
            self._total_thickness,
            self._pixel_vert_size,
            self._pixel_horz_size,
            self._pixel_scale,
        ) == (
            other.row,
            other.col,
            other._total_thickness,
            other._pixel_vert_size,
            other._pixel_horz_size,
            other._pixel_scale,
        )

    # def _repr_html_(self):
    #     """TBW."""
    #     return "Hello World"

    @property
    def row(self) -> int:
        """Get Number of pixel rows."""
        return self._row

    @row.setter
    def row(self, value: int) -> None:
        """Set Number of pixel rows."""
        if value <= 0:
            raise ValueError("'row' must be strictly greater than 0.")

        self._row = value

    @property
    def col(self) -> int:
        """Get Number of pixel columns."""
        return self._col

    @col.setter
    def col(self, value: int) -> None:
        """Set Number of pixel columns."""
        if value <= 0:
            raise ValueError("'col' must be strictly greater than 0.")

        self._col = value

    @property
    def shape(self) -> tuple[int, int]:
        """Return detector shape."""
        return self.row, self.col

    @property
    def total_thickness(self) -> float:
        """Get Thickness of detector."""
        if self._total_thickness:
            return self._total_thickness
        else:
            raise ValueError("'total_thickness' not specified in detector geometry.")

    @total_thickness.setter
    def total_thickness(self, value: float) -> None:
        """Set Thickness of detector."""
        if not (0.0 <= value <= 10000.0):
            raise ValueError("'total_thickness' must be between 0.0 and 10000.0.")

        self._total_thickness = value

    @property
    def pixel_vert_size(self) -> float:
        """Get Vertical dimension of pixel."""
        if self._pixel_vert_size:
            return self._pixel_vert_size
        else:
            raise ValueError("'pixel_vert_size' not specified in detector geometry.")

    @pixel_vert_size.setter
    def pixel_vert_size(self, value: float) -> None:
        """Set Vertical dimension of pixel."""
        if not (0.0 <= value <= 1000.0):
            raise ValueError("'pixel_vert_size' must be between 0.0 and 1000.0.")

        self._pixel_vert_size = value

    @property
    def pixel_horz_size(self) -> float:
        """Get Horizontal dimension of pixel."""
        if self._pixel_horz_size:
            return self._pixel_horz_size
        else:
            raise ValueError("'pixel_horz_size' not specified in detector geometry.")

    @pixel_horz_size.setter
    def pixel_horz_size(self, value: float) -> None:
        """Set Horizontal dimension of pixel."""
        if not (0.0 <= value <= 1000.0):
            raise ValueError("'pixel_horz_size' must be between 0.0 and 1000.0.")

        self._pixel_horz_size = value

    @property
    def pixel_scale(self) -> float:
        """Get pixel scale."""
        if self._pixel_scale:
            return self._pixel_scale
        else:
            raise ValueError("'pixel_scale' not specified in detector geometry.")

    @pixel_scale.setter
    def pixel_scale(self, value: float) -> None:
        """Set pixel scale."""
        if not (0.0 <= value <= 1000.0):
            raise ValueError("'pixel_scale' must be between 0.0 and 1000.0.")

        self._pixel_scale = value

    @property
    def horz_dimension(self) -> float:
        """Get total horizontal dimension of detector. Calculated automatically.

        Return
        ------
        float
            horizontal dimension
        """
        return self.pixel_horz_size * self.col

    @property
    def vert_dimension(self) -> float:
        """Get total vertical dimension of detector. Calculated automatically.

        Return
        ------
        float
            vertical dimension
        """
        return self.pixel_vert_size * self.row

    def vertical_pixel_center_pos_list(self) -> np.ndarray:
        """Generate horizontal position list of all pixel centers in detector imaging area."""
        return get_vertical_pixel_center_pos(
            num_rows=self.row,
            num_cols=self.col,
            pixel_vertical_size=self.pixel_vert_size,
        )

    def horizontal_pixel_center_pos_list(self) -> np.ndarray:
        """Generate horizontal position list of all pixel centers in detector imaging area."""
        return get_horizontal_pixel_center_pos(
            num_rows=self.row,
            num_cols=self.col,
            pixel_horizontal_size=self.pixel_horz_size,
        )

    @property
    def numbytes(self) -> int:
        """Recursively calculates object size in bytes using Pympler library.

        Returns
        -------
        int
            Size of the object in bytes.
        """
        self._numbytes = get_size(self)
        return self._numbytes

    def to_dict(self) -> Mapping:
        """Get the attributes of this instance as a `dict`."""
        return {
            "row": self.row,
            "col": self.col,
            "total_thickness": self._total_thickness,
            "pixel_vert_size": self._pixel_vert_size,
            "pixel_horz_size": self._pixel_horz_size,
            "pixel_scale": self._pixel_scale,
        }

    @classmethod
    def from_dict(cls, dct: Mapping):
        """Create a new instance of `Geometry` from a `dict`."""
        # TODO: This is a simplistic implementation. Improve this.
        return cls(**dct)
