# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Tools for converting between `regions.Region` and Aperture objects and
between `shapely.Polygon` and `regions.PolygonRegion` objects.
"""

import astropy.units as u
import numpy as np

from photutils.aperture.circle import (CircularAnnulus, CircularAperture,
                                       SkyCircularAnnulus, SkyCircularAperture)
from photutils.aperture.core import Aperture
from photutils.aperture.ellipse import (EllipticalAnnulus, EllipticalAperture,
                                        SkyEllipticalAnnulus,
                                        SkyEllipticalAperture)
from photutils.aperture.rectangle import (RectangularAnnulus,
                                          RectangularAperture,
                                          SkyRectangularAnnulus,
                                          SkyRectangularAperture)

__all__ = ['aperture_to_region', 'region_to_aperture']

__doctest_requires__ = {'region_to_aperture': ['regions >= 0.12.dev'],
                        'aperture_to_region': ['regions >= 0.12.dev'],
                        '_scalar_aperture_to_region': ['regions >= 0.12.dev'],
                        '_shapely_polygon_to_region': ['regions >= 0.12.dev',
                                                       'shapely']}


def region_to_aperture(region):
    """
    Convert a given `regions.Region` object to an
    `~photutils.aperture.Aperture` object.

    Parameters
    ----------
    region : `regions.Region`
        A supported `regions.Region` object.

    Returns
    -------
    aperture : `~photutils.aperture.Aperture`
        An equivalent ``photutils`` aperture.

    Raises
    ------
    `TypeError`
        The given `regions.Region` object is not supported.

    Notes
    -----
    The ellipse ``width`` and ``height`` region parameters represent
    the full extent of the shapes and thus are divided by 2 when
    converting to elliptical aperture objects, which are defined using
    the semi-major (``a``) and semi-minor (``b``) axes. The ``width``
    and ``height`` parameters are mapped to the semi-major (``a``) and
    semi-minor (``b``) axes parameters, respectively, of the elliptical
    apertures.

    The region ``angle`` for sky-based regions is defined as the angle
    of the ``width`` axis relative to WCS longitude axis (PA=90).
    However, the sky-based apertures define the ``theta`` as the
    position angle of the semimajor axis relative to the North celestial
    pole (PA=0). Therefore, for sky-based regions the region ``angle``
    is converted to the aperture ``theta`` parameter by subtracting 90
    degrees.

    .. |rarr| unicode:: U+0279E .. RIGHTWARDS ARROW

    The following `regions.Region` objects are supported, shown with
    their equivalent `~photutils.aperture.Aperture` object:

    * `~regions.CirclePixelRegion` |rarr|
      `~photutils.aperture.CircularAperture`
    * `~regions.CircleSkyRegion` |rarr|
      `~photutils.aperture.SkyCircularAperture`
    * `~regions.EllipsePixelRegion` |rarr|
      `~photutils.aperture.EllipticalAperture`
    * `~regions.EllipseSkyRegion` |rarr|
      `~photutils.aperture.SkyEllipticalAperture`
    * `~regions.RectanglePixelRegion` |rarr|
      `~photutils.aperture.RectangularAperture`
    * `~regions.RectangleSkyRegion` |rarr|
      `~photutils.aperture.SkyRectangularAperture`
    * `~regions.CircleAnnulusPixelRegion` |rarr|
      `~photutils.aperture.CircularAnnulus`
    * `~regions.CircleAnnulusSkyRegion` |rarr|
      `~photutils.aperture.SkyCircularAnnulus`
    * `~regions.EllipseAnnulusPixelRegion` |rarr|
      `~photutils.aperture.EllipticalAnnulus`
    * `~regions.EllipseAnnulusSkyRegion` |rarr|
      `~photutils.aperture.SkyEllipticalAnnulus`
    * `~regions.RectangleAnnulusPixelRegion` |rarr|
      `~photutils.aperture.RectangularAnnulus`
    * `~regions.RectangleAnnulusSkyRegion` |rarr|
      `~photutils.aperture.SkyRectangularAnnulus`

    Examples
    --------
    >>> from regions import CirclePixelRegion, PixCoord
    >>> from photutils.aperture import region_to_aperture
    >>> region = CirclePixelRegion(center=PixCoord(x=10, y=20), radius=5)
    >>> aperture = region_to_aperture(region)
    >>> aperture
    <CircularAperture([10., 20.], r=5.0)>
    """
    from regions import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion,
                         CirclePixelRegion, CircleSkyRegion,
                         EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion,
                         EllipsePixelRegion, EllipseSkyRegion,
                         RectangleAnnulusPixelRegion,
                         RectangleAnnulusSkyRegion, RectanglePixelRegion,
                         RectangleSkyRegion, Region)

    if not isinstance(region, Region):
        msg = 'Input region must be a Region object'
        raise TypeError(msg)

    if isinstance(region, CirclePixelRegion):
        aperture = CircularAperture(region.center.xy, region.radius)

    elif isinstance(region, CircleSkyRegion):
        aperture = SkyCircularAperture(region.center, region.radius)

    elif isinstance(region, EllipsePixelRegion):
        aperture = EllipticalAperture(
            region.center.xy, region.width * 0.5, region.height * 0.5,
            theta=region.angle)

    elif isinstance(region, EllipseSkyRegion):
        aperture = SkyEllipticalAperture(
            region.center, region.width * 0.5, region.height * 0.5,
            theta=(region.angle - (90 * u.deg)))

    elif isinstance(region, RectanglePixelRegion):
        aperture = RectangularAperture(
            region.center.xy, region.width, region.height,
            theta=region.angle)

    elif isinstance(region, RectangleSkyRegion):
        aperture = SkyRectangularAperture(
            region.center, region.width, region.height,
            theta=(region.angle - (90 * u.deg)))

    elif isinstance(region, CircleAnnulusPixelRegion):
        aperture = CircularAnnulus(
            region.center.xy, region.inner_radius, region.outer_radius)

    elif isinstance(region, CircleAnnulusSkyRegion):
        aperture = SkyCircularAnnulus(
            region.center, region.inner_radius, region.outer_radius)

    elif isinstance(region, EllipseAnnulusPixelRegion):
        aperture = EllipticalAnnulus(
            region.center.xy,
            region.inner_width * 0.5, region.outer_width * 0.5,
            region.outer_height * 0.5, b_in=region.inner_height * 0.5,
            theta=region.angle)

    elif isinstance(region, EllipseAnnulusSkyRegion):
        aperture = SkyEllipticalAnnulus(
            region.center,
            region.inner_width * 0.5, region.outer_width * 0.5,
            region.outer_height * 0.5, b_in=region.inner_height * 0.5,
            theta=(region.angle - (90 * u.deg)))

    elif isinstance(region, RectangleAnnulusPixelRegion):
        aperture = RectangularAnnulus(
            region.center.xy,
            region.inner_width, region.outer_width,
            region.outer_height, h_in=region.inner_height,
            theta=region.angle)

    elif isinstance(region, RectangleAnnulusSkyRegion):
        aperture = SkyRectangularAnnulus(
            region.center,
            region.inner_width, region.outer_width,
            region.outer_height, h_in=region.inner_height,
            theta=(region.angle - (90 * u.deg)))

    else:
        msg = (f'Cannot convert {region.__class__.__name__!r} to an '
               'Aperture object')
        raise TypeError(msg)

    return aperture


def aperture_to_region(aperture):
    """
    Convert a given `~photutils.aperture.Aperture` object to a
    `regions.Region` or `regions.Regions` object.

    Because a `regions.Region` object can only have one position, a
    `regions.Regions` object will be returned if the input ``aperture``
    has more than one position. Otherwise, a `regions.Region` object
    will be returned.

    Parameters
    ----------
    aperture : `~photutils.aperture.Aperture`
        An `~photutils.aperture.Aperture` object to convert.

    Returns
    -------
    region : `regions.Region` or `regions.Regions`
        An equivalent `regions.Region` object. If the input ``aperture``
        has more than one position then a `regions.Regions` will be
        returned.

    Notes
    -----
    The elliptical aperture ``a`` and ``b`` parameters represent the
    semi-major and semi-minor axes, respectively. The ``a`` and ``b``
    parameters are mapped to the ellipse ``width`` and ``height`` region
    parameters, respectively, by multiplying by 2 because they represent
    the full extent of the ellipse.

    The region ``angle`` for sky-based regions is defined as the angle
    of the ``width`` axis relative to WCS longitude axis (PA=90).
    However, the sky-based apertures define the ``theta`` as the
    position angle of the semimajor axis relative to the North celestial
    pole (PA=0). Therefore, for sky-based apertures the ``theta``
    parameter is converted to the region ``angle`` by adding 90 degrees.

    .. |rarr| unicode:: U+0279E .. RIGHTWARDS ARROW

    The following `~photutils.aperture.Aperture` objects are supported,
    shown with their equivalent `regions.Region` object:

    * `~photutils.aperture.CircularAperture` |rarr|
      `~regions.CirclePixelRegion`
    * `~photutils.aperture.SkyCircularAperture` |rarr|
      `~regions.CircleSkyRegion`
    * `~photutils.aperture.EllipticalAperture` |rarr|
      `~regions.EllipsePixelRegion`
    * `~photutils.aperture.SkyEllipticalAperture` |rarr|
      `~regions.EllipseSkyRegion`
    * `~photutils.aperture.RectangularAperture` |rarr|
      `~regions.RectanglePixelRegion`
    * `~photutils.aperture.SkyRectangularAperture` |rarr|
      `~regions.RectangleSkyRegion`
    * `~photutils.aperture.CircularAnnulus` |rarr|
      `~regions.CircleAnnulusPixelRegion`
    * `~photutils.aperture.SkyCircularAnnulus` |rarr|
      `~regions.CircleAnnulusSkyRegion`
    * `~photutils.aperture.EllipticalAnnulus` |rarr|
      `~regions.EllipseAnnulusPixelRegion`
    * `~photutils.aperture.SkyEllipticalAnnulus` |rarr|
      `~regions.EllipseAnnulusSkyRegion`
    * `~photutils.aperture.RectangularAnnulus` |rarr|
      `~regions.RectangleAnnulusPixelRegion`
    * `~photutils.aperture.SkyRectangularAnnulus` |rarr|
      `~regions.RectangleAnnulusSkyRegion`

    Examples
    --------
    >>> from photutils.aperture import CircularAperture, aperture_to_region
    >>> aperture = CircularAperture((10, 20), r=5)
    >>> region = aperture_to_region(aperture)
    >>> region
    <CirclePixelRegion(center=PixCoord(x=10.0, y=20.0), radius=5.0)>

    >>> aperture = CircularAperture(((10, 20), (30, 40)), r=5)
    >>> region = aperture_to_region(aperture)
    >>> region
    <Regions([
      <CirclePixelRegion(center=PixCoord(x=10.0, y=20.0), radius=5.0)>,
      <CirclePixelRegion(center=PixCoord(x=30.0, y=40.0), radius=5.0)>
    ])>
    """
    from regions import Regions

    if not isinstance(aperture, Aperture):
        msg = 'Input aperture must be an Aperture object'
        raise TypeError(msg)

    if aperture.shape == ():
        return _scalar_aperture_to_region(aperture)

    # Multiple aperture positions return a Regions object
    regs = [_scalar_aperture_to_region(aper) for aper in aperture]
    return Regions(regs)


def _scalar_aperture_to_region(aperture):
    """
    Convert a given scalar `~photutils.aperture.Aperture` object to a
    `regions.Region` object.

    Parameters
    ----------
    aperture : `~photutils.aperture.Aperture`
        An `~photutils.aperture.Aperture` object to convert. The
        ``aperture`` must have a single position (scalar).

    Returns
    -------
    region : `regions.Region` or `regions.Regions`
        An equivalent `regions.Region` object.
    """
    from regions import (CircleAnnulusPixelRegion, CircleAnnulusSkyRegion,
                         CirclePixelRegion, CircleSkyRegion,
                         EllipseAnnulusPixelRegion, EllipseAnnulusSkyRegion,
                         EllipsePixelRegion, EllipseSkyRegion, PixCoord,
                         RectangleAnnulusPixelRegion,
                         RectangleAnnulusSkyRegion, RectanglePixelRegion,
                         RectangleSkyRegion)

    if aperture.shape != ():
        msg = 'Only scalar (single-position) apertures are supported'
        raise ValueError(msg)

    if isinstance(aperture, CircularAperture):
        region = CirclePixelRegion(PixCoord(*aperture.positions), aperture.r)

    elif isinstance(aperture, SkyCircularAperture):
        region = CircleSkyRegion(aperture.positions, aperture.r)

    elif isinstance(aperture, EllipticalAperture):
        region = EllipsePixelRegion(
            PixCoord(*aperture.positions), aperture.a * 2, aperture.b * 2,
            angle=aperture.theta)

    elif isinstance(aperture, SkyEllipticalAperture):
        region = EllipseSkyRegion(
            aperture.positions, aperture.a * 2, aperture.b * 2,
            angle=(aperture.theta + (90 * u.deg)))

    elif isinstance(aperture, RectangularAperture):
        region = RectanglePixelRegion(
            PixCoord(*aperture.positions), aperture.w, aperture.h,
            angle=aperture.theta)

    elif isinstance(aperture, SkyRectangularAperture):
        region = RectangleSkyRegion(
            aperture.positions, aperture.w, aperture.h,
            angle=(aperture.theta + (90 * u.deg)))

    elif isinstance(aperture, CircularAnnulus):
        region = CircleAnnulusPixelRegion(
            PixCoord(*aperture.positions), aperture.r_in, aperture.r_out)

    elif isinstance(aperture, SkyCircularAnnulus):
        region = CircleAnnulusSkyRegion(
            aperture.positions, aperture.r_in, aperture.r_out)

    elif isinstance(aperture, EllipticalAnnulus):
        region = EllipseAnnulusPixelRegion(
            PixCoord(*aperture.positions),
            aperture.a_in * 2, aperture.a_out * 2,
            aperture.b_in * 2, aperture.b_out * 2,
            angle=aperture.theta)

    elif isinstance(aperture, SkyEllipticalAnnulus):
        region = EllipseAnnulusSkyRegion(
            aperture.positions,
            aperture.a_in * 2, aperture.a_out * 2,
            aperture.b_in * 2, aperture.b_out * 2,
            angle=(aperture.theta + (90 * u.deg)))

    elif isinstance(aperture, RectangularAnnulus):
        region = RectangleAnnulusPixelRegion(
            PixCoord(*aperture.positions),
            aperture.w_in, aperture.w_out,
            aperture.h_in, aperture.h_out,
            angle=aperture.theta)

    elif isinstance(aperture, SkyRectangularAnnulus):
        region = RectangleAnnulusSkyRegion(
            aperture.positions,
            aperture.w_in, aperture.w_out,
            aperture.h_in, aperture.h_out,
            angle=(aperture.theta + (90 * u.deg)))

    else:
        msg = 'Cannot convert input aperture to a Region object'
        raise TypeError(msg)

    return region


def _shapely_polygon_to_region(polygon, *, label=None, visual_kwargs=None):
    """
    Convert a `shapely.Polygon` object to a `regions.PolygonPixelRegion`
    object.

    Parameters
    ----------
    polygon : `shapely.Polygon` or `shapely.MultiPolygon`
        A Shapely Polygon or MultiPolygon object.

    label : str or `None`, optional
        A label for the region. If provided, it will be stored in the
        meta attribute of the returned `regions.PolygonPixelRegion`
        objects.

    visual_kwargs : dict or `None`, optional
        A dictionary of visual keyword arguments to pass to
        `regions.RegionVisual`. If provided, the visual attributes will
        be set on the returned region(s).

    Returns
    -------
    result : list of `regions.PolygonPixelRegion` or `regions.Regions`
        If the polygon is a `shapely.Polygon`, then a
        `regions.PolygonPixelRegion` object is returned. If the polygon
        is a `shapely.MultiPolygon`, then a `regions.Regions` object is
        returned containing one or more `regions.PolygonPixelRegion`
        objects.

    Notes
    -----
    The `regions.PolygonPixelRegion` object does not include the
    last Shapely vertex, which is the same as the first vertex. The
    `regions.PolygonPixelRegion` does not need to include the last
    vertex to close the polygon.

    Examples
    --------
    >>> from shapely import Polygon
    >>> from photutils.aperture.converters import _shapely_polygon_to_region
    >>> polygon = Polygon([(1, 1), (3, 1), (2, 4), (1, 2)])
    >>> region = _shapely_polygon_to_region(polygon)
    >>> region
    <PolygonPixelRegion(vertices=PixCoord(x=[1.0 3.0 2.0 1.0],
                                          y=[1.0 1.0 4.0 2.0]))>
    """
    from regions import PixCoord, PolygonPixelRegion, Regions, RegionVisual
    from shapely import MultiPolygon, Polygon

    meta = {'label': label} if label is not None else None
    visual = RegionVisual(visual_kwargs) if visual_kwargs else None

    if isinstance(polygon, Polygon):
        x, y = np.transpose(polygon.exterior.coords[:-1])
        return PolygonPixelRegion(vertices=PixCoord(x=x, y=y), meta=meta,
                                  visual=visual)
    if isinstance(polygon, MultiPolygon):
        geoms = []
        for poly in polygon.geoms:
            x, y = np.transpose(poly.exterior.coords[:-1])
            geoms.append(PolygonPixelRegion(vertices=PixCoord(x=x, y=y),
                                            meta=meta, visual=visual))
        return Regions(geoms)
    msg = 'Input must be a Polygon or MultiPolygon object'
    raise TypeError(msg)
