from dataclasses import dataclass


@dataclass
class Viewport:
    """Maps a geographic bounding box to pixel coordinates."""
    minx: float
    miny: float
    maxx: float
    maxy: float
    pixel_width: int
    pixel_height: int

    @property
    def width(self) -> float:
        return self.maxx - self.minx

    @property
    def height(self) -> float:
        return self.maxy - self.miny

    def geo_to_pixel(self, gx: float, gy: float) -> tuple[int, int]:
        """Convert geographic coordinates to pixel coordinates.

        Y is flipped: geographic Y increases up, pixel Y increases down.
        """
        if self.width == 0 or self.height == 0:
            return 0, 0
        nx = (gx - self.minx) / self.width
        ny = (gy - self.miny) / self.height
        px = int(nx * (self.pixel_width - 1))
        py = int((1.0 - ny) * (self.pixel_height - 1))
        px = max(0, min(self.pixel_width - 1, px))
        py = max(0, min(self.pixel_height - 1, py))
        return px, py

    def zoom(self, factor: float) -> "Viewport":
        """Zoom in (factor < 1) or out (factor > 1) from center."""
        cx = (self.minx + self.maxx) / 2
        cy = (self.miny + self.maxy) / 2
        hw = self.width * factor / 2
        hh = self.height * factor / 2
        return Viewport(
            minx=cx - hw, miny=cy - hh,
            maxx=cx + hw, maxy=cy + hh,
            pixel_width=self.pixel_width,
            pixel_height=self.pixel_height,
        )

    def pan(self, dx_frac: float, dy_frac: float) -> "Viewport":
        """Pan by a fraction of the current extent."""
        dx = self.width * dx_frac
        dy = self.height * dy_frac
        return Viewport(
            minx=self.minx + dx, miny=self.miny + dy,
            maxx=self.maxx + dx, maxy=self.maxy + dy,
            pixel_width=self.pixel_width,
            pixel_height=self.pixel_height,
        )

    @classmethod
    def from_bounds(cls, bounds: tuple[float, float, float, float],
                    pixel_width: int, pixel_height: int,
                    padding: float = 0.05) -> "Viewport":
        """Create a viewport from dataset bounds with padding."""
        minx, miny, maxx, maxy = bounds
        w = maxx - minx
        h = maxy - miny
        # Handle point datasets (zero extent)
        if w == 0:
            w = 1.0
        if h == 0:
            h = 1.0
        pad_x = w * padding
        pad_y = h * padding
        return cls(
            minx=minx - pad_x, miny=miny - pad_y,
            maxx=maxx + pad_x, maxy=maxy + pad_y,
            pixel_width=pixel_width,
            pixel_height=pixel_height,
        )
