from __future__ import annotations

from typing import TYPE_CHECKING

from pydantic import BaseModel, model_validator

from .coordinate import Coordinate
from .exceptions import SpaceMismatchError
from .space import Space

if TYPE_CHECKING:
    from .resize import ResizeMetadata


class BBox(BaseModel, frozen=True):
    x: int
    y: int
    width: int
    height: int
    space: Space

    @model_validator(mode="after")
    def _validate(self) -> BBox:
        if self.width <= 0 or self.height <= 0:
            raise ValueError("width and height must be positive")
        if self.x < 0 or self.y < 0:
            raise ValueError("x and y must be non-negative")
        if self.x + self.width > self.space.width:
            raise ValueError(
                f"bbox right edge {self.x + self.width} exceeds space width {self.space.width}"
            )
        if self.y + self.height > self.space.height:
            raise ValueError(
                f"bbox bottom edge {self.y + self.height} exceeds space height {self.space.height}"
            )
        return self

    @property
    def top_left(self) -> Coordinate:
        return Coordinate(x=self.x, y=self.y, space=self.space)

    @property
    def bottom_right(self) -> Coordinate:
        return Coordinate(
            x=self.x + self.width - 1,
            y=self.y + self.height - 1,
            space=self.space,
        )

    @property
    def center(self) -> Coordinate:
        return Coordinate(
            x=self.x + self.width // 2,
            y=self.y + self.height // 2,
            space=self.space,
        )

    @property
    def as_space(self) -> Space:
        return Space(width=self.width, height=self.height)

    def to_space(
        self,
        target: Space,
        resize_metadata: ResizeMetadata | None = None,
        offset: BBox | None = None,
    ) -> BBox:
        if offset is not None and target != offset.as_space:
            raise SpaceMismatchError(
                f"target space {target} must match offset.as_space {offset.as_space}"
            )
        top_left = self.top_left.to_space(target, resize_metadata=resize_metadata)
        bottom_right = self.bottom_right.to_space(
            target, resize_metadata=resize_metadata
        )
        new_x = top_left.x
        new_y = top_left.y
        new_width = bottom_right.x - top_left.x + 1
        new_height = bottom_right.y - top_left.y + 1
        if offset is None:
            return BBox(
                x=new_x, y=new_y, width=new_width, height=new_height, space=target
            )
        return BBox(
            x=new_x + offset.x,
            y=new_y + offset.y,
            width=new_width,
            height=new_height,
            space=offset.space,
        )

    def to_source_space(
        self,
        resize_metadata: ResizeMetadata,
        offset: BBox | None = None,
    ) -> BBox:
        """Map this bbox from resized target coordinates into original source coordinates."""
        return self.to_space(
            resize_metadata.source_space,
            resize_metadata=resize_metadata,
            offset=offset,
        )

    def to_target_space(
        self,
        resize_metadata: ResizeMetadata,
        offset: BBox | None = None,
    ) -> BBox:
        """Map this bbox from original source coordinates into resized target coordinates."""
        if offset is not None and resize_metadata.target_space != offset.as_space:
            raise SpaceMismatchError(
                f"target space {resize_metadata.target_space} must match "
                f"offset.as_space {offset.as_space}"
            )
        top_left = self.top_left.to_target_space(resize_metadata)
        bottom_right = self.bottom_right.to_target_space(resize_metadata)
        new_x = top_left.x
        new_y = top_left.y
        new_width = bottom_right.x - top_left.x + 1
        new_height = bottom_right.y - top_left.y + 1
        if offset is None:
            return BBox(
                x=new_x,
                y=new_y,
                width=new_width,
                height=new_height,
                space=resize_metadata.target_space,
            )
        return BBox(
            x=new_x + offset.x,
            y=new_y + offset.y,
            width=new_width,
            height=new_height,
            space=offset.space,
        )

    def contains(self, coord: Coordinate) -> bool:
        if coord.space != self.space:
            raise SpaceMismatchError(
                f"Coordinate space {coord.space} must match bbox space {self.space}"
            )
        return (
            self.x <= coord.x <= self.x + self.width - 1
            and self.y <= coord.y <= self.y + self.height - 1
        )

    def localize(self, coord: Coordinate) -> Coordinate:
        if coord.space != self.space:
            raise SpaceMismatchError(
                f"Coordinate space {coord.space} must match bbox space {self.space}"
            )
        return Coordinate(
            x=coord.x - self.x, y=coord.y - self.y, space=self.as_space
        )

    def absolutize(self, coord: Coordinate) -> Coordinate:
        if coord.space != self.as_space:
            raise SpaceMismatchError(
                f"Coordinate space {coord.space} must match local bbox space {self.as_space}"
            )
        return Coordinate(
            x=coord.x + self.x, y=coord.y + self.y, space=self.space
        )
