"""Generate convex hulls of valid raster data for use in STAC Item
geometries."""

import logging
import warnings
from enum import Enum, auto
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, TypeVar, Union

import numpy as np
import numpy.typing as npt
import rasterio
import rasterio.features
from pystac import Item
from rasterio import Affine, DatasetReader
from rasterio.crs import CRS
from shapely.geometry import mapping, shape
from shapely.geometry.multipolygon import MultiPolygon
from shapely.geometry.polygon import Polygon, orient
from shapely.ops import unary_union
from stactools.core.geometry import mutual_intersection

from ..projection import reproject_shape

logger = logging.getLogger(__name__)

# Roughly 1 centimeter in geodetic coordinates
DEFAULT_PRECISION = 7

T = TypeVar("T", bound="RasterFootprint")


class FootprintMergeStrategy(Enum):
    """Strategy for handling the aggregation of differing asset footprints."""

    FIRST = auto()
    """Use the footprint of the first matching asset."""

    UNION = auto()
    """Union the geometries of all matching assets."""

    INTERSECTION = auto()
    """Use the mutual intersection of all matching asset footprints."""


def densify_by_factor(
    point_list: List[Tuple[float, float]], factor: int
) -> List[Tuple[float, float]]:
    """Densifies the number of points in a list of points by a ``factor``. For
    example, a list of 5 points and a factor of 2 will result in 10 points (one
    new point between each original adjacent points).

    Derived from code found at
    https://stackoverflow.com/questions/64995977/generating-equidistance-points-along-the-boundary-of-a-polygon-but-cw-ccw

    Args:
        point_list (List[Tuple[float, float]]): The list of points to be
            densified.
        factor (int): The factor by which to densify the points. A larger
            densification factor should be used when reprojection results in
            greater curvature from the original geometry.

    Returns:
        List[Tuple[float, float]]: A list of the densified points.
    """  # noqa: E501
    points: Any = np.asarray(point_list)
    densified_number = len(points) * factor
    existing_indices = np.arange(0, densified_number, factor)
    interp_indices = np.arange(existing_indices[-1] + 1)
    interp_x = np.interp(interp_indices, existing_indices, points[:, 0])
    interp_y = np.interp(interp_indices, existing_indices, points[:, 1])
    return [(x, y) for x, y in zip(interp_x, interp_y)]


def densify_by_distance(
    point_list: List[Tuple[float, float]], distance: float
) -> List[Tuple[float, float]]:
    """Densifies the number of points in a list of points by inserting new
    points at intervals between each set of successive points. For example, if
    two successive points in the list are separated by 10 units and a
    ``distance`` of 2 is provided, 4 new points will be added between the two
    original points (one new point every 2 units of ``distance``).

    Derived from code found at
    https://stackoverflow.com/questions/64995977/generating-equidistance-points-along-the-boundary-of-a-polygon-but-cw-ccw

    Args:
        point_list (List[Tuple[float, float]]): The list of points to be
            densified.
        distance (float): The interval at which to insert additional points. A
            smaller densification distance should be used when reprojection
            results in greater curvature from the original geometry.

    Returns:
        List[Tuple[float, float]]: A list of the densified points.
    """
    points: Any = np.asarray(point_list)
    dxdy = points[1:, :] - points[:-1, :]
    segment_lengths = np.sqrt(np.sum(np.square(dxdy), axis=1))
    steps = segment_lengths / distance
    coordinate_steps = dxdy / steps.reshape(-1, 1)
    densified_points = np.empty((len(point_list) - 1,), dtype="O")
    for index in range(len(point_list) - 1):
        step = np.arange(steps[index])
        densified_points[index] = (
            np.array((step, step)).T * coordinate_steps[index] + points[index]
        )
    final_point = points[-1].reshape(1, -1)
    densified_array = np.concatenate((*densified_points, final_point), axis=0)
    return [(float(row[0]), float(row[1])) for row in densified_array]


def reproject_polygon(
    polygon: Polygon,
    crs: CRS,
    precision: Optional[int] = DEFAULT_PRECISION,
    dst_crs: CRS = "EPSG:4326",
) -> Polygon:
    """DEPRECATED.

    .. deprecated:: 0.5.0
        Use :func:`projection.reproject_shape` instead.

    Projects a polygon and rounds the projected vertex coordinates to ``precision``.

    Duplicate points caused by rounding are removed.

    Args:
        polygon (Polygon): The polygon to reproject.
        crs (CRS): The CRS of the input polygon.
        precision (int): The number of decimal places to include in the final
            polygon vertex coordinates.
        dst_crs (CRS): The CRS of the output polygon. Defaults to EPSG 4326

    Returns:
        Polygon: Polygon in 'dst_crs'. Default to EPSG 4326
    """
    warnings.warn(
        "``utils.reproject_polygon`` is deprecated and will be removed in v0.6.0. "
        "Use ``projection.reproject_shape`` instead.",
        DeprecationWarning,
    )
    return reproject_shape(
        src_crs=crs, dst_crs=dst_crs, geom=polygon, precision=precision
    )


class RasterFootprint:
    """An object for creating a convex hull polygon around all areas within an
    raster that have data values (i.e., they do not have the nodata value).
    This convex hull is termed the "footprint" of the raster data and is
    returned by the :meth:`footprint` method as a polygon in a GeoJSON
    dictionary for use as the geometry attribute of a STAC Item.

    Two important operations during this calculation are the densification of
    the footprint in the native CRS and simplification of the footprint after
    reprojection. If the initial low-vertex polygon in the native
    CRS is not densified, this can result in a reprojected polygon that does not
    accurately represent the data footprint. For example, a MODIS asset
    represented by a rectangular 5 point Polygon in a sinusoidal projection will
    reproject to a parallelogram in EPSG 4326, when it would be more accurately
    represented by a polygon with two parallel sides and two curved sides. The
    difference between these representations is greater the further away from
    the meridian and equator the asset is located.

    After reprojection, the footprint may have more points than
    desired. This can be simplified to a polygon with fewer points that maintain
    a maximum distance to the original geometry.

    Args:
        data_array (numpy.NDArray[Any]): The raster data used for the
            footprint calculation.
        crs (CRS): Coordinate reference system of the raster data.
        dst_crs (CRS): Coordinate reference system of the footprint data. Defaults
            to EPSG 4236.
        transform (Affine): Matrix defining the transformation from pixel to CRS
            coordinates.
        precision (int): The number of decimal places to include in the
            final footprint coordinates.
        densification_factor (Optional[int]): The factor by which to
            increase point density within the footprint polygon before
            projection. A factor of 2 would double the density of
            points (placing one new point between each existing pair of points),
            a factor of 3 would place two points between each point, etc. Higher
            densities produce higher fidelity footprints in areas of high
            projection distortion. Mutually exclusive with
            ``densification_distance``.
        densification_distance (Optional[float]): The distance by which to
            increase point density within the footprint polygon before
            projection. If the distance is set to 2 and the segment
            length between two polygon vertices is 10, 4 new vertices would be
            created along the segment. Higher densities produce higher
            fidelity footprints in areas of high projection distortion.
            Mutually exclusive with ``densification_factor``.
        simplify_tolerance (Optional[float]): Distance, in degrees, within
            which all locations on the simplified polygon will be to the original
            polygon.
        no_data (Optional[Union[int, float]]): The nodata value in
            ``data_array``. If set to None, this will return a footprint
            including nodata values.
    """

    crs: CRS
    """Coordinate reference system of the raster data."""

    dst_crs: CRS = "EPSG:4326"
    """Coordinate reference system of the footprint."""

    data_array: npt.NDArray[Any]
    """2D or 3D array of raster data."""

    densification_distance: Optional[float]
    """Optional distance for densifying polygon vertices before reprojection."""

    densification_factor: Optional[int]
    """Optional factor for densifying polygon vertices before reprojection."""

    no_data: Optional[Union[int, float]]
    """Optional value defining pixels to exclude from the footprint."""

    precision: int
    """Number of decimal places in the final footprint coordinates."""

    simplify_tolerance: Optional[float]
    """Optional maximum allowable error when simplifying the reprojected
    polygon."""

    transform: Affine
    """Transformation matrix from pixel to CRS coordinates."""

    def __init__(
        self,
        data_array: npt.NDArray[Any],
        crs: CRS,
        transform: Affine,
        *,
        dst_crs: CRS = "EPSG:4326",
        precision: int = DEFAULT_PRECISION,
        densification_factor: Optional[int] = None,
        densification_distance: Optional[float] = None,
        simplify_tolerance: Optional[float] = None,
        no_data: Optional[Union[int, float]] = None,
    ) -> None:
        if data_array.ndim == 2:
            data_array = data_array[np.newaxis, :]
        self.data_array = data_array
        self.crs = crs
        self.dst_crs = dst_crs
        self.transform = transform
        self.precision = precision
        if densification_factor is not None and densification_distance is not None:
            raise ValueError(
                "Only one of 'densification_factor' or 'densification_distance' "
                "can be specified."
            )
        self.densification_factor = densification_factor
        self.densification_distance = densification_distance
        self.simplify_tolerance = simplify_tolerance
        self.no_data = no_data

    def footprint(self) -> Optional[Dict[str, Any]]:
        """Produces the footprint surrounding data (not nodata) pixels in the
        source image. If the footprint is unable to be computed, None is
        returned.

        Returns:
            Optional[Dict[str, Any]]: A GeoJSON dictionary containing the
            footprint polygon.
        """
        mask = self.data_mask()
        polygon = self.data_extent(mask)
        if polygon is None:
            return None
        polygon = self.densify_polygon(polygon)
        polygon = self.reproject_polygon(polygon)
        polygon = self.simplify_polygon(polygon)
        return mapping(polygon)  # type: ignore

    def data_mask(self) -> npt.NDArray[np.uint8]:
        """Produces a mask of valid data in the source image. Nodata pixels
        values are set to 0, data pixels are set to 1.

        Returns:
            numpy.NDArray[numpy.uint8]: A 2D array containing 0s and 1s for
            nodata/data pixels.
        """
        assert self.data_array.ndim == 3
        shape = self.data_array.shape
        if self.no_data is not None:
            mask: npt.NDArray[np.uint8] = np.full(shape, 0, dtype=np.uint8)
            if np.isnan(self.no_data):
                mask[~np.isnan(self.data_array)] = 1
            else:
                mask[self.data_array != self.no_data] = 1
            mask = np.sum(mask, axis=0, dtype=np.uint8)
            mask[mask > 0] = 1
        else:
            mask = np.full(shape, 1, dtype=np.uint8)
        return mask

    def data_extent(self, mask: npt.NDArray[np.uint8]) -> Optional[Polygon]:
        """Produces the data footprint in the native CRS.

        Args:
            mask (numpy.NDArray[numpy.uint8]): A 2D array containing 0s and 1s for
                nodata/data pixels.

        Returns:
            Optional[Polygon]: A native CRS polygon of the convex hull of data
            pixels.
        """
        data_polygons = [
            shape(polygon_dict)
            for polygon_dict, region_value in rasterio.features.shapes(
                mask, transform=self.transform
            )
            if region_value == 1
        ]

        if not data_polygons:
            return None
        elif len(data_polygons) == 1:
            polygon = data_polygons[0]
        else:
            polygon = MultiPolygon(data_polygons).convex_hull

        return orient(polygon)

    def densify_polygon(self, polygon: Polygon) -> Polygon:
        """Adds vertices to the footprint polygon in the native CRS using
        either ``self.densification_factor`` or
        ``self.densification_distance``.

        Args:
            polygon (Polygon): Footprint polygon in the native CRS.

        Returns:
            Polygon: Densified footprint polygon in the native CRS.
        """
        assert not (self.densification_factor and self.densification_distance)
        if self.densification_factor is not None:
            return Polygon(
                densify_by_factor(polygon.exterior.coords, self.densification_factor)
            )
        elif self.densification_distance is not None:
            return Polygon(
                densify_by_distance(
                    polygon.exterior.coords, self.densification_distance
                )
            )
        else:
            return polygon

    def reproject_polygon(self, polygon: Polygon) -> Polygon:
        """Projects a polygon and rounds the projected vertex coordinates to
        ``self.precision``.

        Duplicate points caused by rounding are removed.

        Args:
            polygon (Polygon): Footprint polygon in the native CRS.

        Returns:
            Polygon: Footprint polygon in 'dst_crs'.
        """
        return reproject_shape(
            src_crs=self.crs,
            dst_crs=self.dst_crs,
            geom=polygon,
            precision=self.precision,
        )

    def simplify_polygon(self, polygon: Polygon) -> Polygon:
        """Reduces the number of polygon vertices such that the simplified
        polygon shape is no further away than the original polygon vertices
        than ``self.simplify_tolerance``.

        Args:
            polygon (Polygon): Polygon to be simplified.

        Returns:
            Polygon: Reduced vertex polygon.
        """
        if self.simplify_tolerance is not None:
            return orient(
                polygon.simplify(
                    tolerance=self.simplify_tolerance, preserve_topology=False
                )
            )
        return polygon

    @classmethod
    def from_href(
        cls: Type[T],
        href: str,
        *,
        dst_crs: CRS = "EPSG:4326",
        precision: int = DEFAULT_PRECISION,
        densification_factor: Optional[int] = None,
        densification_distance: Optional[float] = None,
        simplify_tolerance: Optional[float] = None,
        no_data: Optional[Union[int, float]] = None,
        bands: List[int] = [1],
    ) -> T:
        """Produces a :class:`RasterFootprint` instance from an image href.

        The href can point to any file that is openable by rasterio.

        Args:
            href (str): The href of the image to process.
            dst_crs (CRS): Coordinate reference system of the footprint data. Defaults
                to EPSG 4236.
            precision (int): The number of decimal places to include in the
                final footprint coordinates.
            densification_factor (Optional[int]): The factor by which to
                increase point density within the footprint polygon before
                projection. A factor of 2 would double the density
                of points (placing one new point between each existing pair of
                points), a factor of 3 would place two points between each point,
                etc. Higher densities produce higher fidelity footprints in
                areas of high projection distortion. Mutually exclusive with
                ``densification_distance``.
            densification_distance (Optional[float]): The distance by which to
                increase point density within the footprint polygon before
                projection. If the distance is set to 2 and the
                segment length between two polygon vertices is 10, 4 new
                vertices would be created along the segment. Higher densities
                produce higher fidelity footprints in areas of high projection
                distortion.  Mutually exclusive with ``densification_factor``.
            simplify_tolerance (Optional[float]): Distance, in degrees, within
                which all locations on the simplified polygon will be to the
                original polygon.
            no_data (Optional[Union[int, float]]): Explicitly sets the nodata
                value if not in source image metadata. If set to None, this will
                return a footprint including nodata values.
            bands (List[int]): The bands to use to compute the footprint.
                Defaults to [1]. If an empty list is provided, the bands will be
                ORd together; e.g., for a pixel to be outside of the footprint,
                all bands must have nodata in that pixel.

        Returns:
            RasterFootprint: A :class:`RasterFootprint` instance.
        """
        with rasterio.open(href) as source:
            return cls.from_rasterio_dataset_reader(
                reader=source,
                dst_crs=dst_crs,
                no_data=no_data,
                bands=bands,
                precision=precision,
                densification_factor=densification_factor,
                densification_distance=densification_distance,
                simplify_tolerance=simplify_tolerance,
            )

    @classmethod
    def from_rasterio_dataset_reader(
        cls: Type[T],
        reader: DatasetReader,
        *,
        dst_crs: CRS = "EPSG:4326",
        precision: int = DEFAULT_PRECISION,
        densification_factor: Optional[int] = None,
        densification_distance: Optional[float] = None,
        simplify_tolerance: Optional[float] = None,
        no_data: Optional[Union[int, float]] = None,
        bands: List[int] = [1],
    ) -> T:
        """Produces a :class:`RasterFootprint` instance from a
        :class:`rasterio.io.DatasetReader`  object, i.e., an opened dataset
        object returned by a :func:`rasterio.open` call.

        Args:
            reader (DatasetReader): A rasterio dataset reader object for the
                image to process.
            dst_crs (CRS): Coordinate reference system of the footprint data. Defaults
                to EPSG 4236.
            precision (int): The number of decimal places to include in the
                final footprint coordinates.
            densification_factor (Optional[int]): The factor by which to
                increase point density within the footprint polygon before
                projection. A factor of 2 would double the density
                of points (placing one new point between each existing pair of
                points), a factor of 3 would place two points between each point,
                etc. Higher densities produce higher fidelity footprints in
                areas of high projection distortion. Mutually exclusive with
                ``densification_distance``.
            densification_distance (Optional[float]): The distance by which to
                increase point density within the footprint polygon before
                projection. If the distance is set to 2 and the
                segment length between two polygon vertices is 10, 4 new
                vertices would be created along the segment. Higher densities
                produce higher fidelity footprints in areas of high projection
                distortion.  Mutually exclusive with ``densification_factor``.
            simplify_tolerance (Optional[float]): Distance, in degrees, within
                which all locations on the simplified polygon will be to the
                original polygon.
            no_data (Optional[Union[int, float]]): Explicitly sets the nodata
                value if not in source image metadata. If set to None, this will
                return a footprint including nodata values.
            bands (List[int]): The bands to use to compute the footprint.
                Defaults to [1]. If an empty list is provided, the bands will be
                ORd together; e.g., for a pixel to be outside of the footprint,
                all bands must have nodata in that pixel.

        Returns:
            RasterFootprint: A :class:`RasterFootprint` instance.
        """
        if not reader.indexes:
            raise ValueError(
                "Raster footprint cannot be computed for an asset with no bands."
            )
        if len(set(reader.nodatavals)) != 1:
            raise ValueError("All raster bands must have the same 'nodata' value.")

        if not bands:
            bands = reader.indexes
        if no_data is None:
            no_data = reader.nodata

        band_data = []
        for index in bands:
            band_data.append(reader.read(index))

        return cls(
            data_array=np.asarray(band_data),
            crs=reader.crs,
            dst_crs=dst_crs,
            transform=reader.transform,
            no_data=no_data,
            precision=precision,
            densification_factor=densification_factor,
            densification_distance=densification_distance,
            simplify_tolerance=simplify_tolerance,
        )

    @classmethod
    def update_geometry_from_asset_footprint(
        cls,
        item: Item,
        *,
        asset_names: List[str] = [],
        dst_crs: CRS = "EPSG:4326",
        precision: int = DEFAULT_PRECISION,
        densification_factor: Optional[int] = None,
        densification_distance: Optional[float] = None,
        simplify_tolerance: Optional[float] = None,
        no_data: Optional[Union[int, float]] = None,
        bands: List[int] = [1],
        skip_errors: bool = True,
        footprint_merge_strategy: FootprintMergeStrategy = FootprintMergeStrategy.FIRST,
    ) -> bool:
        """Accepts an Item and an optional list of asset names within that
        Item, and updates the geometry of that Item in-place with the data
        footprint derived from the first of the assets that exists in the Item.
        The Item bbox is also updated in-place to bound the new footprint
        extents.

        See :class:`RasterFootprint` for details on the data footprint
        calculation.

        Args:
            item (Item): The PySTAC Item to update.
            asset_names (List[str]): The names of the assets for which to attempt to
                extract footprints. The first successful footprint will be used. If
                the list is empty, all assets will be tried until one is successful.
            dst_crs (CRS): Coordinate reference system of the footprint data. Defaults
                to EPSG 4236.
            precision (int): The number of decimal places to include in the final
                footprint coordinates.
            densification_factor (Optional[int]): The factor by which to
                increase point density within the footprint polygon before
                projection. A factor of 2 would double the density
                of points (placing one new point between each existing pair of
                points), a factor of 3 would place two points between each point,
                etc. Higher densities produce higher fidelity footprints in
                areas of high projection distortion. Mutually exclusive with
                ``densification_distance``.
            densification_distance (Optional[float]): The distance by which to
                increase point density within the footprint polygon before
                projection. If the distance is set to 2 and the
                segment length between two polygon vertices is 10, 4 new
                vertices would be created along the segment. Higher densities
                produce higher fidelity footprints in areas of high projection
                distortion.  Mutually exclusive with ``densification_factor``.
            simplify_tolerance (Optional[float]): Distance, in degrees, within which
                all locations on the simplified polygon will be to the original
                polygon.
            no_data (Optional[Union[int, float]]): Explicitly sets the nodata value
                if not in source image metadata. If set to None, this will return
                a footprint including nodata values.
            bands (List[int]): The bands to use to compute the footprint.
                Defaults to [1]. If an empty list is provided, the bands will be ORd
                together; e.g., for a pixel to be outside of the footprint, all
                bands must have nodata in that pixel.
            skip_errors (bool): If False, raise an error for a missing href or
                footprint calculation failure.
            footprint_aggregator (FootprintMergeStrategy): Provides a
                means to control how the footprints of assets are aggregated;
                see :class:`FootprintMergeStrategy` for details; defaults to using
                the `FIRST` strategy

        Returns:
            bool: True if the Item geometry was successfully updated, False if not.
        """
        asset_extent_iterator = cls.data_footprints_for_data_assets(
            item,
            asset_names=asset_names,
            dst_crs=dst_crs,
            precision=precision,
            densification_factor=densification_factor,
            densification_distance=densification_distance,
            simplify_tolerance=simplify_tolerance,
            no_data=no_data,
            bands=bands,
            skip_errors=skip_errors,
        )

        if footprint_merge_strategy == FootprintMergeStrategy.FIRST:
            asset_name_extent = next(asset_extent_iterator, None)
            if asset_name_extent is None:
                return False
            _, extent = asset_name_extent
        else:
            extents = [shape(extent) for _, extent in asset_extent_iterator]
            if extents == []:
                return False
            if footprint_merge_strategy == FootprintMergeStrategy.INTERSECTION:
                extent = mutual_intersection(extents)
            elif footprint_merge_strategy == FootprintMergeStrategy.UNION:
                extent = unary_union(extents)
            else:
                raise Exception(
                    f"Unrecognized aggregation strategy: {footprint_merge_strategy}"
                )
        item.geometry = extent
        item.bbox = list(shape(extent).bounds)
        return True

    @classmethod
    def data_footprints_for_data_assets(
        cls,
        item: Item,
        *,
        asset_names: List[str] = [],
        dst_crs: CRS = "EPSG:4326",
        precision: int = DEFAULT_PRECISION,
        densification_factor: Optional[int] = None,
        densification_distance: Optional[float] = None,
        simplify_tolerance: Optional[float] = None,
        no_data: Optional[Union[int, float]] = None,
        bands: List[int] = [1],
        skip_errors: bool = True,
    ) -> Iterator[Tuple[str, Dict[str, Any]]]:
        """Accepts an Item and an optional list of asset names within that
        Item, and produces an iterator over the same asset names (if they
        exist) and dictionaries representing GeoJSON Polygons of the data
        footprints of the assets.

        See :class:`RasterFootprint` for details on the data footprint
        calculation.

        Args:
            item (Item): The PySTAC Item to update.
            asset_names (List[str]): The names of the assets for which to attempt to
                extract footprints. The first successful footprint will be used. If
                the list is empty, all assets will be tried until one is successful.
            dst_crs (CRS): Coordinate reference system of the footprint data. Defaults
                to EPSG 4236.
            precision (int): The number of decimal places to include in the final
                footprint coordinates.
            densification_factor (Optional[int]): The factor by which to
                increase point density within the footprint polygon before
                projection. A factor of 2 would double the density
                of points (placing one new point between each existing pair of
                points), a factor of 3 would place two points between each point,
                etc. Higher densities produce higher fidelity footprints in
                areas of high projection distortion. Mutually exclusive with
                ``densification_distance``.
            densification_distance (Optional[float]): The distance by which to
                increase point density within the footprint polygon before
                projection. If the distance is set to 2 and the
                segment length between two polygon vertices is 10, 4 new
                vertices would be created along the segment. Higher densities
                produce higher fidelity footprints in areas of high projection
                distortion.  Mutually exclusive with ``densification_factor``.
            simplify_tolerance (Optional[float]): Distance, in degrees, within which
                all locations on the simplified polygon will be to the original
                polygon.
            no_data (Optional[Union[int, float]]): Explicitly sets the nodata value
                if not in source image metadata. If set to None, this will return
                a footprint including nodata values.
            bands (List[int]): The bands to use to compute the footprint.
                Defaults to [1]. If an empty list is provided, the bands will be ORd
                together; e.g., for a pixel to be outside of the footprint, all
                bands must have nodata in that pixel.
            skip_errors (bool): If False, raise an error for a missing href or
                footprint calculation failure.

        Returns:
            Iterator[Tuple[str, Dict[str, Any]]]: Iterator of the asset name and
            dictionary representing a GeoJSON Polygon of the data footprint for
            each asset.
        """

        def handle_error(message: str) -> None:
            if skip_errors:
                logger.error(message)
            else:
                raise Exception(message)

        for name, asset in item.assets.items():
            if not asset_names or name in asset_names:
                href = asset.get_absolute_href()
                if href is None:
                    handle_error(
                        f"Could not determine extent for asset '{name}', missing href"
                    )
                else:
                    extent = cls.from_href(
                        href=href,
                        dst_crs=dst_crs,
                        no_data=no_data,
                        bands=bands,
                        precision=precision,
                        densification_factor=densification_factor,
                        densification_distance=densification_distance,
                        simplify_tolerance=simplify_tolerance,
                    ).footprint()
                    if extent:
                        yield name, extent
                    else:
                        handle_error(f"Could not determine extent for asset '{name}'")


def update_geometry_from_asset_footprint(
    item: Item,
    *,
    asset_names: List[str] = [],
    precision: int = DEFAULT_PRECISION,
    densification_factor: Optional[int] = None,
    simplify_tolerance: Optional[float] = None,
    no_data: Optional[Union[int, float]] = None,
    bands: List[int] = [1],
    skip_errors: bool = True,
) -> bool:
    """DEPRECATED.

    .. deprecated:: 0.4.4
        Use :meth:`RasterFootprint.update_geometry_from_asset_footprint`
        instead.

    Accepts an Item and an optional list of asset names within that Item, and
    updates the geometry of that Item in-place with the data footprint derived
    from the first of the assets that exists in the Item. The Item bbox is also
    updated in-place to bound the new footprint extents.

    See :class:`RasterFootprint` for details on the data footprint
    calculation.

    Args:
        item (Item): The PySTAC Item to update.
        asset_names (List[str]): The names of the assets for which to attempt to
            extract footprints. The first successful footprint will be used. If
            the list is empty, all assets will be tried until one is successful.
        precision (int): The number of decimal places to include in the final
            footprint coordinates.
        densification_factor (Optional[int]): The factor by which to
            increase point density within the polygon before projection to
            EPSG 4326. A factor of 2 would double the density of points (placing
            one new point between each existing pair of points), a factor of
            3 would place two points between each point, etc. Higher
            densities produce higher fidelity footprints in areas of high
            projection distortion.
        simplify_tolerance (Optional[float]): Distance, in degrees, within which
            all locations on the simplified polygon will be to the original
            polygon.
        no_data (Optional[Union[int, float]]): Explicitly sets the nodata value
            if not in source image metadata. If set to None, this will return
            a footprint including nodata values.
        bands (List[int]): The bands to use to compute the footprint.
            Defaults to [1]. If an empty list is provided, the bands will be ORd
            together; e.g., for a pixel to be outside of the footprint, all
            bands must have nodata in that pixel.
        skip_errors (bool): If False, raise an error for a missing href or
            footprint calculation failure.

    Returns:
        bool: True if the Item geometry was successfully updated, False if not.
    """
    warnings.warn(
        "update_geometry_from_asset_footprint() is deprecated, use "
        "RasterFootprint.update_geometry_from_asset_footprint() instead",
        DeprecationWarning,
    )
    return RasterFootprint.update_geometry_from_asset_footprint(
        item,
        asset_names=asset_names,
        precision=precision,
        densification_factor=densification_factor,
        simplify_tolerance=simplify_tolerance,
        no_data=no_data,
        bands=bands,
        skip_errors=skip_errors,
    )


def data_footprints_for_data_assets(
    item: Item,
    *,
    asset_names: List[str] = [],
    precision: int = DEFAULT_PRECISION,
    densification_factor: Optional[int] = None,
    simplify_tolerance: Optional[float] = None,
    no_data: Optional[Union[int, float]] = None,
    bands: List[int] = [1],
    skip_errors: bool = True,
) -> Iterator[Tuple[str, Dict[str, Any]]]:
    """DEPRECATED.

    .. deprecated:: 0.4.4
        Use :meth:`RasterFootprint.data_footprints_for_data_assets`
        instead.

    Accepts an Item and an optional list of asset names within that Item, and
    produces an iterator over the same asset names (if they exist) and
    dictionaries representing GeoJSON Polygons of the data footprints of the
    assets.

    See :class:`RasterFootprint` for details on the data footprint
    calculation.

    Args:
        item (Item): The PySTAC Item to update.
        asset_names (List[str]): The names of the assets for which to attempt to
            extract footprints. The first successful footprint will be used. If
            the list is empty, all assets will be tried until one is successful.
        precision (int): The number of decimal places to include in the final
            footprint coordinates.
        densification_factor (Optional[int]): The factor by which to
            increase point density within the polygon before projection to
            EPSG 4326. A factor of 2 would double the density of points (placing
            one new point between each existing pair of points), a factor of
            3 would place two points between each point, etc. Higher
            densities produce higher fidelity footprints in areas of high
            projection distortion.
        simplify_tolerance (Optional[float]): Distance, in degrees, within which
            all locations on the simplified polygon will be to the original
            polygon.
        no_data (Optional[Union[int, float]]): Explicitly sets the nodata value
            if not in source image metadata. If set to None, this will return
            a footprint including nodata values.
        bands (List[int]): The bands to use to compute the footprint.
            Defaults to [1]. If an empty list is provided, the bands will be ORd
            together; e.g., for a pixel to be outside of the footprint, all
            bands must have nodata in that pixel.
        skip_errors (bool): If False, raise an error for a missing href or
            footprint calculation failure.

    Returns:
        Iterator[Tuple[str, Dict[str, Any]]]: Iterator of the asset name and
        dictionary representing a GeoJSON Polygon of the data footprint for
        each asset.
    """
    warnings.warn(
        "data_footprints_for_data_assets() is deprecated, use "
        "RasterFootprint.data_footprints_for_data_assets() instead",
        DeprecationWarning,
    )
    return RasterFootprint.data_footprints_for_data_assets(
        item,
        asset_names=asset_names,
        precision=precision,
        densification_factor=densification_factor,
        simplify_tolerance=simplify_tolerance,
        no_data=no_data,
        bands=bands,
        skip_errors=skip_errors,
    )


def data_footprint(
    href: str,
    *,
    precision: int = DEFAULT_PRECISION,
    densification_factor: Optional[int] = None,
    simplify_tolerance: Optional[float] = None,
    no_data: Optional[Union[int, float]] = None,
    bands: List[int] = [1],
) -> Optional[Dict[str, Any]]:
    """DEPRECATED.

    .. deprecated:: 0.4.4
        Call :meth:`~RasterFootprint.footprint` on a :class:`RasterFootprint`
        instance created with :meth:`RasterFootprint.from_href` instead.

    Produces a data footprint from the href of a raster file.

    See :class:`RasterFootprint` for details on the data footprint
    calculation.

    Args:
        href (str): The href of the image to process.
        precision (int): The number of decimal places to include in the final
            footprint coordinates.
        densification_factor (Optional[int]): The factor by which to
            increase point density within the polygon before projection to
            EPSG 4326. A factor of 2 would double the density of points (placing
            one new point between each existing pair of points), a factor of
            3 would place two points between each point, etc. Higher
            densities produce higher fidelity footprints in areas of high
            projection distortion.
        simplify_tolerance (Optional[float]): Distance, in degrees, within which
            all locations on the simplified polygon will be to the original
            polygon.
        no_data (Optional[Union[int, float]]): Explicitly sets the nodata value
            if not in source image metadata. If set to None, this will return
            a footprint including nodata values.
        bands (List[int]): The bands to use to compute the footprint. Defaults
            to [1]. If an empty list is provided, the bands will be ORd
            together; e.g. for a pixel to be outside of the footprint, all bands
            must have nodata in that pixel.

    Returns:
        Optional[Dict[str, Any]]: A dictionary representing a GeoJSON Polygon of
        the data footprint of the raster data retrieved from the given ``href``.
    """
    warnings.warn(
        "data_footprint() is deprecated, use RasterFootprint.data_footprint() "
        "on a RasterFootprint instance created with "
        "RasterFootprint.from_href() instead",
        DeprecationWarning,
    )
    return RasterFootprint.from_href(
        href=href,
        no_data=no_data,
        bands=bands,
        precision=precision,
        densification_factor=densification_factor,
        simplify_tolerance=simplify_tolerance,
    ).footprint()


def densify_reproject_simplify(
    polygon: Polygon,
    crs: CRS,
    *,
    dst_crs: CRS = "EPSG:4326",
    densification_factor: Optional[int] = None,
    precision: int = DEFAULT_PRECISION,
    simplify_tolerance: Optional[float] = None,
) -> Polygon:
    """Densifies the input polygon, reprojects it, and simplifies
    the resulting polygon.

    See :class:`RasterFootprint` for details on densification and
    simplification.

    Args:
        polygon (Polygon): The input polygon.
        crs (CRS): The CRS of the input polygon.
        dst_crs (CRS): The CRS of the output polygon. Defaults to EPSG 4326
        densification_factor (Optional[int]): The factor by which to
            increase point density within the polygon before projection.
            A factor of 2 would double the density of points (placing
            one new point between each existing pair of points), a factor of
            3 would place two points between each point, etc. Higher
            densities produce higher fidelity footprints in areas of high
            projection distortion.
        precision (int): The number of decimal places to include in the final
            polygon vertex coordinates.
        simplify_tolerance (Optional[float]): Distance, in degrees, within which
            all locations on the simplified polygon will be to the original
            polygon.

    Returns:
        Polygon: The reprojected Polygon.
    """
    if densification_factor is not None:
        polygon = Polygon(
            densify_by_factor(polygon.exterior.coords, factor=densification_factor)
        )
    polygon = reproject_shape(
        geom=polygon, src_crs=crs, dst_crs=dst_crs, precision=precision
    )
    if simplify_tolerance is not None:
        polygon = polygon.simplify(
            tolerance=simplify_tolerance, preserve_topology=False
        )
    return polygon
