from __future__ import annotations
import sys
from dataclasses import dataclass

@dataclass(frozen=True)
class Window:
    """Class to hold the pixel dimensions of data in the given projection.

    Args:
        xoff: X axis offset.
        yoff: Y axis offset.
        xsize: Width of data in pixels.
        ysize: Height of data in pixels.

    Attributes:
        xoff: X axis offset.
        yoff: Y axis offset.
        xsize: Width of data in pixels.
        ysize: Height of data in pixels.
    """
    xoff: int
    yoff: int
    xsize: int
    ysize: int

    def __post_init__(self):
        for field in ['xoff', 'yoff', 'xsize', 'ysize']:
            value = getattr(self, field)
            if isinstance(value, bool):
                raise TypeError("Window properties must be whole numbers, not bool")
            if isinstance(value, float):
                if not value.is_integer():
                    raise ValueError("Window properties must be whole numbers, not bools")
                object.__setattr__(self, field, int(value))
            elif not isinstance(value, int):
                raise TypeError(
                    f"Window properties must be whole numbers, not {type(value).__name__}"
                )

    @property
    def as_array_args(self) -> tuple[int, ...]:
        """A tuple containing xoff, yoff, xsize, and ysize."""
        return (self.xoff, self.yoff, self.xsize, self.ysize)

    def __lt__(self, other) -> bool:
        return (self.xsize < other.xsize) and \
            (self.ysize < other.ysize) and \
            (self.xoff >= other.xoff) and \
            (self.yoff >= other.yoff) and \
            ((self.xoff + self.xsize) <= (other.xoff + other.xsize)) and \
            ((self.yoff + self.ysize) <= (other.yoff + other.ysize))

    def __gt__(self, other) -> bool:
        return (self.xsize > other.xsize) and \
            (self.ysize > other.ysize) and \
            (self.xoff <= other.xoff) and \
            (self.yoff <= other.yoff) and \
            ((self.xoff + self.xsize) >= (other.xoff + other.xsize)) and \
            ((self.yoff + self.ysize) >= (other.yoff + other.ysize))

    def __le__(self, other) -> bool:
        return (self.xsize <= other.xsize) and \
            (self.ysize <= other.ysize) and \
            (self.xoff >= other.xoff) and \
            (self.yoff >= other.yoff) and \
            ((self.xoff + self.xsize) <= (other.xoff + other.xsize)) and \
            ((self.yoff + self.ysize) <= (other.yoff + other.ysize))

    def __ge__(self, other) -> bool:
        return (self.xsize >= other.xsize) and \
            (self.ysize >= other.ysize) and \
            (self.xoff <= other.xoff) and \
            (self.yoff <= other.yoff) and \
            ((self.xoff + self.xsize) >= (other.xoff + other.xsize)) and \
            ((self.yoff + self.ysize) >= (other.yoff + other.ysize))

    def grow(self, pixels: int) -> Window:
        """Expand the area in all directions by the given amount.

        Generates a new window that is an expanded version of the current window.

        Args:
            pixels: The amount by which to grow the window in pixels.

        Returns:
            A new window of the expanded size.
        """
        return Window(
            xoff=self.xoff - pixels,
            yoff=self.yoff - pixels,
            xsize=self.xsize + (2 * pixels),
            ysize=self.ysize + (2 * pixels),
        )

    @staticmethod
    def find_intersection(windows: list) -> Window:
        if not windows:
            raise ValueError("Expected list of windows")
        # This isn't very pythonic, as it was originally written, but
        # this method gets called a lot (by every Layer.read_array), so not looping
        # over the window list multiple times halves the performance cost (as
        # measured with cProfile).
        left, top, right, bottom = -sys.maxsize, -sys.maxsize, sys.maxsize, sys.maxsize
        for window in windows:
            left = max(left, window.xoff)
            top = max(top, window.yoff)
            right = min(right, window.xoff + window.xsize)
            bottom = min(bottom, window.yoff + window.ysize)
        if (left >= right) or (top >= bottom):
            raise ValueError('No intersection possible')
        return Window(
            left,
            top,
            right - left,
            bottom - top
        )

    @staticmethod
    def find_intersection_no_throw(windows: list) -> Window | None:
        if not windows:
            raise ValueError("Expected list of windows")
        # This isn't very pythonic, as it was originally written, but
        # this method gets called a lot (by every Layer.read_array), so not looping
        # over the window list multiple times halves the performance cost (as
        # measured with cProfile).
        left, top, right, bottom = -sys.maxsize, -sys.maxsize, sys.maxsize, sys.maxsize
        for window in windows:
            left = max(left, window.xoff)
            top = max(top, window.yoff)
            right = min(right, window.xoff + window.xsize)
            bottom = min(bottom, window.yoff + window.ysize)
        if (left >= right) or (top >= bottom):
            return None
        return Window(
            left,
            top,
            right - left,
            bottom - top
        )
